From eadfd7a957f06b75fe1a47f9c79e8c9ee6067379 Mon Sep 17 00:00:00 2001 From: Supra4E8C Date: Tue, 30 Dec 2025 21:48:12 +0800 Subject: [PATCH] feat(quota): group Gemini CLI buckets and refine Gemini quota groups --- src/pages/QuotaPage.tsx | 204 +++++++++++++++++++++++++++++++++++----- src/types/quota.ts | 1 + 2 files changed, 181 insertions(+), 24 deletions(-) diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx index 0db90ef..116d07f 100644 --- a/src/pages/QuotaPage.tsx +++ b/src/pages/QuotaPage.tsx @@ -132,15 +132,24 @@ const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroupDefinition[] = [ ] }, { - id: 'gemini', - label: 'Gemini', - identifiers: [ - 'gemini-3-pro-high', - 'gemini-3-pro-low', - 'gemini-2.5-flash', - 'gemini-2.5-flash-lite', - 'rev19-uic3-1p' - ] + id: 'gemini-3-pro', + label: 'Gemini 3 Pro', + identifiers: ['gemini-3-pro-high', 'gemini-3-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', @@ -162,6 +171,51 @@ const GEMINI_CLI_REQUEST_HEADERS = { 'Content-Type': 'application/json' }; +interface GeminiCliQuotaGroupDefinition { + id: string; + label: string; + modelIds: string[]; +} + +interface GeminiCliParsedBucket { + modelId: string; + tokenType: string | null; + remainingFraction: number | null; + remainingAmount: number | null; + resetTime: string | undefined; +} + +const GEMINI_CLI_QUOTA_GROUPS: GeminiCliQuotaGroupDefinition[] = [ + { + id: 'gemini-2-5-flash-series', + label: 'Gemini 2.5 Flash Series', + modelIds: ['gemini-2.5-flash', 'gemini-2.5-flash-lite'] + }, + { + id: 'gemini-2-5-pro', + label: 'Gemini 2.5 Pro', + modelIds: ['gemini-2.5-pro'] + }, + { + id: 'gemini-3-pro-preview', + label: 'Gemini 3 Pro Preview', + modelIds: ['gemini-3-pro-preview'] + }, + { + id: 'gemini-3-flash-preview', + label: 'Gemini 3 Flash Preview', + modelIds: ['gemini-3-flash-preview'] + } +]; + +const GEMINI_CLI_GROUP_LOOKUP = new Map( + GEMINI_CLI_QUOTA_GROUPS.flatMap((group) => + group.modelIds.map((modelId) => [modelId, group] as const) + ) +); + +const GEMINI_CLI_IGNORED_MODEL_PREFIXES = ['gemini-2.0-flash']; + interface CodexUsageWindow { used_percent?: number | string; usedPercent?: number | string; @@ -472,6 +526,80 @@ function parseGeminiCliQuotaPayload(payload: unknown): GeminiCliQuotaPayload | n return null; } +function isIgnoredGeminiCliModel(modelId: string): boolean { + return GEMINI_CLI_IGNORED_MODEL_PREFIXES.some( + (prefix) => modelId === prefix || modelId.startsWith(`${prefix}-`) + ); +} + +function pickEarlierResetTime(current?: string, next?: string): string | undefined { + if (!current) return next; + if (!next) return current; + const currentTime = new Date(current).getTime(); + const nextTime = new Date(next).getTime(); + if (Number.isNaN(currentTime)) return next; + if (Number.isNaN(nextTime)) return current; + return currentTime <= nextTime ? current : next; +} + +function minNullableNumber(current: number | null, next: number | null): number | null { + if (current === null) return next; + if (next === null) return current; + return Math.min(current, next); +} + +function buildGeminiCliQuotaBuckets( + buckets: GeminiCliParsedBucket[] +): GeminiCliQuotaBucketState[] { + if (buckets.length === 0) return []; + + const grouped = new Map(); + + buckets.forEach((bucket) => { + if (isIgnoredGeminiCliModel(bucket.modelId)) return; + const group = GEMINI_CLI_GROUP_LOOKUP.get(bucket.modelId); + const groupId = group?.id ?? bucket.modelId; + const label = group?.label ?? bucket.modelId; + const tokenKey = bucket.tokenType ?? ''; + const mapKey = `${groupId}::${tokenKey}`; + const existing = grouped.get(mapKey); + + if (!existing) { + grouped.set(mapKey, { + id: `${groupId}${tokenKey ? `-${tokenKey}` : ''}`, + label, + remainingFraction: bucket.remainingFraction, + remainingAmount: bucket.remainingAmount, + resetTime: bucket.resetTime, + tokenType: bucket.tokenType, + modelIds: [bucket.modelId] + }); + return; + } + + existing.remainingFraction = minNullableNumber( + existing.remainingFraction, + bucket.remainingFraction + ); + existing.remainingAmount = minNullableNumber(existing.remainingAmount, bucket.remainingAmount); + existing.resetTime = pickEarlierResetTime(existing.resetTime, bucket.resetTime); + existing.modelIds.push(bucket.modelId); + }); + + return Array.from(grouped.values()).map((bucket) => { + const uniqueModelIds = Array.from(new Set(bucket.modelIds)); + return { + id: bucket.id, + label: bucket.label, + remainingFraction: bucket.remainingFraction, + remainingAmount: bucket.remainingAmount, + resetTime: bucket.resetTime, + tokenType: bucket.tokenType, + modelIds: uniqueModelIds + }; + }); +} + function getAntigravityQuotaInfo(entry?: AntigravityQuotaInfo): { remainingFraction: number | null; resetTime?: string; @@ -517,8 +645,16 @@ function findAntigravityModel( function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): AntigravityQuotaGroup[] { const groups: AntigravityQuotaGroup[] = []; - let geminiResetTime: string | undefined; - const [claudeDef, geminiDef, flashDef, imageDef] = ANTIGRAVITY_QUOTA_GROUPS; + let geminiProResetTime: string | undefined; + const [ + claudeDef, + geminiProDef, + flashDef, + flashLiteDef, + cuDef, + geminiFlashDef, + imageDef + ] = ANTIGRAVITY_QUOTA_GROUPS; const buildGroup = ( def: AntigravityQuotaGroupDefinition, @@ -565,10 +701,10 @@ function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): Antigrav groups.push(claudeGroup); } - const geminiGroup = buildGroup(geminiDef); - if (geminiGroup) { - geminiResetTime = geminiGroup.resetTime; - groups.push(geminiGroup); + const geminiProGroup = buildGroup(geminiProDef); + if (geminiProGroup) { + geminiProResetTime = geminiProGroup.resetTime; + groups.push(geminiProGroup); } const flashGroup = buildGroup(flashDef); @@ -576,7 +712,22 @@ function buildAntigravityQuotaGroups(models: AntigravityModelsPayload): Antigrav groups.push(flashGroup); } - const imageGroup = buildGroup(imageDef, geminiResetTime); + const flashLiteGroup = buildGroup(flashLiteDef); + if (flashLiteGroup) { + groups.push(flashLiteGroup); + } + + const cuGroup = buildGroup(cuDef); + if (cuGroup) { + groups.push(cuGroup); + } + + const geminiFlashGroup = buildGroup(geminiFlashDef); + if (geminiFlashGroup) { + groups.push(geminiFlashGroup); + } + + const imageGroup = buildGroup(imageDef, geminiProResetTime); if (imageGroup) { groups.push(imageGroup); } @@ -1066,8 +1217,8 @@ export function QuotaPage() { const buckets = Array.isArray(payload?.buckets) ? payload?.buckets : []; if (buckets.length === 0) return []; - return buckets - .map((bucket, index) => { + const parsedBuckets = buckets + .map((bucket) => { const modelId = normalizeStringValue(bucket.modelId ?? bucket.model_id); if (!modelId) return null; const tokenType = normalizeStringValue(bucket.tokenType ?? bucket.token_type); @@ -1086,15 +1237,16 @@ export function QuotaPage() { } const remainingFraction = remainingFractionRaw ?? fallbackFraction; return { - id: `${modelId}-${tokenType ?? index}`, - label: modelId, + modelId, + tokenType, remainingFraction, remainingAmount, - resetTime, - tokenType + resetTime }; }) - .filter((bucket): bucket is GeminiCliQuotaBucketState => bucket !== null); + .filter((bucket): bucket is GeminiCliParsedBucket => bucket !== null); + + return buildGeminiCliQuotaBuckets(parsedBuckets); }, [t] ); @@ -1479,6 +1631,10 @@ export function QuotaPage() { : t('gemini_cli_quota.remaining_amount', { count: bucket.remainingAmount }); + const titleBase = + bucket.modelIds && bucket.modelIds.length > 0 + ? bucket.modelIds.join(', ') + : bucket.label; const quotaBarClass = percent === null ? styles.quotaBarFillMedium @@ -1494,7 +1650,7 @@ export function QuotaPage() { {bucket.label} diff --git a/src/types/quota.ts b/src/types/quota.ts index 00d1f15..14a8deb 100644 --- a/src/types/quota.ts +++ b/src/types/quota.ts @@ -24,6 +24,7 @@ export interface GeminiCliQuotaBucketState { remainingAmount: number | null; resetTime: string | undefined; tokenType: string | null; + modelIds?: string[]; } export interface GeminiCliQuotaState {