Unify quota snapshots and oauth refresh handling

This commit is contained in:
fawney19
2026-04-17 18:22:41 +08:00
parent b8702ae124
commit 7eae1f90f6
38 changed files with 3435 additions and 458 deletions
@@ -112,12 +112,14 @@ import {
} from '@/components/ui'
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 { parseApiError } from '@/utils/errorParser'
const props = defineProps<{
open: boolean
metadata: Record<string, unknown> | null
metadata: UpstreamMetadata | null
quotaSnapshot?: QuotaStatusSnapshot | null
keyName: string
providerId?: string
keyId?: string
@@ -138,14 +140,85 @@ interface QuotaItem {
const { error: showError, success: showSuccess } = useToast()
const testingModel = ref<string | null>(null)
function getQuotaSnapshotUpdatedAt(quota: QuotaStatusSnapshot | null | undefined): number | undefined {
const updatedAt = quota?.updated_at ?? quota?.observed_at
return typeof updatedAt === 'number' ? updatedAt : undefined
}
function getQuotaWindowLiveResetSeconds(
quota: QuotaStatusSnapshot | null | undefined,
window: QuotaWindowSnapshot | null | undefined,
): number | null {
if (!window) return null
const now = Math.floor(Date.now() / 1000)
if (typeof window.reset_at === 'number') {
return Math.max(window.reset_at - now, 0)
}
if (typeof window.reset_seconds === 'number') {
const updatedAt = getQuotaSnapshotUpdatedAt(quota)
const elapsed = typeof updatedAt === 'number' ? Math.max(now - updatedAt, 0) : 0
return Math.max(window.reset_seconds - elapsed, 0)
}
return null
}
function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] {
if (!quota) return []
const providerType = String(quota.provider_type || '').trim().toLowerCase()
if (providerType && providerType !== 'antigravity') return []
const windows = Array.isArray(quota.windows)
? quota.windows.filter(window => String(window?.scope || '').trim().toLowerCase() === 'model')
: []
if (windows.length === 0) return []
const items = windows
.map((window) => {
const model = String(window.model || window.label || window.code || '').trim()
if (!model) return null
const usedPercent =
typeof window.used_ratio === 'number'
? Math.max(Math.min(window.used_ratio * 100, 100), 0)
: typeof window.remaining_ratio === 'number'
? Math.max(Math.min((1 - window.remaining_ratio) * 100, 100), 0)
: null
if (usedPercent == null) return null
const remainingPercent =
typeof window.remaining_ratio === 'number'
? Math.max(Math.min(window.remaining_ratio * 100, 100), 0)
: Math.max(100 - usedPercent, 0)
return {
model,
label: String(window.label || window.model || model),
usedPercent,
remainingPercent,
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
} satisfies QuotaItem
})
.filter((item): item is QuotaItem => item !== null)
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
return items
}
const items = computed<QuotaItem[]>(() => {
const snapshotItems = buildItemsFromQuotaSnapshot(props.quotaSnapshot)
if (snapshotItems.length > 0) return snapshotItems
const antigravity = props.metadata?.antigravity
if (!antigravity || typeof antigravity !== 'object') return []
const quotaByModel = (antigravity as Record<string, unknown>).quota_by_model
const quotaByModel = antigravity.quota_by_model
if (!quotaByModel || typeof quotaByModel !== 'object') return []
const result: QuotaItem[] = []
for (const [model, rawInfo] of Object.entries(quotaByModel as Record<string, unknown>)) {
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
if (!model) continue
const info = (rawInfo || {}) as Record<string, unknown>