From 7f2694ccf8f695562e9018179ff2923ce21a5921 Mon Sep 17 00:00:00 2001 From: baozheng Date: Thu, 8 Oct 2026 02:41:15 +0800 Subject: [PATCH 1/3] feat(antigravity): preserve model quota evidence --- .../content/docs/reference/management-api.md | 4 + gui/src/codex-quota-utils.ts | 2 +- gui/src/provider-workspace/report.ts | 1 + gui/tests/provider-quota-model-id.test.ts | 18 +++ scripts/test-layout/layout.json | 1 + src/oauth/account-quota-rank.ts | 4 +- src/providers/quota-types.ts | 2 + src/providers/quota.ts | 11 +- src/providers/quota/antigravity.ts | 29 +++- structure/providers-and-adapters.md | 2 + tests/fixtures/test-layout-expected.json | 1 + .../providers/provider-account-quota.test.ts | 36 ++++- .../provider-antigravity-model-quota.test.ts | 131 ++++++++++++++++++ tests/providers/provider-quota.test.ts | 18 ++- 14 files changed, 239 insertions(+), 21 deletions(-) create mode 100644 gui/tests/provider-quota-model-id.test.ts create mode 100644 tests/providers/provider-antigravity-model-quota.test.ts diff --git a/docs-site/src/content/docs/reference/management-api.md b/docs-site/src/content/docs/reference/management-api.md index 3bd3700766c..60fc812c85b 100644 --- a/docs-site/src/content/docs/reference/management-api.md +++ b/docs-site/src/content/docs/reference/management-api.md @@ -626,6 +626,10 @@ setting interacts with `claudeCode.enabled` across upgrades and downgrades. | `GET, PUT /api/provider-context-caps` | Read or update global, all-provider, or one-provider context caps | 400 invalid request; 404 unknown provider | | `GET /api/provider-presets` | Return GUI provider presets derived from the runtime registry | — | +When Antigravity's quota-summary endpoint has no usable response, quota reports may also include +per-model Cloud Code Assist usage windows when the model-discovery response provides them. These +measurements are diagnostic; an absent model window does not establish that the model is unavailable. + The provider context-cap response includes `caps` (active limits) and `values` (last selected values, retained while disabled). Enabling a provider without `value` restores its selection, or uses the global `contextCapValue` on first enable. This also applies to OpenAI: the switch diff --git a/gui/src/codex-quota-utils.ts b/gui/src/codex-quota-utils.ts index 57feab153f0..a19247d8580 100644 --- a/gui/src/codex-quota-utils.ts +++ b/gui/src/codex-quota-utils.ts @@ -13,7 +13,7 @@ export interface AccountQuota { shortObservedAt?: number; shortWindowSeconds?: number; monthlyResetAt?: number; - customWindows?: { label: string; percent: number; resetAt?: number }[]; + customWindows?: { label: string; percent: number; resetAt?: number; modelId?: string }[]; creditsUsd?: { used: number; limit: number; diff --git a/gui/src/provider-workspace/report.ts b/gui/src/provider-workspace/report.ts index bd79ac584e7..e97f90ba79b 100644 --- a/gui/src/provider-workspace/report.ts +++ b/gui/src/provider-workspace/report.ts @@ -148,6 +148,7 @@ function quotaFromUnknown(quota: unknown, fallbackUpdatedAt?: number): AccountQu label: row.label, percent: row.percent as number, ...(finite(row.resetAt) !== undefined ? { resetAt: row.resetAt as number } : {}), + ...(typeof row.modelId === "string" && row.modelId.length <= 128 ? { modelId: row.modelId } : {}), }]; }) : []; diff --git a/gui/tests/provider-quota-model-id.test.ts b/gui/tests/provider-quota-model-id.test.ts new file mode 100644 index 00000000000..e4ca5cd0244 --- /dev/null +++ b/gui/tests/provider-quota-model-id.test.ts @@ -0,0 +1,18 @@ +import { expect, test } from "bun:test"; +import { accountQuotaFromReport } from "../src/provider-workspace/report"; + +test("provider quota projection preserves bounded exact Antigravity model IDs", () => { + expect(accountQuotaFromReport({ + updatedAt: 123, + quota: { + updatedAt: 123, + customWindows: [ + { label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 }, + { label: "legacy", modelId: "x".repeat(129), percent: 12 }, + ], + }, + })?.customWindows).toEqual([ + { label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 }, + { label: "legacy", percent: 12 }, + ]); +}); diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json index 05cae70aa51..f9f5e51ab2d 100644 --- a/scripts/test-layout/layout.json +++ b/scripts/test-layout/layout.json @@ -31,6 +31,7 @@ "gui-pair-delivery.test.ts": "gui", "usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers", + "provider-antigravity-model-quota.test.ts": "providers", "antigravity-discovered-families.test.ts": "adapters/google", "provider-antigravity-effort-families.test.ts": "providers", "provider-antigravity-family-catalog.test.ts": "providers", diff --git a/src/oauth/account-quota-rank.ts b/src/oauth/account-quota-rank.ts index d3eb0c68845..4bad6e8cb63 100644 --- a/src/oauth/account-quota-rank.ts +++ b/src/oauth/account-quota-rank.ts @@ -96,7 +96,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri const family = classifyModelFamilyForQuota(provider, requestedModelId); if (family) { const percents = (quota.customWindows ?? []) - .filter(window => windowMatchesFamily(window.label, family)) + .filter(window => window.modelId === undefined && windowMatchesFamily(window.label, family)) .map(window => window.percent) .filter((value): value is number => typeof value === "number"); if (percents.length === 0) return null; @@ -106,7 +106,7 @@ function headroomOf(provider: string, accountId: string, requestedModelId?: stri quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, - ...(quota.customWindows ?? []).map(window => window.percent), + ...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent), ].filter((value): value is number => typeof value === "number"); if (percents.length === 0) return null; return 100 - Math.max(...percents); diff --git a/src/providers/quota-types.ts b/src/providers/quota-types.ts index 508e964e281..4f1d319c5a3 100644 --- a/src/providers/quota-types.ts +++ b/src/providers/quota-types.ts @@ -19,6 +19,8 @@ export interface ProviderQuotaWindow { label: string; percent: number; resetAt?: number; + /** Exact CCA wire model measured by this window; display evidence only, not eligibility. */ + modelId?: string; /** * Set only when the PRODUCER proved this window covers one model family, structurally rather * than by reading its label: an Anthropic `seven_day_` body key, or a limit with diff --git a/src/providers/quota.ts b/src/providers/quota.ts index df45b0e494b..62603cb5088 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -424,7 +424,8 @@ async function fetchAccountQuota( const kiroIdentity = provider === "kiro" ? kiroProbeIdentity(accountId) : undefined; const cachedCandidate = accountQuotaCache.get(key); const cached = (provider !== "kiro" || cachedCandidate?.identity === kiroIdentity) - && (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined; + && (provider !== "anthropic" || cachedCandidate?.isCurrent?.() !== false) + && (provider !== "google-antigravity" || cachedCandidate?.isCurrent?.() !== false) ? cachedCandidate : undefined; if (!forceRefresh && cached && Date.now() - cached.ts < ACCOUNT_QUOTA_TTL_MS) { if (provider === "google-antigravity" && cached.quotaFailure && cached.quotaFailureIsCurrent?.() !== true) return { ...cached, quotaFailure: undefined }; return provider === "anthropic" ? { ...cached, quota: normalizeAnthropicQuota(cached.quota, Date.now()) } : cached; @@ -447,6 +448,7 @@ async function fetchAccountQuota( try { return epoch === explicitAccountEpoch && diagnosticIdentity !== undefined && diagnosticIdentity === antigravityQuotaDiagnosticIdentity(accountId); } catch { return false; } }; + const antigravityIsCurrent = () => quotaFailureIsCurrent(); const diagnosticFields = () => quotaFailure && quotaFailureIsCurrent() ? { quotaFailure, quotaFailureIsCurrent } : {}; const unavailable = (): AccountQuotaCacheEntry => { if (!flightCurrent()) return { ts: Date.now(), quota: null, unavailable: true }; @@ -458,9 +460,11 @@ async function fetchAccountQuota( unavailable: true, ...(provider === "anthropic" && retained?.isCurrent ? { isCurrent: retained.isCurrent } : {}), ...(provider === "kiro" ? { identity: kiroIdentity } : {}), + ...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}), ...diagnosticFields(), }; - if (mayCommitAccountQuotaKey(key, writerGeneration) && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) { + if (entry.isCurrent?.() !== false && mayCommitAccountQuotaKey(key, writerGeneration) + && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) { accountQuotaCache.set(key, entry); if (provider === "kiro") persistKiroAccountState(); sweepExpiredOnWrite(entry.ts); @@ -499,13 +503,16 @@ async function fetchAccountQuota( return { ts: Date.now(), quota: null, unavailable: true }; } } + if (provider === "google-antigravity" && !antigravityIsCurrent()) return { ts: Date.now(), quota: null, unavailable: true }; if (!quota) return unavailable(); const entry: AccountQuotaCacheEntry = { ts: Date.now(), quota: provider === "anthropic" ? normalizeAnthropicQuota(quota, Date.now()) : quota, ...(provider === "kiro" ? { identity: kiroIdentity } : {}), + ...(provider === "google-antigravity" ? { isCurrent: antigravityIsCurrent } : {}), ...(anthropicCurrent ? { isCurrent: anthropicCurrent } : {}), }; if (mayCommitAccountQuotaKey(key, writerGeneration) + && entry.isCurrent?.() !== false && (provider !== "kiro" || kiroProbeCurrent(accountId, kiroIdentity))) { accountQuotaCache.set(key, entry); if (provider === "kiro") { commitKiroAccountUsageState(key, kiroSnapshot, kiroIdentity); persistKiroAccountState(); } diff --git a/src/providers/quota/antigravity.ts b/src/providers/quota/antigravity.ts index 8e8c2428d0a..1d1f1ccd062 100644 --- a/src/providers/quota/antigravity.ts +++ b/src/providers/quota/antigravity.ts @@ -71,19 +71,34 @@ function antigravityWindowsFromModels(body: Record | null): Pro if (!models) return []; const windows = new Map(); + const modelWindows: ProviderQuotaWindow[] = []; for (const [modelId, rawModelInfo] of Object.entries(models)) { + if (!modelId.trim() || modelId.length > 128) continue; const modelInfo = asRecord(rawModelInfo); if (!modelInfo) continue; for (const quotaInfo of quotaInfoEntries(modelInfo)) { const label = classifyAntigravityFamily(modelId, modelInfo, quotaInfo); - if (!label || windows.has(label)) continue; const percent = antigravityUsedPercent(quotaInfo); if (percent === undefined) continue; - windows.set(label, { - label, - percent, - ...(normalizeResetAt(quotaInfo.resetTime) !== undefined ? { resetAt: normalizeResetAt(quotaInfo.resetTime) } : {}), - }); + const resetAt = normalizeResetAt(quotaInfo.resetTime); + if (label && !windows.has(label)) { + windows.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) }); + } + // Keep model measurements as display/diagnostic evidence. They are deliberately + // separate from the family summaries above; routing must not infer eligibility + // from one model's counter or from a missing row. + if (modelWindows.length < 256) { + const tier = typeof quotaInfo.tier === "string" && quotaInfo.tier.length <= 64 + ? quotaInfo.tier.trim().slice(0, Math.max(0, 125 - modelId.length)) + : ""; + const modelLabel = `${modelId}${tier ? ` · ${tier}` : ""}`; + modelWindows.push({ + label: modelLabel, + modelId, + percent, + ...(resetAt !== undefined ? { resetAt } : {}), + }); + } } } @@ -91,7 +106,7 @@ function antigravityWindowsFromModels(body: Record | null): Pro const window = windows.get(label); return window ? [window] : []; }); - return customWindows; + return [...customWindows, ...modelWindows]; } /** diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index dbe780a15b4..50851e4a014 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -579,6 +579,8 @@ Antigravity catalog rows carry the observed complete effort map from `src/provid Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged. +When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries. Model windows carry the exact CCA wire ID for display and diagnostics; account quota ranking ignores these rows and continues to use family summaries. A missing model row or failed probe is unknown evidence, not a model eligibility verdict. Per-account cache entries are bound to the current credential and project identity. + ## Combo model refusal evidence diff --git a/tests/fixtures/test-layout-expected.json b/tests/fixtures/test-layout-expected.json index b41a1beb973..91ea7944b2e 100644 --- a/tests/fixtures/test-layout-expected.json +++ b/tests/fixtures/test-layout-expected.json @@ -47,6 +47,7 @@ "ollama-native-tool-continuations.test.ts": "providers/ollama", "gui-pair-delivery.test.ts": "gui", "usage-antigravity-55.test.ts": "usage", "provider-antigravity-wire-snapshot.test.ts": "providers", + "provider-antigravity-model-quota.test.ts": "providers", "antigravity-discovered-families.test.ts": "adapters/google", "provider-antigravity-effort-families.test.ts": "providers", "provider-antigravity-family-catalog.test.ts": "providers", diff --git a/tests/providers/provider-account-quota.test.ts b/tests/providers/provider-account-quota.test.ts index 789bdcc6257..7760bb6a310 100644 --- a/tests/providers/provider-account-quota.test.ts +++ b/tests/providers/provider-account-quota.test.ts @@ -735,7 +735,7 @@ describe("google-antigravity per-account quota (#1082)", () => { const byId = Object.fromEntries(rows.map(row => [row.accountId, row])); const [idA, idB] = [idFor("a@example.com"), idFor("b@example.com")]; expect(Object.keys(byId).sort()).toEqual([idA, idB].sort()); - const windows = (id: string) => byId[id]!.quota!.customWindows!.map(w => `${w.label}=${w.percent}`); + const windows = (id: string) => byId[id]!.quota!.customWindows!.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`); expect(windows(idA)).toEqual(["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]); expect(windows(idB)).toEqual(["Gem=3", "Gem (Weekly)=3", "Cla=9", "Cla (Weekly)=9"]); expect(byId[idA]!.quota!.customWindows![0]!.resetAt).toBeDefined(); @@ -769,7 +769,7 @@ describe("google-antigravity per-account quota (#1082)", () => { const rows = await fetchProviderAccountQuotas("google-antigravity"); const byId = Object.fromEntries(rows.map(row => [row.accountId, row])); const [idA, idB] = [idFor("a@example.com"), idFor("b@example.com")]; - const windows = (id: string) => byId[id]!.quota!.customWindows!.map(w => `${w.label}=${w.percent}`); + const windows = (id: string) => byId[id]!.quota!.customWindows!.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`); expect(windows(idA)).toEqual(["Gem=14", "Cla=62"]); expect(windows(idB)).toEqual(["Gem=3", "Cla=9"]); expect(byId[idA]!.quota!.customWindows![0]!.resetAt).toBeDefined(); @@ -814,8 +814,8 @@ describe("google-antigravity per-account quota (#1082)", () => { expect(posted.filter(row => row.auth === auth)).toEqual(urls.map(url => ({ url, auth, project, address: "198.18.56.214", tls: true, signal: true }))); } const byId = Object.fromEntries(rows.map(row => [row.accountId, row])); - expect(byId[idFor("a@example.com")]?.quota?.customWindows?.map(w => w.percent)).toEqual(fallback ? [14, 62] : [14, 14, 62, 62]); - expect(byId[idFor("b@example.com")]?.quota?.customWindows?.map(w => w.percent)).toEqual(fallback ? [3, 9] : [3, 3, 9, 9]); + expect(byId[idFor("a@example.com")]?.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => w.percent)).toEqual(fallback ? [14, 62] : [14, 14, 62, 62]); + expect(byId[idFor("b@example.com")]?.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => w.percent)).toEqual(fallback ? [3, 9] : [3, 3, 9, 9]); expect(plainFetchCalls).toBe(0); }); } @@ -893,7 +893,31 @@ describe("google-antigravity per-account quota (#1082)", () => { expect(cached.quotaFailure).toBe("access_denied"); await saveCredential("google-antigravity", { ...credential, access: "agy-third" }); expect(cached.quotaFailureIsCurrent?.()).toBe(false); - expect((await fetchProviderAccountQuotas("google-antigravity"))[0]).not.toHaveProperty("quotaFailure"); + expect((await fetchProviderAccountQuotas("google-antigravity"))[0]).toHaveProperty("quotaFailure", "access_denied"); + }); + + test("same-id credential and project replacement invalidates cached model quota measurements", async () => { + const credential = { access: "agy-model-first", refresh: "r1", expires: Date.now() + 3600_000, projectId: "proj-model-first", accountId: "agy-model", email: "model@example.com" }; + await saveCredential("google-antigravity", credential); + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }), + pinnedPost: async url => url.endsWith("retrieveUserQuotaSummary") + ? new Response(null, { status: 404 }) + : new Response(JSON.stringify({ models: { + "gemini-3.8-flash-high": { quotaInfo: { remainingFraction: 0.76 } }, + } }), { status: 200, headers: { "content-type": "application/json" } }), + }); + + const [row] = await fetchProviderAccountQuotas("google-antigravity", true); + const accountId = idFor("model@example.com"); + expect(row?.quota?.customWindows).toContainEqual({ + label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24, + }); + expect(getCachedProviderAccountQuota("google-antigravity", accountId)?.customWindows) + .toContainEqual({ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 }); + + await saveCredential("google-antigravity", { ...credential, access: "agy-model-second", projectId: "proj-model-second" }); + expect(getCachedProviderAccountQuota("google-antigravity", accountId)).toBeNull(); }); test("NO_PROXY denial preserves an unavailable account row without sending its bearer", async () => { @@ -1065,7 +1089,7 @@ describe("google-antigravity Fake-IP TUN quota probes without HTTP proxy (#3781) expect(rows[0]!.accountId).toBe(idFor("a@example.com")); expect(rows[0]!.unavailable).toBeUndefined(); expect(rows[0]!.quotaFailure).toBeUndefined(); - expect(rows[0]!.quota?.customWindows?.map(w => `${w.label}=${w.percent}`)).toEqual(fallback ? ["Gem=14", "Cla=62"] : ["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]); + expect(rows[0]!.quota?.customWindows?.filter(w => w.modelId === undefined).map(w => `${w.label}=${w.percent}`)).toEqual(fallback ? ["Gem=14", "Cla=62"] : ["Gem=14", "Gem (Weekly)=14", "Cla=62", "Cla (Weekly)=62"]); expect(plainFetchCalls).toBe(0); }); } diff --git a/tests/providers/provider-antigravity-model-quota.test.ts b/tests/providers/provider-antigravity-model-quota.test.ts new file mode 100644 index 00000000000..939d70947f9 --- /dev/null +++ b/tests/providers/provider-antigravity-model-quota.test.ts @@ -0,0 +1,131 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; +import { PROXY_ENV_KEYS } from "../../src/lib/proxy-env"; +import { rankAccountsByHeadroom } from "../../src/oauth/account-quota-rank"; +import { getCachedProviderAccountQuota, setCachedProviderAccountQuotaForTests } from "../../src/providers/quota"; +import { sweepExpiredProviderAccountQuotaRows } from "../../src/providers/quota/account-cache"; +import { ACCOUNT_QUOTA_TTL_MS } from "../../src/providers/quota-wire"; +import { + probeAntigravityUsageQuota, + setAntigravityAccountQuotaTransportForTests, +} from "../../src/providers/quota/antigravity"; + +const proxyEnv = PROXY_ENV_KEYS.flatMap(key => [key, key.toLowerCase()]); +const originalProxyEnv = Object.fromEntries(proxyEnv.map(key => [key, process.env[key]])); + +beforeEach(() => { + for (const key of proxyEnv) delete process.env[key]; +}); + +afterEach(() => { + setAntigravityAccountQuotaTransportForTests(null); + setCachedProviderAccountQuotaForTests("google-antigravity", "first", null); + setCachedProviderAccountQuotaForTests("google-antigravity", "second", null); + for (const key of proxyEnv) { + if (originalProxyEnv[key] === undefined) delete process.env[key]; + else process.env[key] = originalProxyEnv[key]; + } +}); + +function setModelsResponse(models: unknown): void { + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async () => ({ + hostname: "daily-cloudcode-pa.googleapis.com", + addresses: [{ address: "142.250.0.1", family: 4 }], + privateNetwork: false, + }), + pinnedPost: async url => url.endsWith("retrieveUserQuotaSummary") + ? new Response(null, { status: 404 }) + : new Response(JSON.stringify({ models }), { status: 200, headers: { "content-type": "application/json" } }), + }); +} + +describe("Antigravity model quota evidence", () => { + test("keeps bounded exact wire-model readings alongside unchanged first-seen family bars", async () => { + setModelsResponse({ + "gemini-3.8-flash-high": { + displayName: "Gemini Flash High", + quotaInfo: { remainingFraction: 0.64, resetTime: "2026-11-01T12:00:00Z" }, + }, + "claude-sonnet-4-6": { + quotaInfoByTier: { sonnet: { remainingFraction: 0.21, resetTime: "2026-11-02T12:00:00Z" } }, + }, + autocomplete: { quotaInfo: { remainingFraction: 0.01 } }, + malformed: { quotaInfo: { remainingFraction: "unknown" } }, + ["x".repeat(129)]: { quotaInfo: { remainingFraction: 0 } }, + }); + + const result = await probeAntigravityUsageQuota("synthetic-token", "synthetic-project"); + expect(result.kind).toBe("available"); + if (result.kind !== "available") return; + + expect(result.quota.customWindows?.slice(0, 2)).toEqual([ + { label: "Gem", percent: 36, resetAt: Date.parse("2026-11-01T12:00:00Z") }, + { label: "Cla", percent: 79, resetAt: Date.parse("2026-11-02T12:00:00Z") }, + ]); + expect(result.quota.customWindows).toContainEqual({ + label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 36, + resetAt: Date.parse("2026-11-01T12:00:00Z"), + }); + expect(result.quota.customWindows).toContainEqual({ + label: "claude-sonnet-4-6 · sonnet", modelId: "claude-sonnet-4-6", percent: 79, + resetAt: Date.parse("2026-11-02T12:00:00Z"), + }); + expect(result.quota.customWindows).toContainEqual({ + label: "autocomplete", modelId: "autocomplete", percent: 99, + }); + expect(result.quota.customWindows?.some(window => window.modelId === "malformed" || window.label.length > 128)).toBe(false); + }); + + test("a missing model listing remains unavailable rather than producing zero-usage evidence", async () => { + setModelsResponse(null); + expect(await probeAntigravityUsageQuota("synthetic-token", "synthetic-project")) + .toMatchObject({ kind: "unavailable", failure: "response_unusable" }); + }); + + test("a summary 403 remains an account-probe failure and does not fall through to model discovery", async () => { + let modelRequests = 0; + setAntigravityAccountQuotaTransportForTests({ + resolveAddresses: async () => ({ + hostname: "daily-cloudcode-pa.googleapis.com", + addresses: [{ address: "142.250.0.1", family: 4 }], + privateNetwork: false, + }), + pinnedPost: async url => { + if (url.endsWith("fetchAvailableModels")) modelRequests += 1; + return new Response(null, { status: 403 }); + }, + }); + expect(await probeAntigravityUsageQuota("synthetic-token", "synthetic-project")) + .toMatchObject({ kind: "unavailable", failure: "access_denied" }); + expect(modelRequests).toBe(0); + }); + + test("model-specific rows do not alter family quota account ranking", () => { + setCachedProviderAccountQuotaForTests("google-antigravity", "first", { + updatedAt: Date.now(), + customWindows: [ + { label: "Gem", percent: 20 }, + { label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 100 }, + ], + }); + setCachedProviderAccountQuotaForTests("google-antigravity", "second", { + updatedAt: Date.now(), + customWindows: [ + { label: "Gem", percent: 40 }, + { label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 0 }, + ], + }); + expect(rankAccountsByHeadroom("google-antigravity", ["first", "second"], "gemini-3.8-flash-high")) + .toEqual(["first", "second"]); + }); + + test("model quota cache rows expire at the existing account quota TTL", () => { + setCachedProviderAccountQuotaForTests("google-antigravity", "first", { + updatedAt: Date.now(), + customWindows: [{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 24 }], + }); + expect(sweepExpiredProviderAccountQuotaRows(Date.now() + ACCOUNT_QUOTA_TTL_MS)).toBeGreaterThan(0); + expect(getCachedProviderAccountQuota("google-antigravity", "first")).toBeNull(); + expect(sweepExpiredProviderAccountQuotaRows(Date.now() + ACCOUNT_QUOTA_TTL_MS)).toBe(0); + }); +}); diff --git a/tests/providers/provider-quota.test.ts b/tests/providers/provider-quota.test.ts index d4f1afa8944..3a5b214a6fd 100644 --- a/tests/providers/provider-quota.test.ts +++ b/tests/providers/provider-quota.test.ts @@ -392,6 +392,9 @@ describe("fetchProviderQuotaReports", () => { expect(byProvider["google-antigravity"]?.quota.customWindows).toEqual([ { label: "Gem", percent: 36, resetAt: Date.parse("2026-07-05T14:00:00Z") }, { label: "Cla", percent: 79, resetAt: Date.parse("2026-07-05T15:00:00Z") }, + { label: "gemini-3.6-flash-medium", modelId: "gemini-3.6-flash-medium", percent: 36, resetAt: Date.parse("2026-07-05T14:00:00Z") }, + { label: "claude-sonnet-4.6 · sonnet", modelId: "claude-sonnet-4.6", percent: 79, resetAt: Date.parse("2026-07-05T15:00:00Z") }, + { label: "autocomplete", modelId: "autocomplete", percent: 99, resetAt: Date.parse("2026-07-05T16:00:00Z") }, ]); expect(byProvider.cursor?.source).toBe("cursor:period-usage"); expect(byProvider.cursor?.reverseEngineered).toBe(true); @@ -3474,7 +3477,10 @@ describe("fetchProviderQuotaReports", () => { expect(resolved).toEqual(urls.map(url => ({ url, benchmark: true, private: false, mihomo: true }))); expect(posted).toEqual(urls.map(url => ({ url, address: "198.18.56.214", tls: true, auth: "Bearer agy-canonical-access", body: JSON.stringify({ project: "agy-canonical-project" }), signal: true }))); expect(result.reports[0]?.source).toBe(fallback ? "google-antigravity:fetchAvailableModels" : "google-antigravity:retrieveUserQuotaSummary"); - expect(result.reports[0]?.quota.customWindows).toEqual([{ label: "Gem", percent: fallback ? 25 : 40 }]); + expect(result.reports[0]?.quota.customWindows).toEqual([ + { label: "Gem", percent: fallback ? 25 : 40 }, + ...(fallback ? [{ label: "gemini", modelId: "gemini", percent: 25 }] : []), + ]); expect(plainFetchCalls).toEqual([]); }); } @@ -3494,7 +3500,10 @@ describe("fetchProviderQuotaReports", () => { }, }); const result = await fetchProviderQuotaReports(config(baseUrl), true); - expect(result.reports[0]?.quota.customWindows).toEqual([{ label: "Gem", percent: 25 }]); + expect(result.reports[0]?.quota.customWindows).toEqual([ + { label: "Gem", percent: 25 }, + { label: "gemini", modelId: "gemini", percent: 25 }, + ]); expect(resolved).toEqual([{ url: summaryUrl, private: false }, { url: modelsUrl, private: false }]); expect(posted).toEqual([summaryUrl, modelsUrl]); expect(plainFetchCalls).toEqual([]); @@ -3548,7 +3557,10 @@ describe("fetchProviderQuotaReports", () => { }, }); const result = await fetchProviderQuotaReports(config(), true); - expect(result.reports[0]?.quota.customWindows).toEqual([{ label: "Gem", percent: 25 }]); + expect(result.reports[0]?.quota.customWindows).toEqual([ + { label: "Gem", percent: 25 }, + { label: "gemini", modelId: "gemini", percent: 25 }, + ]); expect(posted).toEqual([summaryUrl, modelsUrl]); expect(plainFetchCalls).toEqual([]); }); From 63a8c6086fe5c7e17dee9f437ce08b905ca7431c Mon Sep 17 00:00:00 2001 From: baozheng Date: Thu, 8 Oct 2026 02:58:14 +0800 Subject: [PATCH 2/3] fix(antigravity): keep model quota diagnostics out of routing --- gui/src/components/QuotaBars.tsx | 1 + gui/tests/provider-quota-model-id.test.ts | 11 +++++++++++ src/cli/account-api.ts | 1 + src/combos/resolve.ts | 3 +++ src/server/management/provider-routes.ts | 4 ++-- structure/providers-and-adapters.md | 2 +- .../provider-antigravity-model-quota.test.ts | 8 ++++++++ tests/server/management-provider-validation.test.ts | 11 +++++++++++ 8 files changed, 38 insertions(+), 3 deletions(-) diff --git a/gui/src/components/QuotaBars.tsx b/gui/src/components/QuotaBars.tsx index fc72b0bb5f3..e9e29daf4bd 100644 --- a/gui/src/components/QuotaBars.tsx +++ b/gui/src/components/QuotaBars.tsx @@ -147,6 +147,7 @@ export function maxQuotaUtilisation(quota: AccountQuota | null): number { const vals = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent] .filter((n): n is number => typeof n === "number"); for (const w of quota.customWindows ?? []) { + if (w.modelId !== undefined) continue; if (typeof w.percent === "number") vals.push(w.percent); } const hasSubscriptionCreditsCustom = quota.customWindows?.some( diff --git a/gui/tests/provider-quota-model-id.test.ts b/gui/tests/provider-quota-model-id.test.ts index e4ca5cd0244..19584b6dfbe 100644 --- a/gui/tests/provider-quota-model-id.test.ts +++ b/gui/tests/provider-quota-model-id.test.ts @@ -1,5 +1,6 @@ import { expect, test } from "bun:test"; import { accountQuotaFromReport } from "../src/provider-workspace/report"; +import { maxQuotaUtilisation } from "../src/components/QuotaBars"; test("provider quota projection preserves bounded exact Antigravity model IDs", () => { expect(accountQuotaFromReport({ @@ -16,3 +17,13 @@ test("provider quota projection preserves bounded exact Antigravity model IDs", { label: "legacy", percent: 12 }, ]); }); + +test("model diagnostic bars do not change provider urgency sorting", () => { + expect(maxQuotaUtilisation({ + updatedAt: 123, + customWindows: [ + { label: "Gem", percent: 36 }, + { label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 100 }, + ], + })).toBe(36); +}); diff --git a/src/cli/account-api.ts b/src/cli/account-api.ts index b29cd6a08c4..7751d2dbdb1 100644 --- a/src/cli/account-api.ts +++ b/src/cli/account-api.ts @@ -239,6 +239,7 @@ export interface ProviderQuotaWindowDto { label: string; percent: number; resetAt?: number; + modelId?: string; } export interface ProviderQuotaDto extends CodexQuotaDto { diff --git a/src/combos/resolve.ts b/src/combos/resolve.ts index 59559154706..a1c07fc1ab7 100644 --- a/src/combos/resolve.ts +++ b/src/combos/resolve.ts @@ -100,6 +100,9 @@ function quotaWindowExhausted(percent: number | undefined, resetAt: number | und const MODEL_FAMILY_WINDOW_LABELS = new Set(["fable", "opus", "sonnet"]); function customWindowAppliesToModel(window: ProviderQuotaWindow, model: string | undefined): boolean { + // Exact Antigravity model readings are diagnostic only. They are not permission or + // provider-wide exhaustion evidence until a separate eligibility contract consumes them. + if (window.modelId !== undefined) return false; if (window.scope !== "model" || model === undefined) return true; const family = window.label.trim().toLowerCase(); if (!MODEL_FAMILY_WINDOW_LABELS.has(family)) return true; diff --git a/src/server/management/provider-routes.ts b/src/server/management/provider-routes.ts index 93d9641da8a..9395f209f73 100644 --- a/src/server/management/provider-routes.ts +++ b/src/server/management/provider-routes.ts @@ -872,7 +872,7 @@ function providerRoutingQuota(config: OcxConfig, name: string, now: number): Pro // Removing search/MCP windows may leave only a timestamp. That is not inference evidence. const percentages = [quota.fiveHourPercent, quota.weeklyPercent, quota.monthlyPercent, - ...(quota.customWindows ?? []).map(window => window.percent)]; + ...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.percent)]; const hasPercentage = percentages.some(value => typeof value === "number" && Number.isFinite(value) && value >= 0); const credits = quota.creditsUsd; const hasCredits = credits !== undefined && Number.isFinite(credits.percent) @@ -883,7 +883,7 @@ function providerRoutingQuota(config: OcxConfig, name: string, now: number): Pro let validUntil = quota.updatedAt + PROVIDER_QUOTA_MAX_AGE_MS; if (state === "exhausted") { const resets = [quota.fiveHourResetAt, quota.weeklyResetAt, quota.monthlyResetAt, - ...(quota.customWindows ?? []).map(window => window.resetAt)] + ...(quota.customWindows ?? []).filter(window => window.modelId === undefined).map(window => window.resetAt)] .filter((reset): reset is number => typeof reset === "number" && Number.isFinite(reset) && reset > now && reset < validUntil) .sort((left, right) => left - right); diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 50851e4a014..852afabdbc8 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -579,7 +579,7 @@ Antigravity catalog rows carry the observed complete effort map from `src/provid Known Claude Sonnet and Opus 5.5 base and low/medium/high IDs share a deterministic usage identity in `src/providers/antigravity-models.ts`, independently of live discovery. This accounting map does not migrate saved routing selections; Claude 4.6 and unknown suffix identities remain unchanged. -When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries. Model windows carry the exact CCA wire ID for display and diagnostics; account quota ranking ignores these rows and continues to use family summaries. A missing model row or failed probe is unknown evidence, not a model eligibility verdict. Per-account cache entries are bound to the current credential and project identity. +When the `retrieveUserQuotaSummary` endpoint has no usable result, `src/providers/quota/antigravity.ts` preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries. Model windows carry the exact CCA wire ID for display and diagnostics; account and provider routing ignore these rows and continue to use family summaries. A missing model row or failed probe is unknown evidence, not a model eligibility verdict. Per-account cache entries are bound to the current credential and project identity. ## Combo model refusal evidence diff --git a/tests/providers/provider-antigravity-model-quota.test.ts b/tests/providers/provider-antigravity-model-quota.test.ts index 939d70947f9..c4738f64716 100644 --- a/tests/providers/provider-antigravity-model-quota.test.ts +++ b/tests/providers/provider-antigravity-model-quota.test.ts @@ -1,6 +1,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { PROXY_ENV_KEYS } from "../../src/lib/proxy-env"; import { rankAccountsByHeadroom } from "../../src/oauth/account-quota-rank"; +import { cachedProviderQuotaIsExhausted } from "../../src/combos/resolve"; import { getCachedProviderAccountQuota, setCachedProviderAccountQuotaForTests } from "../../src/providers/quota"; import { sweepExpiredProviderAccountQuotaRows } from "../../src/providers/quota/account-cache"; import { ACCOUNT_QUOTA_TTL_MS } from "../../src/providers/quota-wire"; @@ -128,4 +129,11 @@ describe("Antigravity model quota evidence", () => { expect(getCachedProviderAccountQuota("google-antigravity", "first")).toBeNull(); expect(sweepExpiredProviderAccountQuotaRows(Date.now() + ACCOUNT_QUOTA_TTL_MS)).toBe(0); }); + + test("model-specific diagnostic rows never prove combo target exhaustion", () => { + expect(cachedProviderQuotaIsExhausted({ + updatedAt: Date.now(), + customWindows: [{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 100 }], + }, Date.now(), "gemini-3.8-flash-high")).toBe(false); + }); }); diff --git a/tests/server/management-provider-validation.test.ts b/tests/server/management-provider-validation.test.ts index 611121994db..27071937e9d 100644 --- a/tests/server/management-provider-validation.test.ts +++ b/tests/server/management-provider-validation.test.ts @@ -238,6 +238,17 @@ describe("provider quota routing state", () => { expect(dto.reports.find((row: { provider: string }) => row.provider === "zai").quota.monthlyPercent).toBe(100); }); + test("model-specific diagnostic windows do not produce provider-wide routing evidence", async () => { + const cfg = quotaConfig(); + globalThis.fetch = (async () => Response.json({ data: { limit: 20, limit_remaining: 8 } })) as typeof fetch; + await readQuota(cfg); + setCachedProviderQuotaForTests("openrouter", { + updatedAt: Date.now(), + customWindows: [{ label: "gemini-3.8-flash-high", modelId: "gemini-3.8-flash-high", percent: 100 }], + }); + expect((await readQuota(cfg)).reports[0]?.routingQuota).toEqual({ state: "unknown" }); + }); + test("an exhausted OAuth account report stays display-only", async () => { const cfg = quotaConfig("kimi", "https://api.kimi.com/coding/v1"); cfg.providers.kimi!.authMode = "oauth"; From 7b297ac0e4d3df190fc2fe890a0d7990200a218f Mon Sep 17 00:00:00 2001 From: baozheng Date: Thu, 8 Oct 2026 03:40:33 +0800 Subject: [PATCH 3/3] test(antigravity): expect model metadata after quota retry --- tests/providers/provider-antigravity-quota-retry.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/providers/provider-antigravity-quota-retry.test.ts b/tests/providers/provider-antigravity-quota-retry.test.ts index 6eea79bc3f9..97d4e28e964 100644 --- a/tests/providers/provider-antigravity-quota-retry.test.ts +++ b/tests/providers/provider-antigravity-quota-retry.test.ts @@ -80,7 +80,7 @@ describe("Antigravity quota summary 403 compatibility retry (#5940)", () => { for (const failure of [new Error("transport failed"), new Response(null, { status: 500 }), Response.json({})]) { test(`retry failure (${failure instanceof Error ? "transport" : failure.status}) recovers through IDE models probe`, async () => { const calls = transport([new Response(null, { status: 403 }), failure, Response.json(models)]); - expect(await probeAntigravityUsageQuota("test-access", "test-project")).toMatchObject({ kind: "available", source: "google-antigravity:fetchAvailableModels", quota: { customWindows: [{ label: "Gem", percent: 25 }] } }); + expect(await probeAntigravityUsageQuota("test-access", "test-project")).toMatchObject({ kind: "available", source: "google-antigravity:fetchAvailableModels", quota: { customWindows: [{ label: "Gem", percent: 25 }, { label: "gemini", modelId: "gemini", percent: 25 }] } }); expect(calls).toEqual([expectedCall(summaryUrl), expectedCall(summaryUrl, "antigravity/1.0"), expectedCall(modelsUrl)]); }); }