优化 Antigravity 配额展示顺序

This commit is contained in:
MMEXA
2026-07-08 22:34:29 +08:00
parent accd250226
commit 9ec48ab6b9
6 changed files with 266 additions and 78 deletions
@@ -579,9 +579,25 @@ fn model_quota_window_snapshot(
fn canonical_antigravity_model_label(model_name: &str) -> Option<&'static str> { fn canonical_antigravity_model_label(model_name: &str) -> Option<&'static str> {
match model_name.trim() { match model_name.trim() {
"claude-opus-4-6-thinking" => Some("Claude Opus 4.6 Thinking"),
"claude-sonnet-4-6" => Some("Claude Sonnet 4.6"),
"gemini-3.5-flash-extra-low" => Some("Gemini 3.5 Flash Extra Low"), "gemini-3.5-flash-extra-low" => Some("Gemini 3.5 Flash Extra Low"),
"gemini-3.5-flash-low" => Some("Gemini 3.5 Flash Low"), "gemini-3.5-flash-low" => Some("Gemini 3.5 Flash Low"),
"gemini-3.1-pro-high" => Some("Gemini 3.1 Pro High"),
"gemini-3.1-pro-low" => Some("Gemini 3.1 Pro Low"),
"gemini-pro-agent" => Some("Gemini Pro Agent"),
"gemini-3.1-flash-image" => Some("Gemini 3.1 Flash Image"),
"gemini-3.1-flash-lite" => Some("Gemini 3.1 Flash Lite"),
"gemini-3-flash-agent" => Some("Gemini 3 Flash Agent"), "gemini-3-flash-agent" => Some("Gemini 3 Flash Agent"),
"gemini-3-flash" => Some("Gemini 3 Flash"),
"gemini-2.5-pro" => Some("Gemini 2.5 Pro"),
"gemini-2.5-flash-thinking" => Some("Gemini 2.5 Flash Thinking"),
"gemini-2.5-flash" => Some("Gemini 2.5 Flash"),
"gemini-2.5-flash-lite" => Some("Gemini 2.5 Flash Lite"),
"gpt-oss-120b-medium" => Some("GPT-OSS 120B Medium"),
"tab_flash_lite_preview" => Some("Tab Flash Lite Preview"),
"tab_jump_flash_lite_preview" => Some("Tab Jump Flash Lite Preview"),
"models/proactive-observer" => Some("Proactive Observer"),
_ => None, _ => None,
} }
} }
@@ -3739,14 +3755,22 @@ mod tests {
} }
#[test] #[test]
fn sync_provider_key_quota_status_snapshot_labels_antigravity_flash_tiers_by_model_id() { fn sync_provider_key_quota_status_snapshot_labels_antigravity_models_by_model_id() {
let upstream_metadata = json!({ let upstream_metadata = json!({
"antigravity": { "antigravity": {
"updated_at": 1_775_553_285u64, "updated_at": 1_775_553_285u64,
"quota_by_model": { "quota_by_model": {
"gemini-3.5-flash-extra-low": { "remaining_fraction": 1.0 }, "gemini-3.5-flash-extra-low": { "remaining_fraction": 1.0 },
"gemini-3.5-flash-low": { "remaining_fraction": 0.75 }, "gemini-3.5-flash-low": { "remaining_fraction": 0.75 },
"gemini-3-flash-agent": { "remaining_fraction": 0.5 } "gemini-3-flash-agent": { "remaining_fraction": 0.5 },
"gemini-2.5-flash": {
"display_name": "Gemini 3.1 Flash Lite",
"remaining_fraction": 0.4
},
"claude-sonnet-4-6": {
"display_name": "Claude Sonnet 4.6 (Thinking)",
"remaining_fraction": 0.3
}
} }
} }
}); });
@@ -3782,6 +3806,14 @@ mod tests {
label_for_model("gemini-3-flash-agent"), label_for_model("gemini-3-flash-agent"),
Some(json!("Gemini 3 Flash Agent")) Some(json!("Gemini 3 Flash Agent"))
); );
assert_eq!(
label_for_model("gemini-2.5-flash"),
Some(json!("Gemini 2.5 Flash"))
);
assert_eq!(
label_for_model("claude-sonnet-4-6"),
Some(json!("Claude Sonnet 4.6"))
);
} }
#[test] #[test]
@@ -60,13 +60,6 @@
> >
{{ item.label }} {{ item.label }}
</div> </div>
<div
v-if="item.label !== item.model"
class="text-[9px] leading-tight text-muted-foreground/55 truncate font-mono"
:title="item.model"
>
{{ item.model }}
</div>
</div> </div>
<span :class="getQuotaRemainingClass(item.usedPercent)"> <span :class="getQuotaRemainingClass(item.usedPercent)">
{{ item.remainingPercent.toFixed(1) }}% {{ item.remainingPercent.toFixed(1) }}%
@@ -125,6 +118,10 @@ import { testModel } from '@/api/endpoints/providers'
import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types' import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { parseApiError } from '@/utils/errorParser' import { parseApiError } from '@/utils/errorParser'
import {
compareAntigravityQuotaItems,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@@ -194,32 +191,6 @@ function secondsUntilUnixReset(resetAt: unknown): number | null {
return Math.max(Math.floor(numericResetAt - now), 0) return Math.max(Math.floor(numericResetAt - now), 0)
} }
function sortQuotaItems(a: QuotaItem, b: QuotaItem): number {
return (a.remainingPercent - b.remainingPercent)
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}
function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
function resolveQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(model) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || model
}
function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] { function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] {
if (!quota) return [] if (!quota) return []
@@ -252,7 +223,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
return { return {
model, model,
label: resolveQuotaLabel(model, window.label || window.model, opaqueDisplayIndex), label: resolveAntigravityQuotaLabel(model, window.label || window.model, opaqueDisplayIndex),
usedPercent, usedPercent,
remainingPercent, remainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window), resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
@@ -260,7 +231,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
}) })
.filter((item): item is QuotaItem => item !== null) .filter((item): item is QuotaItem => item !== null)
items.sort(sortQuotaItems) items.sort(compareAntigravityQuotaItems)
return items return items
} }
@@ -305,14 +276,14 @@ const items = computed<QuotaItem[]>(() => {
result.push({ result.push({
model, model,
label: resolveQuotaLabel(model, info['display_name'], opaqueDisplayIndex), label: resolveAntigravityQuotaLabel(model, info['display_name'], opaqueDisplayIndex),
usedPercent, usedPercent,
remainingPercent, remainingPercent,
resetSeconds, resetSeconds,
}) })
} }
result.sort(sortQuotaItems) result.sort(compareAntigravityQuotaItems)
return result return result
}) })
@@ -372,13 +372,6 @@
> >
<template #footer> <template #footer>
<div class="mt-0.5 space-y-0.5"> <div class="mt-0.5 space-y-0.5">
<div
v-if="item.label !== item.model"
class="text-[9px] text-muted-foreground/55 truncate font-mono"
:title="item.model"
>
{{ item.model }}
</div>
<div <div
v-if="item.resetSeconds !== null || item.usedPercent > 0" v-if="item.resetSeconds !== null || item.usedPercent > 0"
class="text-[9px] text-muted-foreground/70" class="text-[9px] text-muted-foreground/70"
@@ -967,6 +960,10 @@ import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMo
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue' import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue' import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes' import { useProxyNodesStore } from '@/stores/proxy-nodes'
import {
compareAntigravityQuotaItems,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
import { import {
deleteEndpointKey, deleteEndpointKey,
recoverKeyHealth, recoverKeyHealth,
@@ -3385,32 +3382,6 @@ function coerceAntigravityRemainingFraction(value: number | string | null | unde
return Math.min(Math.max(numericValue, 0), 1) return Math.min(Math.max(numericValue, 0), 1)
} }
function antigravityQuotaItemSort(a: AntigravityQuotaItem, b: AntigravityQuotaItem): number {
return (a.remainingPercent - b.remainingPercent)
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}
function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
function resolveAntigravityQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(model) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || model
}
function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] { function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] {
const quotaByModel = metadata?.antigravity?.quota_by_model const quotaByModel = metadata?.antigravity?.quota_by_model
if (!quotaByModel || typeof quotaByModel !== 'object') return [] if (!quotaByModel || typeof quotaByModel !== 'object') return []
@@ -3449,7 +3420,7 @@ function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined)
}) })
} }
items.sort(antigravityQuotaItemSort) items.sort(compareAntigravityQuotaItems)
return items return items
} }
@@ -3493,7 +3464,7 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
}) })
.filter((item): item is AntigravityQuotaItem => item !== null) .filter((item): item is AntigravityQuotaItem => item !== null)
items.sort(antigravityQuotaItemSort) items.sort(compareAntigravityQuotaItems)
return items return items
} }
@@ -102,12 +102,13 @@ function mount(metadata: UpstreamMetadata) {
} }
describe('AntigravityQuotaDialog', () => { describe('AntigravityQuotaDialog', () => {
it('renders model display names without losing raw model identifiers', () => { it('renders opaque quota identifiers with concise visible labels', () => {
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
const { root, unmount } = mount({ const { root, unmount } = mount({
antigravity: { antigravity: {
quota_by_model: { quota_by_model: {
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb': { [rawIdentifier]: {
display_name: 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb', display_name: rawIdentifier,
remaining_fraction: 0.25, remaining_fraction: 0.25,
used_percent: 75, used_percent: 75,
}, },
@@ -116,9 +117,45 @@ describe('AntigravityQuotaDialog', () => {
}) })
expect(root.textContent).toContain('Key-1') expect(root.textContent).toContain('Key-1')
expect(root.textContent).toContain('RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb') expect(root.textContent).not.toContain(rawIdentifier)
expect(root.textContent).toContain('25.0%') expect(root.textContent).toContain('25.0%')
unmount() unmount()
}) })
it('orders important Gemini and Claude quota rows before low-priority rows', () => {
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
tab_flash_lite_preview: {
display_name: 'Tab Flash Lite Preview',
remaining_fraction: 0.01,
used_percent: 99,
},
'gemini-3.5-flash-low': {
display_name: 'Gemini 3.5 Flash Low',
remaining_fraction: 0.8,
used_percent: 20,
},
'claude-opus-4-6-thinking': {
display_name: 'Claude Opus 4.6 Thinking',
remaining_fraction: 1,
used_percent: 0,
},
chat_20706: {
display_name: 'chat_20706',
remaining_fraction: 0,
used_percent: 100,
},
},
},
})
const text = root.textContent || ''
expect(text.indexOf('Claude Opus 4.6 Thinking')).toBeLessThan(text.indexOf('Gemini 3.5 Flash Low'))
expect(text.indexOf('Gemini 3.5 Flash Low')).toBeLessThan(text.indexOf('Tab Flash Lite Preview'))
expect(text.indexOf('Tab Flash Lite Preview')).toBeLessThan(text.indexOf('chat_20706'))
unmount()
})
}) })
@@ -0,0 +1,77 @@
import { describe, expect, it } from 'vitest'
import {
compareAntigravityQuotaItems,
resolveAntigravityQuotaLabel,
} from '@/features/providers/utils/antigravityQuota'
describe('antigravityQuota', () => {
it('uses canonical labels from model ids before stale upstream labels', () => {
const opaqueDisplayIndex = { value: 1 }
expect(
resolveAntigravityQuotaLabel(
'gemini-2.5-flash',
'Gemini 3.1 Flash Lite',
opaqueDisplayIndex,
),
).toBe('Gemini 2.5 Flash')
expect(
resolveAntigravityQuotaLabel(
'claude-sonnet-4-6',
'Claude Sonnet 4.6 (Thinking)',
opaqueDisplayIndex,
),
).toBe('Claude Sonnet 4.6')
expect(
resolveAntigravityQuotaLabel(
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb',
'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb',
opaqueDisplayIndex,
),
).toBe('Key-1')
})
it('keeps latest Gemini and Claude quota buckets before low-priority buckets', () => {
const items = [
{
model: 'tab_flash_lite_preview',
label: 'Tab Flash Lite Preview',
remainingPercent: 1,
resetSeconds: 10,
},
{
model: 'gemini-3.5-flash-low',
label: 'Gemini 3.5 Flash Low',
remainingPercent: 90,
resetSeconds: null,
},
{
model: 'chat_20706',
label: 'chat_20706',
remainingPercent: 0,
resetSeconds: 1,
},
{
model: 'claude-opus-4-6-thinking',
label: 'Claude Opus 4.6 Thinking',
remainingPercent: 100,
resetSeconds: null,
},
{
model: 'gemini-2.5-flash-lite',
label: 'Gemini 2.5 Flash Lite',
remainingPercent: 5,
resetSeconds: 5,
},
].sort(compareAntigravityQuotaItems)
expect(items.map(item => item.model)).toEqual([
'claude-opus-4-6-thinking',
'gemini-3.5-flash-low',
'gemini-2.5-flash-lite',
'tab_flash_lite_preview',
'chat_20706',
])
})
})
@@ -0,0 +1,100 @@
export interface AntigravityQuotaSortableItem {
model: string
label: string
remainingPercent: number
resetSeconds: number | null
}
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
'claude-opus-4-6-thinking': 'Claude Opus 4.6 Thinking',
'claude-sonnet-4-6': 'Claude Sonnet 4.6',
'gemini-3.5-flash-low': 'Gemini 3.5 Flash Low',
'gemini-3.5-flash-extra-low': 'Gemini 3.5 Flash Extra Low',
'gemini-3.1-pro-high': 'Gemini 3.1 Pro High',
'gemini-3.1-pro-low': 'Gemini 3.1 Pro Low',
'gemini-pro-agent': 'Gemini Pro Agent',
'gemini-3.1-flash-image': 'Gemini 3.1 Flash Image',
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
'gemini-3-flash-agent': 'Gemini 3 Flash Agent',
'gemini-3-flash': 'Gemini 3 Flash',
'gemini-2.5-pro': 'Gemini 2.5 Pro',
'gemini-2.5-flash-thinking': 'Gemini 2.5 Flash Thinking',
'gemini-2.5-flash': 'Gemini 2.5 Flash',
'gemini-2.5-flash-lite': 'Gemini 2.5 Flash Lite',
'gpt-oss-120b-medium': 'GPT-OSS 120B Medium',
'tab_flash_lite_preview': 'Tab Flash Lite Preview',
'tab_jump_flash_lite_preview': 'Tab Jump Flash Lite Preview',
'models/proactive-observer': 'Proactive Observer',
}
const ANTIGRAVITY_MODEL_PRIORITY: Record<string, number> = {
'claude-opus-4-6-thinking': 10,
'claude-sonnet-4-6': 20,
'gemini-3.5-flash-low': 100,
'gemini-3.5-flash-extra-low': 110,
'gemini-3.1-pro-high': 120,
'gemini-3.1-pro-low': 130,
'gemini-pro-agent': 140,
'gemini-3.1-flash-image': 150,
'gemini-3.1-flash-lite': 160,
'gemini-3-flash-agent': 170,
'gemini-3-flash': 180,
'gemini-2.5-pro': 300,
'gemini-2.5-flash-thinking': 310,
'gemini-2.5-flash': 320,
'gemini-2.5-flash-lite': 330,
'gpt-oss-120b-medium': 700,
'models/proactive-observer': 780,
'tab_flash_lite_preview': 800,
'tab_jump_flash_lite_preview': 810,
}
export function isOpaqueAntigravityQuotaIdentifier(value: string): boolean {
return value.trim().startsWith('RateLimitResetCredit_')
}
export function resolveAntigravityQuotaLabel(
model: string,
rawLabel: unknown,
opaqueDisplayIndex: { value: number },
): string {
const normalizedModel = model.trim()
const canonical = ANTIGRAVITY_MODEL_LABELS[normalizedModel]
if (canonical) return canonical
const candidate = String(rawLabel || '').trim()
if (candidate && !isOpaqueAntigravityQuotaIdentifier(candidate)) return candidate
if (isOpaqueAntigravityQuotaIdentifier(normalizedModel) || (candidate && isOpaqueAntigravityQuotaIdentifier(candidate))) {
const label = `Key-${opaqueDisplayIndex.value}`
opaqueDisplayIndex.value += 1
return label
}
return candidate || normalizedModel
}
function getAntigravityModelPriority(model: string): number {
const normalizedModel = model.trim()
const explicit = ANTIGRAVITY_MODEL_PRIORITY[normalizedModel]
if (explicit !== undefined) return explicit
if (normalizedModel.startsWith('claude-')) return 90
if (normalizedModel.startsWith('gemini-3.')) return 200
if (normalizedModel.startsWith('gemini-2.')) return 390
if (normalizedModel.startsWith('gemini-')) return 490
if (normalizedModel.startsWith('gpt-oss-')) return 700
if (normalizedModel.startsWith('models/')) return 780
if (normalizedModel.startsWith('tab_')) return 800
if (normalizedModel.startsWith('chat_')) return 900
if (isOpaqueAntigravityQuotaIdentifier(normalizedModel)) return 950
return 850
}
export function compareAntigravityQuotaItems<T extends AntigravityQuotaSortableItem>(
a: T,
b: T,
): number {
return (getAntigravityModelPriority(a.model) - getAntigravityModelPriority(b.model))
|| ((a.resetSeconds ?? Number.POSITIVE_INFINITY) - (b.resetSeconds ?? Number.POSITIVE_INFINITY))
|| (a.remainingPercent - b.remainingPercent)
|| a.label.localeCompare(b.label)
|| a.model.localeCompare(b.model)
}