mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 18:59:50 +08:00
fix(frontend): deduplicate Antigravity quota groups
This commit is contained in:
@@ -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(
|
||||
|
||||
+80
-91
@@ -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()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user