diff --git a/src/components/quota/QuotaCard.tsx b/src/components/quota/QuotaCard.tsx
index 6c1f3f23..b1b6ca8d 100644
--- a/src/components/quota/QuotaCard.tsx
+++ b/src/components/quota/QuotaCard.tsx
@@ -41,7 +41,7 @@ export function QuotaProgressBar({
: normalized >= mediumThreshold
? styles.quotaBarFillMedium
: styles.quotaBarFillLow;
- const widthPercent = Math.round(normalized ?? 0);
+ const widthPercent = Math.round((normalized ?? 0) * 100) / 100;
return (
diff --git a/src/components/quota/quotaConfigs.ts b/src/components/quota/quotaConfigs.ts
index d5f81dd5..2f8be849 100644
--- a/src/components/quota/quotaConfigs.ts
+++ b/src/components/quota/quotaConfigs.ts
@@ -7,7 +7,7 @@ import type { ReactNode } from 'react';
import type { TFunction } from 'i18next';
import type {
AntigravityQuotaGroup,
- AntigravityModelsPayload,
+ AntigravityQuotaSummaryPayload,
AntigravityQuotaState,
AuthFileItem,
ClaudeExtraUsage,
@@ -93,7 +93,11 @@ type QuotaUpdater
= T | ((prev: T) => T);
type QuotaType = 'antigravity' | 'claude' | 'codex' | 'gemini-cli' | 'kimi' | 'xai';
-const DEFAULT_ANTIGRAVITY_PROJECT_ID = 'bamboo-precept-lgxtn';
+type AntigravityQuotaData = {
+ groups: AntigravityQuotaGroup[];
+ serverTimeOffsetMs: number | null;
+};
+
const QUOTA_PROGRESS_HIGH_THRESHOLD = 70;
const QUOTA_PROGRESS_MEDIUM_THRESHOLD = 30;
const geminiCliSupplementaryRequestIds = new Map();
@@ -144,10 +148,33 @@ export interface QuotaConfig {
}
const resolveAntigravityProjectId = async (file: AuthFileItem): Promise => {
+ const directProjectId = normalizeStringValue(file.project_id ?? file.projectId);
+ if (directProjectId) return directProjectId;
+
+ const metadata =
+ file.metadata && typeof file.metadata === 'object' && file.metadata !== null
+ ? (file.metadata as Record)
+ : null;
+ const metadataProjectId = metadata
+ ? normalizeStringValue(metadata.project_id ?? metadata.projectId)
+ : null;
+ if (metadataProjectId) return metadataProjectId;
+
+ const attributes =
+ file.attributes && typeof file.attributes === 'object' && file.attributes !== null
+ ? (file.attributes as Record)
+ : null;
+ const attributesProjectId = attributes
+ ? normalizeStringValue(
+ attributes.project_id ?? attributes.projectId ?? attributes.gemini_virtual_project
+ )
+ : null;
+ if (attributesProjectId) return attributesProjectId;
+
try {
const text = await authFilesApi.downloadText(file.name);
const trimmed = text.trim();
- if (!trimmed) return DEFAULT_ANTIGRAVITY_PROJECT_ID;
+ if (!trimmed) return '';
const parsed = JSON.parse(trimmed) as Record;
const topLevel = normalizeStringValue(parsed.project_id ?? parsed.projectId);
@@ -169,16 +196,28 @@ const resolveAntigravityProjectId = async (file: AuthFileItem): Promise
const webProjectId = web ? normalizeStringValue(web.project_id ?? web.projectId) : null;
if (webProjectId) return webProjectId;
} catch {
- return DEFAULT_ANTIGRAVITY_PROJECT_ID;
+ return '';
}
- return DEFAULT_ANTIGRAVITY_PROJECT_ID;
+ return '';
+};
+
+const resolveResponseServerTimeOffsetMs = (
+ header: Record | undefined
+): number | null => {
+ if (!header) return null;
+ const dateEntry = Object.entries(header).find(([key]) => key.toLowerCase() === 'date');
+ const rawDate = dateEntry?.[1]?.[0];
+ if (!rawDate) return null;
+ const serverTime = new Date(rawDate).getTime();
+ if (Number.isNaN(serverTime)) return null;
+ return serverTime - Date.now();
};
const fetchAntigravityQuota = async (
file: AuthFileItem,
t: TFunction
-): Promise => {
+): Promise => {
const rawAuthIndex = file['auth_index'] ?? file.authIndex;
const authIndex = normalizeAuthIndex(rawAuthIndex);
if (!authIndex) {
@@ -186,6 +225,9 @@ const fetchAntigravityQuota = async (
}
const projectId = await resolveAntigravityProjectId(file);
+ if (!projectId) {
+ throw new Error(t('antigravity_quota.missing_project_id'));
+ }
const requestBody = JSON.stringify({ project: projectId });
let lastError = '';
@@ -213,20 +255,24 @@ const fetchAntigravityQuota = async (
}
hadSuccess = true;
- const payload = parseAntigravityPayload(result.body ?? result.bodyText);
- const models = payload?.models;
- if (!models || typeof models !== 'object' || Array.isArray(models)) {
+ const payload = parseAntigravityPayload(
+ result.body ?? result.bodyText
+ ) as AntigravityQuotaSummaryPayload | null;
+ if (!payload || !Array.isArray(payload.groups)) {
lastError = t('antigravity_quota.empty_models');
continue;
}
- const groups = buildAntigravityQuotaGroups(models as AntigravityModelsPayload);
+ const groups = buildAntigravityQuotaGroups(payload);
if (groups.length === 0) {
lastError = t('antigravity_quota.empty_models');
continue;
}
- return groups;
+ return {
+ groups,
+ serverTimeOffsetMs: resolveResponseServerTimeOffsetMs(result.header),
+ };
} catch (err: unknown) {
lastError = err instanceof Error ? err.message : t('common.unknown_error');
const status = getStatusFromError(err);
@@ -240,7 +286,7 @@ const fetchAntigravityQuota = async (
}
if (hadSuccess) {
- return [];
+ return { groups: [], serverTimeOffsetMs: null };
}
throw createStatusError(lastError || t('common.unknown_error'), priorityStatus ?? lastStatus);
@@ -825,45 +871,116 @@ const fetchGeminiCliQuota = async (
};
};
+const formatAntigravityDuration = (t: TFunction, deltaMs: number): string => {
+ const totalMinutes = Math.max(1, Math.ceil(deltaMs / 60000));
+ const days = Math.floor(totalMinutes / 1440);
+ const hours = Math.floor((totalMinutes % 1440) / 60);
+ const minutes = totalMinutes % 60;
+
+ if (days > 0) {
+ return t('antigravity_quota.duration_day_hour', {
+ days,
+ hours,
+ });
+ }
+ if (hours > 0) {
+ return t('antigravity_quota.duration_hour_minute', {
+ hours,
+ minutes,
+ });
+ }
+ if (minutes > 0) {
+ return t('antigravity_quota.duration_minute', {
+ minutes,
+ });
+ }
+ return t('antigravity_quota.duration_less_than_minute');
+};
+
+const formatAntigravityResetLabel = (
+ resetTime: string | undefined,
+ t: TFunction,
+ nowMs: number
+): string => {
+ if (!resetTime) return '-';
+ const resetMs = new Date(resetTime).getTime();
+ if (Number.isNaN(resetMs)) return '-';
+ const deltaMs = resetMs - nowMs;
+ if (deltaMs <= 0) return t('antigravity_quota.refresh_available');
+ return t('antigravity_quota.refreshes_in', {
+ duration: formatAntigravityDuration(t, deltaMs),
+ });
+};
+
const renderAntigravityItems = (
quota: AntigravityQuotaState,
t: TFunction,
helpers: QuotaRenderHelpers
): ReactNode => {
const { styles: styleMap, QuotaProgressBar } = helpers;
- const { createElement: h } = React;
+ const { createElement: h, Fragment } = React;
const groups = quota.groups ?? [];
if (groups.length === 0) {
return h('div', { className: styleMap.quotaMessage }, t('antigravity_quota.empty_models'));
}
- return groups.map((group) => {
- const clamped = Math.max(0, Math.min(1, group.remainingFraction));
- const percent = Math.round(clamped * 100);
- const resetLabel = formatQuotaResetTime(group.resetTime);
+ const nowMs = Date.now() + (quota.serverTimeOffsetMs ?? 0);
- return h(
- 'div',
- { key: group.id, className: styleMap.quotaRow },
+ return h(
+ Fragment,
+ null,
+ ...groups.map((group) =>
h(
'div',
- { className: styleMap.quotaRowHeader },
- h('span', { className: styleMap.quotaModel, title: group.models.join(', ') }, group.label),
+ { key: group.id, className: styleMap.antigravityQuotaGroup },
h(
'div',
- { className: styleMap.quotaMeta },
- h('span', { className: styleMap.quotaPercent }, `${percent}%`),
- h('span', { className: styleMap.quotaReset }, resetLabel)
- )
- ),
- h(QuotaProgressBar, {
- percent,
- highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
- mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
- })
- );
- });
+ { className: styleMap.antigravityQuotaGroupHeader },
+ h('span', { className: styleMap.antigravityQuotaGroupTitle }, group.label),
+ group.description
+ ? h('span', { className: styleMap.antigravityQuotaGroupDescription }, group.description)
+ : null
+ ),
+ ...group.buckets.map((bucket) => {
+ const clamped = Math.max(0, Math.min(1, bucket.remainingFraction));
+ const percent = clamped * 100;
+ const percentLabel =
+ bucket.remainingFraction === 1
+ ? t('antigravity_quota.quota_available')
+ : t('antigravity_quota.remaining_percent', {
+ percent: Math.round(percent),
+ });
+ const resetLabel = formatAntigravityResetLabel(bucket.resetTime, t, nowMs);
+
+ return h(
+ 'div',
+ { key: bucket.id, className: styleMap.quotaRow },
+ h(
+ 'div',
+ { className: styleMap.quotaRowHeader },
+ h(
+ 'span',
+ { className: styleMap.quotaModel, title: bucket.description },
+ bucket.label
+ ),
+ h(
+ 'div',
+ { className: styleMap.quotaMeta },
+ h('span', { className: styleMap.quotaPercent }, percentLabel),
+ h('span', { className: styleMap.quotaReset }, resetLabel)
+ )
+ ),
+ h(QuotaProgressBar, {
+ percent,
+ highThreshold: QUOTA_PROGRESS_HIGH_THRESHOLD,
+ mediumThreshold: QUOTA_PROGRESS_MEDIUM_THRESHOLD,
+ })
+ );
+ })
+ )
+ )
+ );
};
const PREMIUM_GEMINI_CLI_TIER_IDS = new Set(['g1-ultra-tier']);
@@ -1343,7 +1460,7 @@ export const CLAUDE_CONFIG: QuotaConfig<
renderQuotaItems: renderClaudeItems,
};
-export const ANTIGRAVITY_CONFIG: QuotaConfig = {
+export const ANTIGRAVITY_CONFIG: QuotaConfig = {
type: 'antigravity',
i18nPrefix: 'antigravity_quota',
cardIdleMessageKey: 'quota_management.card_idle_hint',
@@ -1351,11 +1468,16 @@ export const ANTIGRAVITY_CONFIG: QuotaConfig state.antigravityQuota,
storeSetter: 'setAntigravityQuota',
- buildLoadingState: () => ({ status: 'loading', groups: [] }),
- buildSuccessState: (groups) => ({ status: 'success', groups }),
+ buildLoadingState: () => ({ status: 'loading', groups: [], serverTimeOffsetMs: null }),
+ buildSuccessState: (data) => ({
+ status: 'success',
+ groups: data.groups,
+ serverTimeOffsetMs: data.serverTimeOffsetMs,
+ }),
buildErrorState: (message, status) => ({
status: 'error',
groups: [],
+ serverTimeOffsetMs: null,
error: message,
errorStatus: status,
}),
diff --git a/src/features/authFiles/components/QuotaProgressBar.tsx b/src/features/authFiles/components/QuotaProgressBar.tsx
index 7ae10e98..4119d502 100644
--- a/src/features/authFiles/components/QuotaProgressBar.tsx
+++ b/src/features/authFiles/components/QuotaProgressBar.tsx
@@ -17,7 +17,7 @@ export function QuotaProgressBar({ percent, highThreshold, mediumThreshold }: Qu
: normalized >= mediumThreshold
? styles.quotaBarFillMedium
: styles.quotaBarFillLow;
- const widthPercent = Math.round(normalized ?? 0);
+ const widthPercent = Math.round((normalized ?? 0) * 100) / 100;
return (
@@ -25,4 +25,3 @@ export function QuotaProgressBar({ percent, highThreshold, mediumThreshold }: Qu
);
}
-
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
index 7ed094d2..76236b63 100644
--- a/src/i18n/locales/en.json
+++ b/src/i18n/locales/en.json
@@ -386,7 +386,16 @@
"loading": "Loading quota...",
"load_failed": "Failed to load quota: {{message}}",
"missing_auth_index": "Auth file missing auth_index",
+ "missing_project_id": "Antigravity credential missing project_id. Re-login or refresh the credential to discover the project.",
"empty_models": "No quota data available",
+ "quota_available": "Quota available",
+ "remaining_percent": "{{percent}}% remaining",
+ "refreshes_in": "Refreshes in {{duration}}",
+ "refresh_available": "Refresh available",
+ "duration_day_hour": "{{days}}d {{hours}}h",
+ "duration_hour_minute": "{{hours}}h {{minutes}}m",
+ "duration_minute": "{{minutes}}m",
+ "duration_less_than_minute": "<1m",
"refresh_button": "Refresh Quota",
"fetch_all": "Fetch All"
},
diff --git a/src/i18n/locales/ru.json b/src/i18n/locales/ru.json
index 68d963f2..c09798fc 100644
--- a/src/i18n/locales/ru.json
+++ b/src/i18n/locales/ru.json
@@ -380,7 +380,16 @@
"loading": "Загрузка квоты...",
"load_failed": "Не удалось загрузить квоту: {{message}}",
"missing_auth_index": "В файле авторизации отсутствует auth_index",
+ "missing_project_id": "В учётных данных Antigravity отсутствует project_id. Войдите заново или обновите учётные данные, чтобы определить проект.",
"empty_models": "Данные по квоте отсутствуют",
+ "quota_available": "Квота доступна",
+ "remaining_percent": "Осталось {{percent}}%",
+ "refreshes_in": "Обновится через {{duration}}",
+ "refresh_available": "Можно обновить",
+ "duration_day_hour": "{{days}} д {{hours}} ч",
+ "duration_hour_minute": "{{hours}} ч {{minutes}} мин",
+ "duration_minute": "{{minutes}} мин",
+ "duration_less_than_minute": "<1 мин",
"refresh_button": "Обновить квоту",
"fetch_all": "Получить все"
},
diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json
index 9c4432b5..a922ec0b 100644
--- a/src/i18n/locales/zh-CN.json
+++ b/src/i18n/locales/zh-CN.json
@@ -386,7 +386,16 @@
"loading": "正在加载额度...",
"load_failed": "额度获取失败:{{message}}",
"missing_auth_index": "认证文件缺少 auth_index",
+ "missing_project_id": "Antigravity 凭证缺少 project_id。请重新登录或刷新凭证以发现项目。",
"empty_models": "暂无额度数据",
+ "quota_available": "额度可用",
+ "remaining_percent": "剩余 {{percent}}%",
+ "refreshes_in": "{{duration}} 后刷新",
+ "refresh_available": "可刷新",
+ "duration_day_hour": "{{days}} 天 {{hours}} 小时",
+ "duration_hour_minute": "{{hours}} 小时 {{minutes}} 分钟",
+ "duration_minute": "{{minutes}} 分钟",
+ "duration_less_than_minute": "小于 1 分钟",
"refresh_button": "刷新额度",
"fetch_all": "获取全部"
},
diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json
index 414391fa..d8e0e11e 100644
--- a/src/i18n/locales/zh-TW.json
+++ b/src/i18n/locales/zh-TW.json
@@ -386,7 +386,16 @@
"loading": "正在載入配額...",
"load_failed": "配額取得失敗:{{message}}",
"missing_auth_index": "驗證檔案缺少 auth_index",
+ "missing_project_id": "Antigravity 憑證缺少 project_id。請重新登入或重新整理憑證以探索專案。",
"empty_models": "暫無配額資料",
+ "quota_available": "配額可用",
+ "remaining_percent": "剩餘 {{percent}}%",
+ "refreshes_in": "{{duration}} 後重新整理",
+ "refresh_available": "可重新整理",
+ "duration_day_hour": "{{days}} 天 {{hours}} 小時",
+ "duration_hour_minute": "{{hours}} 小時 {{minutes}} 分鐘",
+ "duration_minute": "{{minutes}} 分鐘",
+ "duration_less_than_minute": "小於 1 分鐘",
"refresh_button": "重新整理配額",
"fetch_all": "取得全部"
},
diff --git a/src/pages/AuthFilesPage.module.scss b/src/pages/AuthFilesPage.module.scss
index fc917c1d..12a8aa68 100644
--- a/src/pages/AuthFilesPage.module.scss
+++ b/src/pages/AuthFilesPage.module.scss
@@ -536,6 +536,37 @@
gap: $spacing-xs;
}
+.antigravityQuotaGroup {
+ display: flex;
+ flex-direction: column;
+ gap: $spacing-sm;
+ padding-bottom: $spacing-xs;
+}
+
+.antigravityQuotaGroup + .antigravityQuotaGroup {
+ padding-top: $spacing-sm;
+ border-top: 1px solid color-mix(in srgb, var(--border-color) 65%, transparent);
+}
+
+.antigravityQuotaGroupHeader {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.antigravityQuotaGroupTitle {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.antigravityQuotaGroupDescription {
+ font-size: 12px;
+ line-height: 1.35;
+ color: var(--text-tertiary);
+}
+
.quotaRowHeader {
display: flex;
align-items: center;
diff --git a/src/pages/QuotaPage.module.scss b/src/pages/QuotaPage.module.scss
index 3bcc079c..7823d979 100644
--- a/src/pages/QuotaPage.module.scss
+++ b/src/pages/QuotaPage.module.scss
@@ -288,6 +288,37 @@
gap: $spacing-xs;
}
+.antigravityQuotaGroup {
+ display: flex;
+ flex-direction: column;
+ gap: $spacing-sm;
+ padding-bottom: $spacing-xs;
+}
+
+.antigravityQuotaGroup + .antigravityQuotaGroup {
+ padding-top: $spacing-sm;
+ border-top: 1px solid color-mix(in srgb, var(--border-color) 65%, transparent);
+}
+
+.antigravityQuotaGroupHeader {
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+ min-width: 0;
+}
+
+.antigravityQuotaGroupTitle {
+ font-size: 13px;
+ font-weight: 700;
+ color: var(--text-primary);
+}
+
+.antigravityQuotaGroupDescription {
+ font-size: 12px;
+ line-height: 1.35;
+ color: var(--text-tertiary);
+}
+
.quotaRowHeader {
display: flex;
align-items: center;
diff --git a/src/types/quota.ts b/src/types/quota.ts
index 982069cc..d4d8677b 100644
--- a/src/types/quota.ts
+++ b/src/types/quota.ts
@@ -47,31 +47,28 @@ export interface GeminiCliCodeAssistPayload {
paid_tier?: GeminiCliUserTier | null;
}
-export interface AntigravityQuotaInfo {
+export interface AntigravityQuotaSummaryBucketPayload {
+ bucketId?: string;
+ bucket_id?: string;
displayName?: string;
- quotaInfo?: {
- remainingFraction?: number | string;
- remaining_fraction?: number | string;
- remaining?: number | string;
- resetTime?: string;
- reset_time?: string;
- };
- quota_info?: {
- remainingFraction?: number | string;
- remaining_fraction?: number | string;
- remaining?: number | string;
- resetTime?: string;
- reset_time?: string;
- };
+ display_name?: string;
+ window?: string;
+ resetTime?: string;
+ reset_time?: string;
+ remainingFraction?: number | string;
+ remaining_fraction?: number | string;
+ description?: string;
}
-export type AntigravityModelsPayload = Record;
+export interface AntigravityQuotaSummaryGroupPayload {
+ displayName?: string;
+ display_name?: string;
+ description?: string;
+ buckets?: AntigravityQuotaSummaryBucketPayload[];
+}
-export interface AntigravityQuotaGroupDefinition {
- id: string;
- label: string;
- identifiers: string[];
- labelFromModel?: boolean;
+export interface AntigravityQuotaSummaryPayload {
+ groups?: AntigravityQuotaSummaryGroupPayload[];
}
export interface GeminiCliQuotaGroupDefinition {
@@ -204,14 +201,23 @@ export interface ClaudeQuotaState {
export interface AntigravityQuotaGroup {
id: string;
label: string;
- models: string[];
+ description?: string;
+ buckets: AntigravityQuotaBucket[];
+}
+
+export interface AntigravityQuotaBucket {
+ id: string;
+ label: string;
+ window?: string;
remainingFraction: number;
resetTime?: string;
+ description?: string;
}
export interface AntigravityQuotaState {
status: 'idle' | 'loading' | 'success' | 'error';
groups: AntigravityQuotaGroup[];
+ serverTimeOffsetMs?: number | null;
error?: string;
errorStatus?: number;
}
diff --git a/src/utils/quota/builders.ts b/src/utils/quota/builders.ts
index 51800b61..2dc9108f 100644
--- a/src/utils/quota/builders.ts
+++ b/src/utils/quota/builders.ts
@@ -3,10 +3,9 @@
*/
import type {
+ AntigravityQuotaBucket,
AntigravityQuotaGroup,
- AntigravityQuotaGroupDefinition,
- AntigravityQuotaInfo,
- AntigravityModelsPayload,
+ AntigravityQuotaSummaryPayload,
GeminiCliParsedBucket,
GeminiCliQuotaBucketState,
KimiUsagePayload,
@@ -15,12 +14,8 @@ import type {
KimiLimitWindow,
KimiQuotaRow,
} from '@/types';
-import {
- ANTIGRAVITY_QUOTA_GROUPS,
- GEMINI_CLI_GROUP_LOOKUP,
- GEMINI_CLI_GROUP_ORDER,
-} from './constants';
-import { normalizeQuotaFraction } from './parsers';
+import { GEMINI_CLI_GROUP_LOOKUP, GEMINI_CLI_GROUP_ORDER } from './constants';
+import { normalizeQuotaFraction, normalizeStringValue } from './parsers';
import { isIgnoredGeminiCliModel } from './validators';
export function pickEarlierResetTime(current?: string, next?: string): string | undefined {
@@ -137,120 +132,80 @@ export function buildGeminiCliQuotaBuckets(
});
}
-export function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): {
- remainingFraction: number | null;
- resetTime?: string;
- displayName?: string;
-} {
- if (!entry) {
- return { remainingFraction: null };
- }
- const quotaInfo = entry.quotaInfo ?? entry.quota_info ?? {};
- const remainingValue =
- quotaInfo.remainingFraction ?? quotaInfo.remaining_fraction ?? quotaInfo.remaining;
- const remainingFraction = normalizeQuotaFraction(remainingValue);
- const resetValue = quotaInfo.resetTime ?? quotaInfo.reset_time;
- const resetTime = typeof resetValue === 'string' ? resetValue : undefined;
- const displayName = typeof entry.displayName === 'string' ? entry.displayName : undefined;
+const ANTIGRAVITY_BUCKET_WINDOW_ORDER = new Map([
+ ['weekly', 0],
+ ['week', 0],
+ ['5h', 1],
+ ['five-hour', 1],
+ ['five_hour', 1],
+]);
- return {
- remainingFraction,
- resetTime,
- displayName,
- };
+function toStableId(value: string, fallback: string): string {
+ const normalized = value
+ .trim()
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '');
+ return normalized || fallback;
}
-export function findAntigravityModel(
- models: AntigravityModelsPayload,
- identifier: string
-): { id: string; entry: AntigravityQuotaInfo } | null {
- const direct = models[identifier];
- if (direct) {
- return { id: identifier, entry: direct };
- }
-
- const match = Object.entries(models).find(([, entry]) => {
- const name = typeof entry?.displayName === 'string' ? entry.displayName : '';
- return name.toLowerCase() === identifier.toLowerCase();
- });
- if (match) {
- return { id: match[0], entry: match[1] };
- }
-
- return null;
+function getAntigravityWindowOrder(bucket: AntigravityQuotaBucket): number {
+ const window = bucket.window?.toLowerCase();
+ if (!window) return Number.MAX_SAFE_INTEGER;
+ return ANTIGRAVITY_BUCKET_WINDOW_ORDER.get(window) ?? Number.MAX_SAFE_INTEGER;
}
export function buildAntigravityQuotaGroups(
- models: AntigravityModelsPayload
+ payload: AntigravityQuotaSummaryPayload
): AntigravityQuotaGroup[] {
- const groups: AntigravityQuotaGroup[] = [];
- const definitions = new Map(
- ANTIGRAVITY_QUOTA_GROUPS.map((definition) => [definition.id, definition] as const)
- );
+ const groups = Array.isArray(payload.groups) ? payload.groups : [];
- const buildGroup = (
- def: AntigravityQuotaGroupDefinition,
- overrideResetTime?: string
- ): AntigravityQuotaGroup | null => {
- const matches = def.identifiers
- .map((identifier) => findAntigravityModel(models, identifier))
- .filter((entry): entry is { id: string; entry: AntigravityQuotaInfo } => Boolean(entry));
+ return groups
+ .map((group, groupIndex): AntigravityQuotaGroup | null => {
+ const label =
+ normalizeStringValue(group.displayName ?? group.display_name) ??
+ `Quota Group ${groupIndex + 1}`;
+ const groupId = toStableId(label, `quota-group-${groupIndex + 1}`);
+ const buckets = Array.isArray(group.buckets) ? group.buckets : [];
+ const parsedBuckets = buckets
+ .map((bucket, bucketIndex): AntigravityQuotaBucket | null => {
+ const remainingFraction = normalizeQuotaFraction(
+ bucket.remainingFraction ?? bucket.remaining_fraction
+ );
+ if (remainingFraction === null) return null;
- const quotaEntries = matches
- .map(({ id, entry }) => {
- const info = getAntigravityQuotaInfo(entry);
- const remainingFraction = info.remainingFraction ?? (info.resetTime ? 0 : null);
- if (remainingFraction === null) return null;
- return {
- id,
- remainingFraction,
- resetTime: info.resetTime,
- displayName: info.displayName,
- };
- })
- .filter((entry): entry is NonNullable => entry !== null);
+ const window = normalizeStringValue(bucket.window) ?? undefined;
+ const rawId =
+ normalizeStringValue(bucket.bucketId ?? bucket.bucket_id) ??
+ `${groupId}-${window ?? `bucket-${bucketIndex + 1}`}`;
+ const label = normalizeStringValue(bucket.displayName ?? bucket.display_name) ?? rawId;
- if (quotaEntries.length === 0) return null;
+ return {
+ id: rawId,
+ label,
+ window,
+ remainingFraction,
+ resetTime: normalizeStringValue(bucket.resetTime ?? bucket.reset_time) ?? undefined,
+ description: normalizeStringValue(bucket.description) ?? undefined,
+ };
+ })
+ .filter((bucket): bucket is AntigravityQuotaBucket => bucket !== null)
+ .sort((a, b) => {
+ const orderDiff = getAntigravityWindowOrder(a) - getAntigravityWindowOrder(b);
+ if (orderDiff !== 0) return orderDiff;
+ return a.label.localeCompare(b.label);
+ });
- const remainingFraction = Math.min(...quotaEntries.map((entry) => entry.remainingFraction));
- const resetTime =
- overrideResetTime ?? quotaEntries.map((entry) => entry.resetTime).find(Boolean);
- const displayName = quotaEntries.map((entry) => entry.displayName).find(Boolean);
- const label = def.labelFromModel && displayName ? displayName : def.label;
+ if (parsedBuckets.length === 0) return null;
- return {
- id: def.id,
- label,
- models: quotaEntries.map((entry) => entry.id),
- remainingFraction,
- resetTime,
- };
- };
-
- const appendGroup = (
- id: string,
- overrideResetTime?: string
- ): AntigravityQuotaGroup | null => {
- const definition = definitions.get(id);
- if (!definition) return null;
- const group = buildGroup(definition, overrideResetTime);
- if (group) {
- groups.push(group);
- }
- return group;
- };
-
- appendGroup('claude-gpt');
- const gemini31ProGroup = appendGroup('gemini-3-1-pro-series');
- const geminiProGroup = appendGroup('gemini-3-pro');
- const geminiProResetTime = gemini31ProGroup?.resetTime ?? geminiProGroup?.resetTime;
- appendGroup('gemini-2-5-flash');
- appendGroup('gemini-2-5-flash-lite');
- appendGroup('gemini-2-5-cu');
- appendGroup('gemini-3-flash');
- appendGroup('gemini-image', geminiProResetTime);
-
- return groups;
+ return {
+ id: groupId,
+ label,
+ description: normalizeStringValue(group.description) ?? undefined,
+ buckets: parsedBuckets,
+ };
+ })
+ .filter((group): group is AntigravityQuotaGroup => group !== null);
}
function toInt(value: unknown): number | null {
diff --git a/src/utils/quota/constants.ts b/src/utils/quota/constants.ts
index 43643d4b..3663c19e 100644
--- a/src/utils/quota/constants.ts
+++ b/src/utils/quota/constants.ts
@@ -2,11 +2,7 @@
* Quota constants for API URLs, headers, and theme colors.
*/
-import type {
- AntigravityQuotaGroupDefinition,
- GeminiCliQuotaGroupDefinition,
- TypeColorSet,
-} from '@/types';
+import type { GeminiCliQuotaGroupDefinition, TypeColorSet } from '@/types';
// Theme colors for type badges — 与 authFiles/constants.ts 保持同步
export const TYPE_COLORS: Record = {
@@ -66,61 +62,17 @@ export const TYPE_COLORS: Record = {
// Antigravity API configuration
export const ANTIGRAVITY_QUOTA_URLS = [
- 'https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
- 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:fetchAvailableModels',
- 'https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels',
+ 'https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
+ 'https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:retrieveUserQuotaSummary',
+ 'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary',
];
export const ANTIGRAVITY_REQUEST_HEADERS = {
Authorization: 'Bearer $TOKEN$',
'Content-Type': 'application/json',
- 'User-Agent': 'antigravity/1.11.5 windows/amd64',
+ 'User-Agent': 'antigravity/cli/1.0.8 darwin/arm64',
};
-export const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [
- {
- id: 'claude-gpt',
- label: 'Claude/GPT',
- identifiers: ['claude-sonnet-4-6', 'claude-opus-4-6-thinking', 'gpt-oss-120b-medium'],
- },
- {
- id: 'gemini-3-pro',
- label: 'Gemini 3 Pro',
- identifiers: ['gemini-3-pro-high', 'gemini-3-pro-low'],
- },
- {
- id: 'gemini-3-1-pro-series',
- label: 'Gemini 3.1 Pro Series',
- identifiers: ['gemini-3.1-pro-high', 'gemini-3.1-pro-low'],
- },
- {
- id: 'gemini-2-5-flash',
- label: 'Gemini 2.5 Flash',
- identifiers: ['gemini-2.5-flash', 'gemini-2.5-flash-thinking'],
- },
- {
- id: 'gemini-2-5-flash-lite',
- label: 'Gemini 2.5 Flash Lite',
- identifiers: ['gemini-2.5-flash-lite'],
- },
- {
- id: 'gemini-2-5-cu',
- label: 'Gemini 2.5 CU',
- identifiers: ['rev19-uic3-1p'],
- },
- {
- id: 'gemini-3-flash',
- label: 'Gemini 3 Flash',
- identifiers: ['gemini-3-flash'],
- },
- {
- id: 'gemini-image',
- label: 'gemini-3.1-flash-image',
- identifiers: ['gemini-3.1-flash-image'],
- labelFromModel: true,
- },
-];
-
// Gemini CLI API configuration
export const GEMINI_CLI_QUOTA_URL =
'https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota';