mirror of
https://github.com/fawney19/Aether.git
synced 2026-10-09 10:57:03 +08:00
Merge pull request #663 from MMEXA/codex/gemini-interactions-antigravity-20260705
完善 Gemini Interactions 与 Antigravity 全链路兼容
This commit is contained in:
@@ -34,9 +34,10 @@
|
||||
<DropdownMenuItem
|
||||
v-for="item in items"
|
||||
:key="item.model"
|
||||
:title="item.model"
|
||||
@select="handleTestModel(item.model)"
|
||||
>
|
||||
{{ item.model }}
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -52,12 +53,14 @@
|
||||
:key="item.model"
|
||||
>
|
||||
<div class="flex items-center justify-between text-[10px] mb-0.5">
|
||||
<span
|
||||
class="text-muted-foreground truncate mr-2 min-w-0 flex-1"
|
||||
:title="item.model"
|
||||
>
|
||||
{{ item.label }}
|
||||
</span>
|
||||
<div class="min-w-0 flex-1 mr-2">
|
||||
<div
|
||||
class="text-muted-foreground truncate"
|
||||
:title="item.model"
|
||||
>
|
||||
{{ item.label }}
|
||||
</div>
|
||||
</div>
|
||||
<span :class="getQuotaRemainingClass(item.usedPercent)">
|
||||
{{ item.remainingPercent.toFixed(1) }}%
|
||||
</span>
|
||||
@@ -115,6 +118,11 @@ 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'
|
||||
import {
|
||||
compareAntigravityQuotaItems,
|
||||
dedupeAntigravityQuotaItemsByLabel,
|
||||
resolveAntigravityQuotaLabel,
|
||||
} from '@/features/providers/utils/antigravityQuota'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -165,6 +173,25 @@ function getQuotaWindowLiveResetSeconds(
|
||||
return null
|
||||
}
|
||||
|
||||
function coercePercent(value: unknown): number | null {
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return null
|
||||
return Math.min(Math.max(numericValue, 0), 100)
|
||||
}
|
||||
|
||||
function coerceRemainingFraction(value: unknown): number | null {
|
||||
const numericValue = Number(value)
|
||||
if (!Number.isFinite(numericValue)) return null
|
||||
return Math.min(Math.max(numericValue, 0), 1)
|
||||
}
|
||||
|
||||
function secondsUntilUnixReset(resetAt: unknown): 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 buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefined): QuotaItem[] {
|
||||
if (!quota) return []
|
||||
|
||||
@@ -175,6 +202,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
|
||||
? quota.windows.filter(window => String(window?.scope || '').trim().toLowerCase() === 'model')
|
||||
: []
|
||||
if (windows.length === 0) return []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
|
||||
const items = windows
|
||||
.map((window) => {
|
||||
@@ -196,7 +224,7 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
|
||||
|
||||
return {
|
||||
model,
|
||||
label: String(window.label || window.model || model),
|
||||
label: resolveAntigravityQuotaLabel(model, window.label || window.model, opaqueDisplayIndex),
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
|
||||
@@ -204,8 +232,8 @@ function buildItemsFromQuotaSnapshot(quota: QuotaStatusSnapshot | null | undefin
|
||||
})
|
||||
.filter((item): item is QuotaItem => item !== null)
|
||||
|
||||
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
|
||||
return items
|
||||
items.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(items)
|
||||
}
|
||||
|
||||
const items = computed<QuotaItem[]>(() => {
|
||||
@@ -218,26 +246,26 @@ const items = computed<QuotaItem[]>(() => {
|
||||
if (!quotaByModel || typeof quotaByModel !== 'object') return []
|
||||
|
||||
const result: QuotaItem[] = []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
for (const [model, rawInfo] of Object.entries(quotaByModel)) {
|
||||
if (!model) continue
|
||||
const info = (rawInfo || {}) as Record<string, unknown>
|
||||
|
||||
let usedPercent = Number(info['used_percent'])
|
||||
if (!Number.isFinite(usedPercent)) {
|
||||
const remainingFraction = Number(info['remaining_fraction'])
|
||||
if (Number.isFinite(remainingFraction)) {
|
||||
let usedPercent = coercePercent(info['used_percent'])
|
||||
if (usedPercent === null) {
|
||||
const remainingFraction = coerceRemainingFraction(info['remaining_fraction'])
|
||||
if (remainingFraction !== null) {
|
||||
usedPercent = (1 - remainingFraction) * 100
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (usedPercent < 0) usedPercent = 0
|
||||
if (usedPercent > 100) usedPercent = 100
|
||||
usedPercent = coercePercent(usedPercent) ?? 0
|
||||
|
||||
const remainingPercent = Math.max(100 - usedPercent, 0)
|
||||
|
||||
let resetSeconds: number | null = null
|
||||
let resetSeconds = secondsUntilUnixReset(info['reset_at'])
|
||||
const resetTime = info['reset_time']
|
||||
if (typeof resetTime === 'string' && resetTime.trim()) {
|
||||
const ts = Date.parse(resetTime.trim())
|
||||
@@ -247,11 +275,17 @@ const items = computed<QuotaItem[]>(() => {
|
||||
}
|
||||
}
|
||||
|
||||
result.push({ model, label: model, usedPercent, remainingPercent, resetSeconds })
|
||||
result.push({
|
||||
model,
|
||||
label: resolveAntigravityQuotaLabel(model, info['display_name'], opaqueDisplayIndex),
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
result.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
|
||||
return result
|
||||
result.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(result)
|
||||
})
|
||||
|
||||
async function handleTestModel(modelName: string) {
|
||||
|
||||
@@ -361,31 +361,39 @@
|
||||
/>
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<ProviderQuotaProgressRow
|
||||
v-for="group in getAntigravityQuotaSummaryForKey(key)"
|
||||
:key="group.key"
|
||||
:label="group.label"
|
||||
:used-percent="group.usedPercent"
|
||||
:remaining-percent="group.remainingPercent"
|
||||
:meter-class="getQuotaRemainingClass(group.usedPercent)"
|
||||
:bar-class="getQuotaRemainingBarColor(group.usedPercent)"
|
||||
v-for="item in getAntigravityQuotaPreviewForKey(key)"
|
||||
:key="item.model"
|
||||
:label="item.label"
|
||||
:title="item.model"
|
||||
:used-percent="item.usedPercent"
|
||||
:remaining-percent="item.remainingPercent"
|
||||
:meter-class="getQuotaRemainingClass(item.usedPercent)"
|
||||
:bar-class="getQuotaRemainingBarColor(item.usedPercent)"
|
||||
>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="group.resetSeconds !== null || group.usedPercent > 0"
|
||||
class="text-[9px] text-muted-foreground/70 mt-0.5"
|
||||
>
|
||||
<template v-if="group.resetSeconds !== null && group.resetSeconds > 0">
|
||||
{{ formatResetTime(group.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else-if="group.resetSeconds !== null && group.resetSeconds <= 0">
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ legacyT('重置时间未知') }}
|
||||
</template>
|
||||
<div class="mt-0.5 space-y-0.5">
|
||||
<div
|
||||
v-if="item.resetSeconds !== null"
|
||||
class="text-[9px] text-muted-foreground/70"
|
||||
>
|
||||
<template v-if="item.resetSeconds !== null && item.resetSeconds > 0">
|
||||
{{ formatResetTime(item.resetSeconds) }}{{ legacyT('后重置') }}
|
||||
</template>
|
||||
<template v-else-if="item.resetSeconds !== null && item.resetSeconds <= 0">
|
||||
{{ legacyT('已重置') }}
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</ProviderQuotaProgressRow>
|
||||
<button
|
||||
v-if="getAntigravityQuotaHiddenCountForKey(key) > 0"
|
||||
type="button"
|
||||
class="col-span-2 text-left text-[10px] text-muted-foreground hover:text-foreground transition-colors"
|
||||
@click="openAntigravityQuotaDialog(key)"
|
||||
>
|
||||
{{ legacyT('另有') }} {{ getAntigravityQuotaHiddenCountForKey(key) }} {{ legacyT('个模型,查看全部') }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
@@ -949,6 +957,11 @@ 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,
|
||||
} from '@/features/providers/utils/antigravityQuota'
|
||||
import {
|
||||
deleteEndpointKey,
|
||||
recoverKeyHealth,
|
||||
@@ -3348,53 +3361,72 @@ function secondsUntilReset(resetTime: string): number | null {
|
||||
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 = Number(info.used_percent)
|
||||
if (!Number.isFinite(usedPercent)) {
|
||||
const remainingFraction = Number(info.remaining_fraction)
|
||||
if (Number.isFinite(remainingFraction)) {
|
||||
let usedPercent = coerceAntigravityPercent(info.used_percent)
|
||||
if (usedPercent === undefined) {
|
||||
const remainingFraction = coerceAntigravityRemainingFraction(info.remaining_fraction)
|
||||
if (remainingFraction !== undefined) {
|
||||
usedPercent = (1 - remainingFraction) * 100
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (usedPercent < 0) usedPercent = 0
|
||||
if (usedPercent > 100) usedPercent = 100
|
||||
usedPercent = coerceAntigravityPercent(usedPercent) ?? 0
|
||||
|
||||
const remainingPercent = Math.max(100 - usedPercent, 0)
|
||||
|
||||
let resetSeconds: number | null = null
|
||||
let resetSeconds = secondsUntilUnixReset(info.reset_at)
|
||||
if (typeof info.reset_time === 'string' && info.reset_time.trim()) {
|
||||
resetSeconds = secondsUntilReset(info.reset_time.trim())
|
||||
resetSeconds = secondsUntilReset(info.reset_time.trim()) ?? resetSeconds
|
||||
}
|
||||
|
||||
items.push({
|
||||
model,
|
||||
label: model,
|
||||
label: resolveAntigravityQuotaLabel(model, info.display_name, opaqueDisplayIndex),
|
||||
usedPercent,
|
||||
remainingPercent,
|
||||
resetSeconds,
|
||||
})
|
||||
}
|
||||
|
||||
// 按“最紧张”(已用最多)优先排序,便于快速定位额度风险;完整列表通过滚动展示
|
||||
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
|
||||
return items
|
||||
items.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(items)
|
||||
}
|
||||
|
||||
function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
const quota = getQuotaSnapshotForProvider(key, 'antigravity')
|
||||
const windows = getQuotaWindowByScope(quota, 'model')
|
||||
if (!quota || windows.length === 0) return []
|
||||
const opaqueDisplayIndex = { value: 1 }
|
||||
|
||||
const items = windows
|
||||
.map((window) => {
|
||||
@@ -3418,7 +3450,11 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
|
||||
|
||||
return {
|
||||
model,
|
||||
label: String(window.label || window.model || model),
|
||||
label: resolveAntigravityQuotaLabel(
|
||||
model,
|
||||
window.label || window.model,
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
usedPercent: normalizedUsedPercent,
|
||||
remainingPercent: normalizedRemainingPercent,
|
||||
resetSeconds: getQuotaWindowLiveResetSeconds(quota, window),
|
||||
@@ -3426,124 +3462,24 @@ function getAntigravityQuotaItemsFromSnapshot(key: EndpointAPIKey): AntigravityQ
|
||||
})
|
||||
.filter((item): item is AntigravityQuotaItem => item !== null)
|
||||
|
||||
items.sort((a, b) => (b.usedPercent - a.usedPercent) || a.model.localeCompare(b.model))
|
||||
return items
|
||||
items.sort(compareAntigravityQuotaItems)
|
||||
return dedupeAntigravityQuotaItemsByLabel(items)
|
||||
}
|
||||
|
||||
// Antigravity 配额分组定义(按匹配优先级排列,具体规则在前)
|
||||
interface AntigravityQuotaGroup {
|
||||
key: string
|
||||
label: string
|
||||
match: (model: string) => boolean
|
||||
}
|
||||
const ANTIGRAVITY_QUOTA_PREVIEW_LIMIT = 6
|
||||
|
||||
const ANTIGRAVITY_QUOTA_GROUPS: AntigravityQuotaGroup[] = [
|
||||
{ key: 'claude', label: 'Claude', match: m => m.includes('claude') },
|
||||
{ key: 'gemini-2.5', label: 'Gemini 2.5', match: m => m.includes('gemini-2.5') || m.includes('gemini-2-5') },
|
||||
{ key: 'gemini-3', label: 'Gemini 3', match: m => m.includes('gemini-3') && !m.includes('image') },
|
||||
{ key: 'gemini-3-image', label: 'Gemini 3 Image', match: m => m.includes('gemini-3') && m.includes('image') },
|
||||
]
|
||||
|
||||
interface AntigravityQuotaSummaryItem {
|
||||
key: string
|
||||
label: string
|
||||
usedPercent: number // 组内最高已用百分比(最紧张)
|
||||
remainingPercent: number // 100 - usedPercent
|
||||
resetSeconds: number | null
|
||||
}
|
||||
|
||||
function getAntigravityQuotaSummary(metadata: UpstreamMetadata | null | undefined): AntigravityQuotaSummaryItem[] {
|
||||
const items = getAntigravityQuotaItems(metadata)
|
||||
if (!items.length) return []
|
||||
|
||||
// 将每个模型归入分组
|
||||
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>()
|
||||
|
||||
for (const item of items) {
|
||||
const model = item.model.toLowerCase()
|
||||
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model))
|
||||
if (!group) continue
|
||||
|
||||
const existing = groupMap.get(group.key)
|
||||
if (!existing) {
|
||||
groupMap.set(group.key, {
|
||||
label: group.label,
|
||||
maxUsed: item.usedPercent,
|
||||
resetSeconds: item.resetSeconds,
|
||||
})
|
||||
} else {
|
||||
if (item.usedPercent > existing.maxUsed) {
|
||||
existing.maxUsed = item.usedPercent
|
||||
}
|
||||
if (existing.resetSeconds === null) {
|
||||
existing.resetSeconds = item.resetSeconds
|
||||
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
|
||||
existing.resetSeconds = item.resetSeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按 ANTIGRAVITY_QUOTA_GROUPS 定义的顺序输出
|
||||
const result: AntigravityQuotaSummaryItem[] = []
|
||||
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
|
||||
const data = groupMap.get(group.key)
|
||||
if (!data) continue
|
||||
result.push({
|
||||
key: group.key,
|
||||
label: data.label,
|
||||
usedPercent: data.maxUsed,
|
||||
remainingPercent: Math.max(100 - data.maxUsed, 0),
|
||||
resetSeconds: data.resetSeconds,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function getAntigravityQuotaSummaryForKey(key: EndpointAPIKey): AntigravityQuotaSummaryItem[] {
|
||||
function getAntigravityQuotaItemsForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
const snapshotItems = getAntigravityQuotaItemsFromSnapshot(key)
|
||||
if (snapshotItems.length > 0) {
|
||||
const groupMap = new Map<string, { label: string, maxUsed: number, resetSeconds: number | null }>()
|
||||
if (snapshotItems.length > 0) return snapshotItems
|
||||
return getAntigravityQuotaItems(key.upstream_metadata)
|
||||
}
|
||||
|
||||
for (const item of snapshotItems) {
|
||||
const model = item.model.toLowerCase()
|
||||
const group = ANTIGRAVITY_QUOTA_GROUPS.find(g => g.match(model))
|
||||
if (!group) continue
|
||||
function getAntigravityQuotaPreviewForKey(key: EndpointAPIKey): AntigravityQuotaItem[] {
|
||||
return getAntigravityQuotaItemsForKey(key).slice(0, ANTIGRAVITY_QUOTA_PREVIEW_LIMIT)
|
||||
}
|
||||
|
||||
const existing = groupMap.get(group.key)
|
||||
if (!existing) {
|
||||
groupMap.set(group.key, {
|
||||
label: group.label,
|
||||
maxUsed: item.usedPercent,
|
||||
resetSeconds: item.resetSeconds,
|
||||
})
|
||||
} else {
|
||||
if (item.usedPercent > existing.maxUsed) {
|
||||
existing.maxUsed = item.usedPercent
|
||||
}
|
||||
if (existing.resetSeconds === null) {
|
||||
existing.resetSeconds = item.resetSeconds
|
||||
} else if (item.resetSeconds !== null && item.resetSeconds < existing.resetSeconds) {
|
||||
existing.resetSeconds = item.resetSeconds
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: AntigravityQuotaSummaryItem[] = []
|
||||
for (const group of ANTIGRAVITY_QUOTA_GROUPS) {
|
||||
const data = groupMap.get(group.key)
|
||||
if (!data) continue
|
||||
result.push({
|
||||
key: group.key,
|
||||
label: data.label,
|
||||
usedPercent: data.maxUsed,
|
||||
remainingPercent: Math.max(100 - data.maxUsed, 0),
|
||||
resetSeconds: data.resetSeconds,
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
return getAntigravityQuotaSummary(key.upstream_metadata)
|
||||
function getAntigravityQuotaHiddenCountForKey(key: EndpointAPIKey): number {
|
||||
return Math.max(getAntigravityQuotaItemsForKey(key).length - ANTIGRAVITY_QUOTA_PREVIEW_LIMIT, 0)
|
||||
}
|
||||
|
||||
function getResetCountdownText(
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { createApp, defineComponent, h } from 'vue'
|
||||
|
||||
import AntigravityQuotaDialog from '@/features/providers/components/AntigravityQuotaDialog.vue'
|
||||
import type { UpstreamMetadata } from '@/api/endpoints/types'
|
||||
|
||||
vi.mock('@/components/ui', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
const passthrough = (name: string) => defineComponent({
|
||||
name,
|
||||
setup(_, { slots }) {
|
||||
return () => h('div', [
|
||||
slots.headerActions?.(),
|
||||
slots.default?.(),
|
||||
slots.footer?.(),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
Dialog: passthrough('DialogStub'),
|
||||
DropdownMenu: passthrough('DropdownMenuStub'),
|
||||
DropdownMenuTrigger: passthrough('DropdownMenuTriggerStub'),
|
||||
DropdownMenuContent: passthrough('DropdownMenuContentStub'),
|
||||
DropdownMenuItem: defineComponent({
|
||||
name: 'DropdownMenuItemStub',
|
||||
emits: ['select'],
|
||||
setup(_, { emit, slots }) {
|
||||
return () => h('button', { type: 'button', onClick: () => emit('select') }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/components/ui/button.vue', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
|
||||
return {
|
||||
default: defineComponent({
|
||||
name: 'ButtonStub',
|
||||
setup(_, { attrs, slots }) {
|
||||
return () => h('button', { ...attrs, type: 'button' }, slots.default?.())
|
||||
},
|
||||
}),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('lucide-vue-next', async () => {
|
||||
const { defineComponent, h } = await import('vue')
|
||||
const Icon = defineComponent({
|
||||
name: 'IconStub',
|
||||
setup() {
|
||||
return () => h('span')
|
||||
},
|
||||
})
|
||||
|
||||
return {
|
||||
BarChart3: Icon,
|
||||
Loader2: Icon,
|
||||
Play: Icon,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/api/endpoints/providers', () => ({
|
||||
testModel: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/composables/useToast', () => ({
|
||||
useToast: () => ({
|
||||
error: vi.fn(),
|
||||
success: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/errorParser', () => ({
|
||||
parseApiError: (value: unknown) => String(value),
|
||||
}))
|
||||
|
||||
function mount(metadata: UpstreamMetadata) {
|
||||
const root = document.createElement('div')
|
||||
document.body.appendChild(root)
|
||||
|
||||
const app = createApp(defineComponent({
|
||||
setup() {
|
||||
return () => h(AntigravityQuotaDialog, {
|
||||
open: true,
|
||||
metadata,
|
||||
keyName: 'Key-1',
|
||||
})
|
||||
},
|
||||
}))
|
||||
app.mount(root)
|
||||
|
||||
return {
|
||||
root,
|
||||
unmount: () => {
|
||||
app.unmount()
|
||||
root.remove()
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
describe('AntigravityQuotaDialog', () => {
|
||||
it('renders opaque quota identifiers with concise visible labels', () => {
|
||||
const rawIdentifier = 'RateLimitResetCredit_05cbb6eeeb9c81918e011d8300f9ebfb'
|
||||
const { root, unmount } = mount({
|
||||
antigravity: {
|
||||
quota_by_model: {
|
||||
[rawIdentifier]: {
|
||||
display_name: rawIdentifier,
|
||||
remaining_fraction: 0.25,
|
||||
used_percent: 75,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
expect(root.textContent).toContain('Key-1')
|
||||
expect(root.textContent).not.toContain(rawIdentifier)
|
||||
expect(root.textContent).toContain('25.0%')
|
||||
|
||||
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-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.indexOf('Claude Opus 4.6 (Thinking)')).toBeLessThan(text.indexOf('Gemini 3.1 Pro (High)'))
|
||||
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)'))
|
||||
expect(text.indexOf('Gemini 3.5 Flash (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (Medium)'))
|
||||
expect(text.indexOf('Gemini 3.5 Flash (Medium)')).toBeLessThan(text.indexOf('Tab Flash Lite Preview'))
|
||||
expect(text.indexOf('Tab Flash Lite Preview')).toBeLessThan(text.indexOf('chat_20706'))
|
||||
|
||||
unmount()
|
||||
})
|
||||
|
||||
it('renders one row for duplicate quota labels and keeps the preferred active bucket', () => {
|
||||
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 3\.1 Pro \(High\)/g)).toHaveLength(1)
|
||||
expect(text).toContain('40.0%')
|
||||
expect(text).not.toContain('95.0%')
|
||||
expect(text.indexOf('Gemini 3.1 Pro (High)')).toBeLessThan(text.indexOf('Gemini 3.5 Flash (High)'))
|
||||
|
||||
unmount()
|
||||
})
|
||||
})
|
||||
@@ -5,6 +5,7 @@ import { getDefaultEndpointBaseUrl, getDefaultEndpointPath } from '../endpoint-d
|
||||
const apiFormats = [
|
||||
{ value: 'openai:chat', default_path: '/v1/chat/completions' },
|
||||
{ value: 'gemini:generate_content', default_path: '/v1beta/models/{model}:{action}' },
|
||||
{ value: 'gemini:interactions', default_path: '/v1/interactions' },
|
||||
{ value: 'gemini:embedding', default_path: '/v1beta/models/{model}:embedContent' },
|
||||
{ value: 'gemini:video', default_path: '/v1beta/models/{model}:predictLongRunning' },
|
||||
{ value: 'openai:responses', default_path: '/v1/responses' },
|
||||
@@ -31,6 +32,12 @@ describe('endpoint default paths', () => {
|
||||
apiFormats,
|
||||
})).toBe('/models/{model}:embedContent')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:interactions',
|
||||
providerType: 'custom',
|
||||
apiFormats,
|
||||
})).toBe('/interactions')
|
||||
|
||||
expect(getDefaultEndpointPath({
|
||||
apiFormat: 'gemini:video',
|
||||
providerType: 'custom',
|
||||
@@ -218,6 +225,11 @@ describe('endpoint default paths', () => {
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
})).toBe('https://generativelanguage.googleapis.com/v1beta')
|
||||
|
||||
expect(getDefaultEndpointBaseUrl({
|
||||
apiFormat: 'gemini:interactions',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
})).toBe('https://generativelanguage.googleapis.com/v1')
|
||||
|
||||
expect(getDefaultEndpointBaseUrl({
|
||||
apiFormat: 'gemini:video',
|
||||
baseUrl: 'https://generativelanguage.googleapis.com',
|
||||
|
||||
@@ -98,11 +98,17 @@ function usesVersionedApiRootByDefault(apiFormat: string): boolean {
|
||||
|| apiFormat === 'jina:rerank'
|
||||
|| apiFormat === 'claude:messages'
|
||||
|| apiFormat === 'gemini:generate_content'
|
||||
|| apiFormat === 'gemini:interactions'
|
||||
|| apiFormat === 'gemini:embedding'
|
||||
|| apiFormat === 'gemini:video'
|
||||
}
|
||||
|
||||
function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
|
||||
if (
|
||||
apiFormat === 'gemini:interactions'
|
||||
) {
|
||||
return '/v1'
|
||||
}
|
||||
if (
|
||||
apiFormat === 'gemini:generate_content'
|
||||
|| apiFormat === 'gemini:embedding'
|
||||
@@ -116,6 +122,7 @@ function versionedApiRootSuffix(apiFormat: string): '/v1' | '/v1beta' {
|
||||
function skipsVersionedApiRootDefault(apiFormat: string, baseUrl: string): boolean {
|
||||
if (
|
||||
apiFormat === 'gemini:generate_content'
|
||||
|| apiFormat === 'gemini:interactions'
|
||||
|| apiFormat === 'gemini:embedding'
|
||||
|| apiFormat === 'gemini:video'
|
||||
) {
|
||||
|
||||
+11
@@ -86,6 +86,17 @@ describe('buildDefaultModelTestRequestBody', () => {
|
||||
expect(body.messages).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses Gemini Interactions payloads for interaction endpoints', () => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody('gemini-3.5-flash', 'gemini:interactions'))
|
||||
|
||||
expect(body).toEqual({
|
||||
model: 'gemini-3.5-flash',
|
||||
input: 'Hello! This is a test message.',
|
||||
stream: true,
|
||||
})
|
||||
expect(body.messages).toBeUndefined()
|
||||
})
|
||||
|
||||
it('uses image generation tools for image models on OpenAI Responses endpoints', () => {
|
||||
const body = JSON.parse(buildDefaultModelTestRequestBody(
|
||||
'gpt-image-2',
|
||||
|
||||
@@ -196,6 +196,14 @@ export function buildDefaultModelTestRequestBody(
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
if (normalizedApiFormat === 'gemini:interactions') {
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
input: DEFAULT_MODEL_TEST_MESSAGE,
|
||||
stream: true,
|
||||
}, null, 2)
|
||||
}
|
||||
|
||||
if (normalizedApiFormat === 'openai:responses' && modelSupportsImageGeneration(model)) {
|
||||
return JSON.stringify({
|
||||
model: modelName,
|
||||
|
||||
@@ -12,6 +12,7 @@ const ENDPOINT_SORT_ORDER = [
|
||||
'openai:embedding',
|
||||
'openai:rerank',
|
||||
'gemini:generate_content',
|
||||
'gemini:interactions',
|
||||
'gemini:embedding',
|
||||
'openai:video',
|
||||
'gemini:video',
|
||||
|
||||
@@ -32,6 +32,7 @@ export function useProviderFilters(
|
||||
{ value: 'openai:embedding', label: 'OpenAI Embedding' },
|
||||
{ value: 'openai:rerank', label: 'OpenAI Rerank' },
|
||||
{ value: 'gemini:generate_content', label: 'Gemini Generate Content' },
|
||||
{ value: 'gemini:interactions', label: 'Gemini Interactions' },
|
||||
{ value: 'gemini:embedding', label: 'Gemini Embedding' },
|
||||
{ value: 'jina:embedding', label: 'Jina Embedding' },
|
||||
{ value: 'jina:rerank', label: 'Jina Rerank' },
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
compareAntigravityQuotaItems,
|
||||
dedupeAntigravityQuotaItemsByLabel,
|
||||
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-3-flash-agent',
|
||||
'Gemini 3.5 Flash (High)',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.5 Flash (High)')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'gemini-3.5-flash-low',
|
||||
'Gemini 3.5 Flash (Medium)',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.5 Flash (Medium)')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'gemini-3.5-flash-extra-low',
|
||||
'Gemini 3.5 Flash (Low)',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.5 Flash (Low)')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'gemini-pro-agent',
|
||||
'gemini-pro-agent',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.1 Pro (High)')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'gemini-3.1-pro-high',
|
||||
'gemini-3.1-pro-high',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.1 Pro (High)')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'gemini-2.5-flash',
|
||||
'Gemini 3.1 Flash Lite',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Gemini 3.1 Flash Lite')
|
||||
expect(
|
||||
resolveAntigravityQuotaLabel(
|
||||
'claude-sonnet-4-6',
|
||||
'Claude Sonnet 4.6 (Thinking)',
|
||||
opaqueDisplayIndex,
|
||||
),
|
||||
).toBe('Claude Sonnet 4.6 (Thinking)')
|
||||
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-extra-low',
|
||||
label: 'Gemini 3.5 Flash (Low)',
|
||||
remainingPercent: 95,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'gemini-3.5-flash-low',
|
||||
label: 'Gemini 3.5 Flash (Medium)',
|
||||
remainingPercent: 90,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'gemini-3-flash-agent',
|
||||
label: 'Gemini 3.5 Flash (High)',
|
||||
remainingPercent: 80,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'gemini-pro-agent',
|
||||
label: 'Gemini 3.1 Pro (High)',
|
||||
remainingPercent: 70,
|
||||
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 3.1 Flash Lite',
|
||||
remainingPercent: 5,
|
||||
resetSeconds: 5,
|
||||
},
|
||||
].sort(compareAntigravityQuotaItems)
|
||||
|
||||
expect(items.map(item => item.model)).toEqual([
|
||||
'claude-opus-4-6-thinking',
|
||||
'gemini-pro-agent',
|
||||
'gemini-3-flash-agent',
|
||||
'gemini-3.5-flash-low',
|
||||
'gemini-3.5-flash-extra-low',
|
||||
'gemini-2.5-flash-lite',
|
||||
'tab_flash_lite_preview',
|
||||
'chat_20706',
|
||||
])
|
||||
})
|
||||
|
||||
it('deduplicates display labels while keeping the current preferred quota bucket', () => {
|
||||
const items = dedupeAntigravityQuotaItemsByLabel([
|
||||
{
|
||||
model: 'gemini-3.1-pro-high',
|
||||
label: 'Gemini 3.1 Pro (High)',
|
||||
remainingPercent: 95,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'gemini-pro-agent',
|
||||
label: 'Gemini 3.1 Pro (High)',
|
||||
remainingPercent: 40,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'claude-sonnet-4-6-thinking',
|
||||
label: 'Claude Sonnet 4.6 (Thinking)',
|
||||
remainingPercent: 95,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'claude-sonnet-4-6',
|
||||
label: 'Claude Sonnet 4.6 (Thinking)',
|
||||
remainingPercent: 40,
|
||||
resetSeconds: null,
|
||||
},
|
||||
{
|
||||
model: 'gemini-3-flash-agent',
|
||||
label: 'Gemini 3.5 Flash (High)',
|
||||
remainingPercent: 80,
|
||||
resetSeconds: null,
|
||||
},
|
||||
])
|
||||
|
||||
expect(items.map(item => item.model)).toEqual([
|
||||
'claude-sonnet-4-6',
|
||||
'gemini-pro-agent',
|
||||
'gemini-3-flash-agent',
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,117 @@
|
||||
export interface AntigravityQuotaSortableItem {
|
||||
model: string
|
||||
label: string
|
||||
remainingPercent: number
|
||||
resetSeconds: number | null
|
||||
}
|
||||
|
||||
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
|
||||
'gemini-pro-agent': 'Gemini 3.1 Pro (High)',
|
||||
'gemini-3.1-pro-high': 'Gemini 3.1 Pro (High)',
|
||||
'gemini-3.1-pro-low': 'Gemini 3.1 Pro (Low)',
|
||||
'gemini-3-flash-agent': 'Gemini 3.5 Flash (High)',
|
||||
'gemini-3.5-flash-low': 'Gemini 3.5 Flash (Medium)',
|
||||
'gemini-3.5-flash-extra-low': 'Gemini 3.5 Flash (Low)',
|
||||
'claude-opus-4-6-thinking': 'Claude Opus 4.6 (Thinking)',
|
||||
'claude-sonnet-4-6': 'Claude Sonnet 4.6 (Thinking)',
|
||||
'claude-sonnet-4-6-thinking': 'Claude Sonnet 4.6 (Thinking)',
|
||||
'gemini-3.1-flash-image': 'Gemini 3.1 Flash Image',
|
||||
'gemini-3.1-flash-lite': 'Gemini 3.1 Flash Lite',
|
||||
'gemini-3-flash': 'Gemini 3 Flash',
|
||||
'gemini-2.5-pro': 'Gemini 2.5 Pro',
|
||||
'gemini-2.5-flash-thinking': 'Gemini 3.1 Flash Lite',
|
||||
'gemini-2.5-flash': 'Gemini 3.1 Flash Lite',
|
||||
'gemini-2.5-flash-lite': 'Gemini 3.1 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,
|
||||
'claude-sonnet-4-6-thinking': 25,
|
||||
'gemini-pro-agent': 30,
|
||||
'gemini-3.1-pro-high': 35,
|
||||
'gemini-3.1-pro-low': 40,
|
||||
'gemini-3-flash-agent': 50,
|
||||
'gemini-3.5-flash-low': 60,
|
||||
'gemini-3.5-flash-extra-low': 70,
|
||||
'gemini-3.1-flash-image': 80,
|
||||
'gemini-3.1-flash-lite': 90,
|
||||
'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 30
|
||||
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)
|
||||
}
|
||||
|
||||
export function dedupeAntigravityQuotaItemsByLabel<T extends AntigravityQuotaSortableItem>(
|
||||
items: T[],
|
||||
): T[] {
|
||||
const selectedByLabel = new Map<string, T>()
|
||||
for (const item of items) {
|
||||
const label = item.label.trim()
|
||||
if (!label) continue
|
||||
const selected = selectedByLabel.get(label)
|
||||
if (!selected || compareAntigravityQuotaItems(item, selected) < 0) {
|
||||
selectedByLabel.set(label, item)
|
||||
}
|
||||
}
|
||||
return Array.from(selectedByLabel.values()).sort(compareAntigravityQuotaItems)
|
||||
}
|
||||
Reference in New Issue
Block a user