fix(frontend): deduplicate Antigravity quota groups

This commit is contained in:
ZheFox
2026-09-03 19:22:42 +08:00
parent 3dfc15963c
commit 09ef3adf70
10 changed files with 224 additions and 288 deletions
@@ -31,6 +31,11 @@ export interface QuotaWindowSnapshot {
scope?: 'account' | 'workspace' | 'model' | string
unit?: 'percent' | 'count' | 'usd' | 'tokens' | string
model?: string | null
quota_group?: string | null
quota_group_label?: string | null
bucket_id?: string | null
window?: string | null
description?: string | null
used_ratio?: number | null
remaining_ratio?: number | null
used_value?: number | null
@@ -96,7 +96,7 @@ describe('pool key display panels', () => {
const app = createApp(PoolKeyQuotaPanel, {
items: [
{
label: 'Gemini额度',
label: 'Gemini Models · 周',
remainingPercent: 90.6,
resetText: '1h 后重置',
meterText: '90.6%100%',
@@ -104,7 +104,7 @@ describe('pool key display panels', () => {
meterClass: 'text-emerald-600',
},
{
label: 'Claude & ChatGPT',
label: 'Claude and GPT models · 周',
remainingPercent: 100,
resetText: '1h 后重置',
meterText: '100%',
@@ -118,8 +118,8 @@ describe('pool key display panels', () => {
expect(root.querySelector('[data-testid="pool-quota-rows"]')?.className).toContain('space-y-2')
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-period-label"]')).map(node => node.textContent)).toEqual([
'Gemini额度',
'Claude & ChatGPT',
'Gemini Models · 周',
'Claude and GPT models · 周',
])
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-meter-text"]')).map(node => node.textContent)).toEqual(['90.6%100%', '100%'])
expect(root.querySelectorAll('[data-testid="pool-quota-progress-track"]')).toHaveLength(2)
@@ -117,12 +117,13 @@ import Button from '@/components/ui/button.vue'
import { testModel } from '@/api/endpoints/providers'
import type { UpstreamMetadata, QuotaStatusSnapshot, QuotaWindowSnapshot } from '@/api/endpoints/types'
import { useToast } from '@/composables/useToast'
import { useI18n } from '@/i18n'
import { parseApiError } from '@/utils/errorParser'
import {
compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaGroupLabel,
resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota'
const props = defineProps<{
@@ -148,6 +149,7 @@ interface QuotaItem {
}
const { error: showError, success: showSuccess } = useToast()
const { t } = useI18n()
const testingModel = ref<string | null>(null)
function getQuotaSnapshotUpdatedAt(quota: QuotaStatusSnapshot | null | undefined): number | undefined {
@@ -253,7 +255,7 @@ function buildGroupedItemsFromQuotaSnapshot(
return windows
.map((window) => {
const code = String(window.code || '').trim()
const label = String(window.label || code).trim()
const label = resolveAntigravityQuotaGroupLabel(window, t)
if (!code || !label) return null
const usedPercent =
@@ -332,9 +334,8 @@ const rawItems = computed<QuotaItem[]>(() => {
return dedupeAntigravityQuotaItemsByLabel(result)
})
const items = computed<QuotaItem[]>(() => summarizeAntigravityQuotaItems(rawItems.value))
const groupedItems = computed<QuotaItem[]>(() => buildGroupedItemsFromQuotaSnapshot(props.quotaSnapshot))
const displayItems = computed<QuotaItem[]>(() => [...groupedItems.value, ...items.value])
const displayItems = computed<QuotaItem[]>(() => groupedItems.value)
async function handleTestModel(modelName: string) {
if (!props.providerId || testingModel.value) return
@@ -377,7 +377,7 @@
/>
<div class="grid grid-cols-2 gap-3">
<ProviderQuotaProgressRow
v-for="item in getAntigravityQuotaSummaryForKey(key)"
v-for="item in getAntigravityQuotaGroupItems(key)"
:key="item.model"
:label="item.label"
:title="item.label"
@@ -979,12 +979,7 @@ import ProviderMonthlyQuotaCard from '@/features/providers/components/ProviderMo
import ProviderQuotaProgressRow from '@/features/providers/components/ProviderQuotaProgressRow.vue'
import ProviderQuotaSectionHeader from '@/features/providers/components/ProviderQuotaSectionHeader.vue'
import { useProxyNodesStore } from '@/stores/proxy-nodes'
import {
compareAntigravityQuotaItems,
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota'
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
import {
deleteEndpointKey,
recoverKeyHealth,
@@ -1004,7 +999,6 @@ import {
} from '@/api/endpoints'
import type {
UpstreamMetadata,
AntigravityModelQuota,
CodexUpstreamMetadata,
ChatGPTWebUpstreamMetadata,
GrokUpstreamMetadata,
@@ -1078,7 +1072,7 @@ const { error: showError, success: showSuccess, warning: showWarning } = useToas
const { confirm } = useConfirm()
const { copyToClipboard } = useClipboard()
const { tick: countdownTick, start: startCountdownTimer, stop: stopCountdownTimer } = useCountdownTimer()
const { legacyT, locale } = useI18n()
const { legacyT, locale, t } = useI18n()
function localizedApiError(err: unknown, fallback: string): string {
return legacyT(parseApiError(err, fallback))
@@ -3398,17 +3392,8 @@ interface GeminiCliQuotaItem {
resetSeconds: number | null
}
function hasAntigravityQuotaData(metadata: UpstreamMetadata | null | undefined): boolean {
const quotaByModel = metadata?.antigravity?.quota_by_model
return !!quotaByModel && typeof quotaByModel === 'object' && Object.keys(quotaByModel).length > 0
}
function hasAntigravityQuotaDisplayData(key: EndpointAPIKey): boolean {
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
if (Array.isArray(quota?.windows) && quota.windows.length > 0) {
return true
}
return hasAntigravityQuotaData(key.upstream_metadata)
return getAntigravityQuotaGroupItems(key).length > 0
}
function getGeminiCliQuotaUpdatedAt(key: EndpointAPIKey): number | undefined {
@@ -3480,84 +3465,14 @@ function formatUpdatedAt(updatedAt: number): string {
const formatCodexUpdatedAt = formatUpdatedAt
const formatAntigravityUpdatedAt = formatUpdatedAt
function secondsUntilReset(resetTime: string): number | null {
if (!resetTime) return null
const ts = Date.parse(resetTime)
if (Number.isNaN(ts)) return null
const diff = Math.floor((ts - Date.now()) / 1000)
return diff > 0 ? diff : 0
}
function secondsUntilUnixReset(resetAt: number | string | null | undefined): number | null {
const numericResetAt = Number(resetAt)
if (!Number.isFinite(numericResetAt) || numericResetAt <= 0) return null
const now = Math.floor(Date.now() / 1000)
return Math.max(Math.floor(numericResetAt - now), 0)
}
function coerceAntigravityPercent(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 100)
}
function coerceAntigravityRemainingFraction(value: number | string | null | undefined): number | undefined {
const numericValue = Number(value)
if (!Number.isFinite(numericValue)) return undefined
return Math.min(Math.max(numericValue, 0), 1)
}
function getAntigravityQuotaItems(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaItem[] {
const quotaByModel = metadata?.antigravity?.quota_by_model
if (!quotaByModel || typeof quotaByModel !== 'object') return []
const items: AntigravityQuotaItem[] = []
const opaqueDisplayIndex = { value: 1 }
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue
const info: Partial<AntigravityModelQuota> = rawInfo || {}
let usedPercent = coerceAntigravityPercent(info.used_percent)
if (usedPercent === undefined) {
const remainingFraction = coerceAntigravityRemainingFraction(info.remaining_fraction)
if (remainingFraction !== undefined) {
usedPercent = (1 - remainingFraction) * 100
} else {
continue
}
}
usedPercent = coerceAntigravityPercent(usedPercent) ?? 0
const remainingPercent = Math.max(100 - usedPercent, 0)
let resetSeconds = secondsUntilUnixReset(info.reset_at)
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
resetSeconds = secondsUntilReset(info.reset_time.trim()) ?? resetSeconds
}
items.push({
model,
label: resolveAntigravityQuotaLabel(model, info.display_name, opaqueDisplayIndex),
usedPercent,
remainingPercent,
resetSeconds,
})
}
items.sort(compareAntigravityQuotaItems)
return dedupeAntigravityQuotaItemsByLabel(items)
}
function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQuotaItem[] {
function getAntigravityQuotaGroupItems(key: EndpointAPIKey): AntigravityQuotaItem[] {
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
const windows = getQuotaWindowByScope(quota, 'model')
const windows = getQuotaWindowByScope(quota, 'quota_group')
if (!quota || windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
const items = windows
return windows
.map((window) => {
const model = String(window.model || window.label || window.code || '').trim()
const model = String(window.code || window.bucket_id || window.label || '').trim()
if (!model) return null
const usedPercent = getQuotaWindowUsedPercent(window)
@@ -3577,30 +3492,13 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
return {
model,
label: resolveAntigravityQuotaLabel(
model,
window.label || window.model,
opaqueDisplayIndex,
),
label: resolveAntigravityQuotaGroupLabel(window, t),
usedPercent: normalizedUsedPercent,
remainingPercent: normalizedRemainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
} satisfies AntigravityQuotaItem
})
.filter((item): item is AntigravityQuotaItem => item !== null)
items.sort(compareAntigravityQuotaItems)
return dedupeAntigravityQuotaItemsByLabel(items)
}
function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
if (snapshotItems.length > 0) return snapshotItems
return getAntigravityQuotaItems(key.upstream_metadata)
}
function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
return summarizeAntigravityQuotaItems(getAntigravityQuotaItemsForKey(key))
}
function getResetCountdownText(
@@ -1,8 +1,9 @@
import { describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { createApp, defineComponent, h } from 'vue'
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
import type { QuotaStatusSnapshot, UpstreamMetadata } from '@/api/endpoints/types'
import { createI18n, setI18nLocale, type Locale } from '@/i18n'
vi.mock('@/components/ui', async () => {
const { defineComponent, h } = await import('vue')
@@ -77,7 +78,12 @@ vi.mock('@/utils/errorParser', () => ({
parseApiError: (value: unknown) => String(value),
}))
function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot) {
function mount(
metadata: UpstreamMetadata,
quotaSnapshot?: QuotaStatusSnapshot,
locale: Locale = 'zh-CN',
) {
setI18nLocale(locale)
const root = document.createElement('div')
document.body.appendChild(root)
@@ -91,6 +97,7 @@ function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot)
})
},
}))
app.use(createI18n())
app.mount(root)
return {
@@ -103,140 +110,122 @@ function mount(metadata: UpstreamMetadata, quotaSnapshot?: QuotaStatusSnapshot)
}
describe('AntigravityQuotaDialog', () => {
it('renders grouped five-hour and weekly quota windows', () => {
afterEach(() => {
setI18nLocale('zh-CN')
})
it('renders only compact localized quota-group windows in Chinese', () => {
const { root, unmount } = mount({}, {
code: 'ok',
provider_type: 'antigravity',
exhausted: false,
windows: [{
code: 'group:0:3p-5h',
code: 'group:0:gemini-weekly',
label: 'Gemini Models · Weekly Limit Remaining',
scope: 'quota_group',
quota_group_label: 'Gemini models',
bucket_id: 'gemini-weekly',
window: 'weekly',
used_ratio: 0.1,
remaining_ratio: 0.9,
}, {
code: 'group:1:3p-5h',
label: 'Claude and GPT models · 5 hour',
scope: 'quota_group',
quota_group_label: 'Claude and GPT models',
bucket_id: '3p-5h',
window: '5h',
used_ratio: 0.75,
remaining_ratio: 0.25,
}, {
code: 'group:0:3p-weekly',
label: 'Claude and GPT models · weekly',
code: 'group:1:3p-weekly',
label: 'Claude and GPT models · Weekly Limit Remaining',
scope: 'quota_group',
quota_group_label: 'Claude and GPT models',
bucket_id: '3p-weekly',
window: 'weekly',
used_ratio: 0.2,
remaining_ratio: 0.8,
}, {
code: 'model:gemini-3.7-flash-tiered',
label: 'Gemini 3.7 Flash',
scope: 'model',
model: 'gemini-3.7-flash-tiered',
used_ratio: 0.05,
remaining_ratio: 0.95,
}],
})
const text = root.textContent || ''
expect(text).toContain('Claude and GPT models · 5 hour')
expect(text).toContain('Gemini Models · ')
expect(text).toContain('90.0%')
expect(text).toContain('Claude and GPT models · 5小时')
expect(text).toContain('25.0%')
expect(text).toContain('Claude and GPT models · weekly')
expect(text).toContain('Claude and GPT models · ')
expect(text).toContain('80.0%')
expect(text).not.toContain('Weekly Limit Remaining')
expect(text).not.toContain('Gemini额度')
expect(text).not.toContain('Claude & ChatGPT')
expect(text).not.toContain('Gemini 3.7 Flash')
unmount()
})
it('hides quota buckets outside the shared pool summary families', () => {
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
[rawIdentifier]: {
display_name: rawIdentifier,
remaining_fraction: 0.25,
used_percent: 75,
},
},
},
})
it('uses Weekly and 5 Hours for the English locale', () => {
const { root, unmount } = mount({}, {
code: 'ok',
provider_type: 'antigravity',
exhausted: false,
windows: [{
code: 'group:0:gemini-weekly',
label: 'Gemini Models · Weekly Limit Remaining',
scope: 'quota_group',
quota_group_label: 'Gemini Models',
bucket_id: 'gemini-weekly',
window: 'weekly',
remaining_ratio: 0.9,
}, {
code: 'group:1:3p-5h',
label: 'Claude and GPT models · 5 hour',
scope: 'quota_group',
quota_group_label: 'Claude and GPT models',
bucket_id: '3p-5h',
window: '5h',
remaining_ratio: 0.25,
}],
}, 'en-US')
const text = root.textContent || ''
expect(root.textContent).toContain('暂无配额数据')
expect(root.textContent).not.toContain('Key-1')
expect(root.textContent).not.toContain(rawIdentifier)
expect(root.textContent).not.toContain('25.0%')
expect(text).toContain('Gemini Models · Weekly')
expect(text).toContain('Claude and GPT models · 5 Hours')
expect(text).not.toContain('Weekly Limit Remaining')
unmount()
})
it('renders only the shared Gemini and Claude ChatGPT family summaries', () => {
it('does not fall back to the removed model-family summaries', () => {
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-flash-agent': {
display_name: 'Gemini 3.5 Flash (High)',
remaining_fraction: 0.9,
used_percent: 10,
},
'gemini-pro-agent': {
display_name: 'gemini-pro-agent',
remaining_fraction: 0.95,
used_percent: 5,
},
'gemini-3.5-flash-low': {
display_name: 'Gemini 3.5 Flash (Medium)',
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).not.toContain('gemini-pro-agent')
expect(text).not.toContain('claude-opus-4-6-thinking')
expect(text).toContain('Gemini额度')
expect(text).toContain('80%95%')
expect(text).toContain('Claude & ChatGPT')
expect(text).toContain('100%')
expect(text).not.toContain('Gemini 3.1 Pro (High)')
expect(text).not.toContain('Gemini 3.5 Flash (High)')
expect(text).not.toContain('Gemini 3.5 Flash (Medium)')
expect(text).not.toContain('Tab Flash Lite Preview')
expect(text).not.toContain('chat_20706')
unmount()
})
it('collapses duplicate model buckets into one family range', () => {
const { root, unmount } = mount({
antigravity: {
quota_by_model: {
'gemini-3.1-pro-high': {
display_name: 'Gemini 3.1 Pro (High)',
remaining_fraction: 0.95,
used_percent: 5,
},
'gemini-pro-agent': {
display_name: 'Gemini 3.1 Pro (High)',
remaining_fraction: 0.4,
used_percent: 60,
},
'gemini-3-flash-agent': {
display_name: 'Gemini 3.5 Flash (High)',
remaining_fraction: 0.9,
used_percent: 10,
},
},
},
})
const text = root.textContent || ''
expect(text.match(/Gemini额度/g)).toHaveLength(1)
expect(text).toContain('40%90%')
expect(text).not.toContain('95.0%')
expect(text).not.toContain('Gemini 3.1 Pro (High)')
expect(text).not.toContain('Gemini 3.5 Flash (High)')
expect(root.textContent).toContain('暂无配额数据')
expect(root.textContent).not.toContain('Gemini额度')
expect(root.textContent).not.toContain('Claude & ChatGPT')
expect(root.textContent).not.toContain('Gemini 3.5 Flash (High)')
expect(root.textContent).not.toContain('Claude Opus 4.6 (Thinking)')
unmount()
})
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest'
import { messages } from '@/i18n/messages'
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
describe('resolveAntigravityQuotaGroupLabel', () => {
it('uses the localized compact weekly label instead of the upstream display name', () => {
const window = {
code: 'group:0:gemini-weekly',
label: 'Gemini Models · Weekly Limit Remaining',
quota_group_label: 'Gemini models',
bucket_id: 'gemini-weekly',
window: 'weekly',
}
expect(resolveAntigravityQuotaGroupLabel(
window,
key => messages['zh-CN'][key],
)).toBe('Gemini Models · 周')
expect(resolveAntigravityQuotaGroupLabel(
window,
key => messages['en-US'][key],
)).toBe('Gemini Models · Weekly')
})
it('normalizes the legacy Claude family name and detects a five-hour bucket', () => {
const window = {
code: 'group:1:3p-5h',
label: 'Claude & ChatGPT · 5 hour',
quota_group_label: 'Claude & ChatGPT',
bucket_id: '3p-5h',
}
expect(resolveAntigravityQuotaGroupLabel(
window,
key => messages['zh-CN'][key],
)).toBe('Claude and GPT models · 5小时')
expect(resolveAntigravityQuotaGroupLabel(
window,
key => messages['en-US'][key],
)).toBe('Claude and GPT models · 5 Hours')
})
})
@@ -1,24 +0,0 @@
import { describe, expect, it } from 'vitest'
import { summarizeAntigravityQuotaItems } from '@/features/providers/utils/antigravityQuota'
describe('summarizeAntigravityQuotaItems', () => {
it('groups model families without collapsing their independent quota values', () => {
const items = summarizeAntigravityQuotaItems([
{ model: 'claude-opus-4-6-thinking', label: 'Claude Opus', remainingPercent: 100, resetSeconds: 60 },
{ model: 'claude-sonnet-4-6', label: 'Claude Sonnet', remainingPercent: 82, resetSeconds: 120 },
{ model: 'gemini-3.1-pro-high', label: 'Gemini Pro', remainingPercent: 90.6, resetSeconds: 180 },
{ model: 'gemini-3-flash-agent', label: 'Gemini Flash', remainingPercent: 95, resetSeconds: 240 },
{ model: 'gpt-oss-120b-medium', label: 'GPT-OSS', remainingPercent: 100, resetSeconds: 300 },
{ model: 'tab_flash_lite_preview', label: 'Tab', remainingPercent: 76, resetSeconds: 360 },
{ model: 'chat_20706', label: 'Chat', remainingPercent: 64, resetSeconds: 420 },
])
expect(items.map(item => [item.label, item.remainingPercent, item.detail])).toEqual([
['Gemini额度', 90.6, '90.6%95%'],
['Claude & ChatGPT', 82, '82%100%'],
])
expect(items.map(item => item.resetSeconds)).toEqual([180, 120])
expect(items[1]?.model).toBe('claude-sonnet-4-6')
})
})
@@ -1,3 +1,5 @@
import type { QuotaWindowSnapshot } from '@/api/endpoints/types'
export interface AntigravityQuotaSortableItem {
model: string
label: string
@@ -6,13 +8,11 @@ export interface AntigravityQuotaSortableItem {
detail?: string
}
const ANTIGRAVITY_QUOTA_GROUPS = [
{ label: 'Gemini额度', matches: (model: string) => model.startsWith('gemini-') },
{
label: 'Claude & ChatGPT',
matches: (model: string) => model.startsWith('claude-') || model.startsWith('gpt-'),
},
] as const
type AntigravityQuotaPeriodMessageKey =
| 'providers.antigravity.quota.period.weekly'
| 'providers.antigravity.quota.period.fiveHours'
type AntigravityQuotaTranslator = (key: AntigravityQuotaPeriodMessageKey) => string
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
'gemini-pro-agent': 'Gemini 3.1 Pro (High)',
@@ -125,39 +125,63 @@ export function dedupeAntigravityQuotaItemsByLabel<T extends AntigravityQuotaSor
return Array.from(selectedByLabel.values()).sort(compareAntigravityQuotaItems)
}
export function summarizeAntigravityQuotaItems<T extends AntigravityQuotaSortableItem>(
items: T[],
): T[] {
const itemsByGroup = new Map<string, T[]>()
for (const item of items) {
const normalizedModel = item.model.trim().toLowerCase().replace(/^model:/, '')
const group = ANTIGRAVITY_QUOTA_GROUPS.find(candidate => candidate.matches(normalizedModel))
if (!group) continue
const groupedItems = itemsByGroup.get(group.label) ?? []
groupedItems.push(item)
itemsByGroup.set(group.label, groupedItems)
function canonicalizeAntigravityQuotaGroupLabel(rawLabel: string): string {
const normalized = rawLabel
.trim()
.toLowerCase()
.replace(/[._-]+/g, ' ')
.replace(/\s+/g, ' ')
if (normalized === 'gemini额度' || /^gemini(?: models?)?$/.test(normalized)) {
return 'Gemini Models'
}
if (
normalized === 'claude & chatgpt'
|| /^claude\s*(?:&|and)\s*(?:gpt|chatgpt)(?: models?)?$/.test(normalized)
) {
return 'Claude and GPT models'
}
return rawLabel.trim()
}
function getAntigravityQuotaGroupName(window: QuotaWindowSnapshot): string {
const explicitGroupLabel = String(window.quota_group_label || '').trim()
if (explicitGroupLabel) {
return canonicalizeAntigravityQuotaGroupLabel(explicitGroupLabel)
}
return ANTIGRAVITY_QUOTA_GROUPS.map(group => group.label)
.map((label) => {
const groupedItems = itemsByGroup.get(label)
if (!groupedItems?.length) return undefined
const remainingValues = groupedItems
.map(item => item.remainingPercent)
.sort((left, right) => left - right)
const minRemaining = remainingValues[0] ?? 0
const maxRemaining = remainingValues.at(-1) ?? minRemaining
const selected = groupedItems.find(item => item.remainingPercent === minRemaining) ?? groupedItems[0]
const detail = Math.abs(maxRemaining - minRemaining) < 1e-6
? formatAntigravityQuotaValue(minRemaining)
: `${formatAntigravityQuotaValue(minRemaining)}${formatAntigravityQuotaValue(maxRemaining)}`
return { ...selected, label, remainingPercent: minRemaining, detail }
})
.filter((item): item is T => item !== undefined)
const rawLabel = String(window.label || '').trim()
const groupLabel = rawLabel.split(/\s*·\s*/, 1)[0]
?.replace(/\s+(?:weekly limit remaining|weekly|5\s*hours?|5h)$/i, '')
.trim()
return canonicalizeAntigravityQuotaGroupLabel(groupLabel || rawLabel)
}
function formatAntigravityQuotaValue(value: number): string {
const rounded = Math.round(value)
const formatted = Math.abs(value - rounded) < 1e-6 ? String(rounded) : value.toFixed(1)
return `${formatted}%`
function getAntigravityQuotaPeriodLabel(
window: QuotaWindowSnapshot,
translate: AntigravityQuotaTranslator,
): string {
const periodCandidates = [window.window, window.bucket_id, window.code, window.label]
.map(value => String(value || '').trim())
.filter(Boolean)
const normalized = periodCandidates.join(' ').toLowerCase()
if (/(?:^|[^a-z])weekly(?:[^a-z]|$)/.test(normalized)) {
return translate('providers.antigravity.quota.period.weekly')
}
if (/(?:^|[^a-z0-9])5\s*(?:h|hours?)(?:[^a-z0-9]|$)/.test(normalized)) {
return translate('providers.antigravity.quota.period.fiveHours')
}
return String(window.window || '').trim()
}
export function resolveAntigravityQuotaGroupLabel(
window: QuotaWindowSnapshot,
translate: AntigravityQuotaTranslator,
): string {
const groupName = getAntigravityQuotaGroupName(window)
const periodLabel = getAntigravityQuotaPeriodLabel(window, translate)
if (groupName && periodLabel) return `${groupName} · ${periodLabel}`
return groupName || String(window.label || window.code || '').trim()
}
+4
View File
@@ -334,6 +334,8 @@ export const messages = {
'nav.cacheMonitoring': '缓存监控',
'nav.moduleManagement': '模块管理',
'nav.systemSettings': '系统设置',
'providers.antigravity.quota.period.weekly': '周',
'providers.antigravity.quota.period.fiveHours': '5小时',
'providers.modelMapping.scope.allRequests': '所有请求',
'providers.modelMapping.scope.sessionCompactionOnly': '仅会话压缩',
'providers.modelMapping.scope.customOperations': '仅匹配:{operations}',
@@ -684,6 +686,8 @@ export const messages = {
'nav.cacheMonitoring': 'Cache monitoring',
'nav.moduleManagement': 'Modules',
'nav.systemSettings': 'System settings',
'providers.antigravity.quota.period.weekly': 'Weekly',
'providers.antigravity.quota.period.fiveHours': '5 Hours',
'providers.modelMapping.scope.allRequests': 'All requests',
'providers.modelMapping.scope.sessionCompactionOnly': 'Session compaction only',
'providers.modelMapping.scope.customOperations': 'Match only: {operations}',
+12 -16
View File
@@ -593,8 +593,8 @@
<button
v-if="item.key === 'priority'"
type="button"
class="inline-flex max-w-full items-center rounded-full border px-2 py-0.5 text-[10px] font-medium leading-4"
:class="`${getMobileTagClass(item)} hover:border-primary/40 hover:text-foreground`"
class="inline-flex max-w-full items-center rounded-full border px-2 py-0.5 text-[10px] font-medium leading-4 hover:border-primary/40 hover:text-foreground"
:class="getMobileTagClass(item)"
:title="`${item.label},点击编辑优先级`"
@click="quickEditInternalPriority(key)"
>
@@ -1143,11 +1143,7 @@ import {
} from '@/features/pool/utils/poolStatsDisplay'
import { resetCodexCycleUsageWindows } from '@/features/pool/utils/poolCycleStats'
import { mergePoolKeyQuotaSnapshots } from '@/features/pool/utils/poolQuotaRefresh'
import {
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota'
import { resolveAntigravityQuotaGroupLabel } from '@/features/providers/utils/antigravityQuota'
import {
clearPendingCodexResetCreditIdempotencyKey,
clearPendingCodexResetCreditIdempotencyKeyForOutcome,
@@ -1197,7 +1193,7 @@ function prefetchProviderDetailDrawer(): void {
type PoolKeyScore = NonNullable<PoolKeyDetail['pool_score']>
const { success, error: showError, warning: showWarning } = useToast()
const { legacyT } = useI18n()
const { legacyT, t } = useI18n()
const { confirm } = useConfirm()
const { copyToClipboard } = useClipboard()
const { tick: countdownTick, start: startCountdownTimer } = useCountdownTimer()
@@ -3864,17 +3860,17 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
}
if (providerType === 'antigravity') {
const windows = getQuotaSnapshotWindowsByScope(quota, 'model')
const windows = getQuotaSnapshotWindowsByScope(quota, 'quota_group')
if (windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
return summarizeAntigravityQuotaItems(dedupeAntigravityQuotaItemsByLabel(windows
.map((window): (QuotaProgressItem & { model: string, resetSeconds: number | null }) | null => {
return windows
.map((window, index): QuotaProgressItem | null => {
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (remainingPercent == null) return null
const model = String(window.model || window.code || '').trim().replace(/^model:/i, '')
const label = resolveAntigravityQuotaGroupLabel(window, t)
if (!label) return null
return {
model,
label: resolveAntigravityQuotaLabel(model, window.label, opaqueDisplayIndex),
label,
sortOrder: index,
remainingPercent,
resetAtSeconds: normalizeUnixSeconds(window.reset_at ?? quota.reset_at ?? null),
resetSeconds: normalizeRemainingSeconds(window.reset_seconds ?? quota.reset_seconds ?? null),
@@ -3882,7 +3878,7 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
allowDynamicReset: true,
}
})
.filter((item): item is QuotaProgressItem & { model: string, resetSeconds: number | null } => item != null)))
.filter((item): item is QuotaProgressItem => item != null)
}
if (providerType === 'gemini_cli') {