From 7d08bd2954c0bbc380b6d7358e3fee70bb4732c6 Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 18 Oct 2026 04:28:28 +0900 Subject: [PATCH 1/2] fix(antigravity): show the Gemini 4 Argon quota group separately retrieveUserQuotaSummary reports Gemini 4 Argon as its own group. The summary parser matched it as "gemini", so its weekly bucket collided with Gem (Weekly) and was dropped, or replaced the Gemini reading when the Argon group came first. The model-list fallback had the same order problem. Each bucket now decides its family from its own id and name (Argon before Gemini), with the group display name only as a fallback. Argon buckets become Argon / Argon (Weekly) windows tagged with the canonical pool model gemini-4-argon, which keeps them out of headroom, exhaustion, reset ordering and reset notifications like the existing model rows. The account reset-time selection now skips model-attributed rows as well. Closes #7272 --- .../docs/reference/configuration/providers.md | 1 + src/oauth/account-quota-rank.ts | 3 +- src/providers/quota-types.ts | 7 +- src/providers/quota/antigravity.ts | 45 ++++- structure/providers-and-adapters.md | 2 +- structure/transports/inventory.md | 2 + .../provider-antigravity-model-quota.test.ts | 172 +++++++++++++++++- 7 files changed, 222 insertions(+), 10 deletions(-) diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index a8e4499e9a6..c72a3048f3f 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -367,6 +367,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. | | — | — | Antigravity account quota probes (`retrieveUserQuota` and `retrieveUserQuotaSummary`) always go to Google's own Cloud Code host through the pinned outbound transport, regardless of a configured `baseUrl`; the account bearer is never sent to an operator-configured endpoint and a redirect aborts the probe. Only the model-list fallback still honors `baseUrl`. | | — | — | If Antigravity quota summary returns 403 for a valid OAuth account, OpenCodex retries that endpoint once with the legacy `antigravity/1.0` User-Agent and the same token and project. A 401 is not retried. Inference and model discovery retain the IDE User-Agent. | +| — | — | Antigravity quota shows Gemini 4 Argon as separate `Argon` / `Argon (Weekly)` windows next to Gem and Cla. They are display-only: account ranking, quota exhaustion, reset ordering and reset notifications keep using the Gemini and Claude windows, and Argon requests are still judged by the Gemini window. | | `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | | `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. | diff --git a/src/oauth/account-quota-rank.ts b/src/oauth/account-quota-rank.ts index d1b51a51f21..6582a29d3b0 100644 --- a/src/oauth/account-quota-rank.ts +++ b/src/oauth/account-quota-rank.ts @@ -153,7 +153,8 @@ export function accountResetTimestamp( const quota = getCachedProviderAccountQuota(provider, accountId); if (!quota) return null; const family = classifyModelFamilyForQuota(provider, requestedModelId); - const windows = (quota.customWindows ?? []).filter(w => !family || windowMatchesFamily(w.label, family)); + // Model-attributed rows are diagnostics; one must never become the account's reset time. + const windows = (quota.customWindows ?? []).filter(w => w.modelId === undefined && (!family || windowMatchesFamily(w.label, family))); const weekly = windows.find(w => /week/i.test(w.label)); let rawReset = weekly?.resetAt ?? quota.weeklyResetAt; if (rawReset === undefined && provider !== "google-antigravity" && provider !== "anthropic") { diff --git a/src/providers/quota-types.ts b/src/providers/quota-types.ts index 4f1d319c5a3..8306bc4754f 100644 --- a/src/providers/quota-types.ts +++ b/src/providers/quota-types.ts @@ -19,7 +19,12 @@ export interface ProviderQuotaWindow { label: string; percent: number; resetAt?: number; - /** Exact CCA wire model measured by this window; display evidence only, not eligibility. */ + /** + * Model this window is attributed to: the exact CCA wire model of a fallback measurement, or + * the canonical model of a quota-summary bucket dedicated to one model (Antigravity's Gemini 4 + * Argon pool). Display evidence only, never eligibility: routing, exhaustion, reset ordering + * and reset notifications ignore every window that carries it. + */ modelId?: string; /** * Set only when the PRODUCER proved this window covers one model family, structurally rather diff --git a/src/providers/quota/antigravity.ts b/src/providers/quota/antigravity.ts index fda26590baf..9ae43e9a676 100644 --- a/src/providers/quota/antigravity.ts +++ b/src/providers/quota/antigravity.ts @@ -45,8 +45,31 @@ function quotaInfoEntries(modelInfo: Record): Record): AntigravitySummaryFamily | undefined { + const identity = `${typeof bucket.bucketId === "string" ? bucket.bucketId : ""} ${typeof bucket.displayName === "string" ? bucket.displayName : ""}`; + if (ARGON_TOKEN.test(identity)) return "argon"; + if (GEMINI_TOKEN.test(identity)) return "gem"; + if (CLAUDE_FAMILY_TOKEN.test(identity)) return "cla"; + return undefined; +} + function classifyAntigravityFamily(modelId: string, modelInfo: Record, quotaInfo: Record): "Gem" | "Cla" | null { const displayName = typeof modelInfo.displayName === "string" ? modelInfo.displayName : ""; + // Argon models keep their exact model rows but never seed the shared Gem family bar. + if (ARGON_TOKEN.test(`${modelId} ${displayName}`)) return null; const tier = typeof quotaInfo.tier === "string" ? quotaInfo.tier : ""; const haystack = `${modelId} ${displayName} ${tier}`.toLowerCase(); if (haystack.includes("gemini")) return "Gem"; @@ -128,9 +151,13 @@ function parseAntigravityQuotaSummary(body: Record | null): Pro for (const rawGroup of groups) { const group = asRecord(rawGroup); if (!group) continue; - const groupName = `${typeof group.displayName === "string" ? group.displayName : ""} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase(); - const isGemini = groupName.includes("gemini"); - const isClaude = groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt"); + const groupDisplayName = typeof group.displayName === "string" ? group.displayName : ""; + const groupName = `${groupDisplayName} ${typeof group.description === "string" ? group.description : ""}`.toLowerCase(); + // A description that merely mentions Argon must not move a whole Gemini group. + const groupFamily: AntigravitySummaryFamily | undefined = ARGON_TOKEN.test(groupDisplayName) ? "argon" + : groupName.includes("gemini") ? "gem" + : groupName.includes("claude") || groupName.includes("3p") || groupName.includes("gpt") ? "cla" + : undefined; const buckets = Array.isArray(group.buckets) ? (group.buckets as unknown[]) : []; for (const rawBucket of buckets) { @@ -143,13 +170,19 @@ function parseAntigravityQuotaSummary(body: Record | null): Pro const isWeekly = windowStr.includes("week"); const is5h = windowStr.includes("5h") || windowStr.includes("five"); + const family = antigravitySummaryBucketFamily(bucket) ?? groupFamily; - if (isGemini) { + if (family === "argon") { + const label = is5h ? "Argon" : isWeekly ? "Argon (Weekly)" : ""; + if (label && !customWindowsMap.has(label)) { + customWindowsMap.set(label, { label, modelId: ARGON_POOL_MODEL_ID, percent, ...(resetAt !== undefined ? { resetAt } : {}) }); + } + } else if (family === "gem") { const label = is5h ? "Gem" : isWeekly ? "Gem (Weekly)" : ""; if (label && !customWindowsMap.has(label)) { customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) }); } - } else if (isClaude) { + } else if (family === "cla") { const label = is5h ? "Cla" : isWeekly ? "Cla (Weekly)" : ""; if (label && !customWindowsMap.has(label)) { customWindowsMap.set(label, { label, percent, ...(resetAt !== undefined ? { resetAt } : {}) }); @@ -164,7 +197,7 @@ function parseAntigravityQuotaSummary(body: Record | null): Pro } } - const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)"]; + const PREFERRED_ORDER = ["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)", "Argon", "Argon (Weekly)"]; const customWindows = Array.from(customWindowsMap.values()).sort((a, b) => { const ia = PREFERRED_ORDER.indexOf(a.label); const ib = PREFERRED_ORDER.indexOf(b.label); diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index aab02c118d2..67784a65f0d 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -578,7 +578,7 @@ Antigravity discovery in `src/providers/antigravity-models.ts` collapses complet Antigravity catalog rows carry the observed complete effort map from `src/providers/antigravity-models.ts` through the in-memory cache. `src/providers/antigravity-effort-families.ts` uses that evidence to group public rows and project suffix allowlists into the base selection, including both final catalog merge callers. Internal retained suffix rows remain available to combo resolution. Authoritative new-model reconciliation normalizes both the prior baseline and policy input before detecting arrivals: an existing suffix family is not a new model. All-disabled tiers transfer a base disable once; a known base keeps the operator's later choice. Genuinely new families still follow new-model policy. Saved defaults, combo targets, suffix disables and provider selections remain unchanged. A discovered ladder advertises only its wire-backed efforts unless explicitly overridden; unknown single-wire models gain no invented ladder. 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. Antigravity model entitlement is per account: one account may list Claude 4.6, another only the Claude 5.5 suffix families, and Gemini 4 Argon only on Ultra/Enterprise. `ANTIGRAVITY_MODELS` is therefore a static superset (Gemini 3.8 Flash, Gemini 4 Argon, Claude Opus 5.5, Claude Sonnet 5.5), and `ANTIGRAVITY_KNOWN_PICKER_MODELS` keeps dropped bases known to routing, discovery collapse and usage identity. The catalog exposes only what the active account's authoritative discovery returns. `src/providers/antigravity-account-roster.ts` records each account's discovered roster (in memory, six-hour freshness, scoped to config home); `src/oauth/generic-account-failover.ts` ranks Antigravity accounts by it the way it ranks Kiro listings: a listing account wins, a known non-listing account is skipped while another remains, and an account without a fresh roster is unknown and stays usable. Gemini 4 Argon requests and Antigravity model discovery send the Antigravity CLI User-Agent; every other model keeps the IDE User-Agent. -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 and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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. Paused reads retain only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication. +`retrieveUserQuotaSummary` reports Gemini 4 Argon as its own group; `src/providers/quota/antigravity.ts` decides each bucket's family from its own id/name (Argon before Gemini, then Claude/3P/GPT), falls back to the group display name only for an inconclusive bucket, and never reads Argon from a group description. Argon buckets become `Argon`/`Argon (Weekly)` windows tagged with the canonical pool model `gemini-4-argon`, so they never fill or replace Gem/Cla in either group order and stay diagnostic like the model windows below; an Argon-only summary is a display report with no routing evidence, and the compact GUI summary and tray aggregation treat it like other diagnostic-only data. When the summary endpoint has no usable result, the same file preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries, where an Argon model never seeds Gem. Model windows carry the exact CCA wire ID and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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. Paused reads retain only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication. Account-row projection is loaded with `import()` after each account probe in `src/providers/quota.ts`; its cache dependencies do not create a static return edge into the quota facade. diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 9481c2a8c7a..f0c9870e6e7 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -304,6 +304,8 @@ Unicode pattern normalization uses [copy-on-write traversal](byte-accounting.md# `src/oauth/account-quota-rank.ts` ranks Antigravity custom windows for the requested Gemini or Claude family, including GPT-OSS in the Claude family. An unknown model retains all-window ranking; absent matching evidence retains the existing unranked behavior. +Windows carrying `modelId` (exact model rows and the Argon summary pool) are excluded +from headroom and from reset-time selection, including family-less requests. `src/server/responses/request-transport.ts` passes the routed model at initial selection. The passthrough, adapter, continuation, sidecar and run-turn execution owners pass the same routed model during account rotation, without bypassing their send-budget diff --git a/tests/providers/provider-antigravity-model-quota.test.ts b/tests/providers/provider-antigravity-model-quota.test.ts index 9a486185534..be00103dd96 100644 --- a/tests/providers/provider-antigravity-model-quota.test.ts +++ b/tests/providers/provider-antigravity-model-quota.test.ts @@ -1,6 +1,11 @@ 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 { + accountHeadroomPercent, + accountResetTimestamp, + rankAccountsByHeadroom, + rankAccountsByResetFirst, +} 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"; @@ -187,3 +192,168 @@ describe("Antigravity model quota evidence", () => { expect(providerWindowObservations(result.quota).map(observation => observation.window)).toEqual(["custom:Gem"]); }); }); + +describe("Antigravity Argon quota summary (#7272)", () => { + const argonReset = "2026-10-24T04:34:17Z"; + const geminiGroup = { + displayName: "Gemini Models", + // A description that mentions Argon must not move the whole Gemini group. + description: "Models within this group: Gemini Flash, Gemini Pro, Gemini 4 Argon", + buckets: [ + { bucketId: "gemini-weekly", displayName: "Weekly Limit Remaining", window: "weekly", resetTime: "2026-10-25T00:00:00Z", remainingFraction: 0.88 }, + { bucketId: "gemini-5h", displayName: "Five Hour Limit Remaining", window: "5h", resetTime: "2026-10-18T05:00:00Z", remainingFraction: 0.75 }, + ], + }; + const claudeGroup = { + displayName: "Claude and GPT models", + buckets: [{ bucketId: "3p-weekly", displayName: "Weekly Limit Remaining", window: "weekly", resetTime: "2026-10-26T00:00:00Z", remainingFraction: 0.95 }], + }; + const argonGroup = { + displayName: "Gemini 4 Argon", + description: "For paid Pro plans, Argon quota is separate.", + buckets: [{ bucketId: "gemini-argon-wtu-weekly", displayName: "Gemini 4 Argon Weekly Limit", window: "weekly", resetTime: argonReset, remainingFraction: 0.75 }], + }; + + let modelRequests = 0; + function setSummaryResponse(groups: unknown[], models: unknown = {}): void { + 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("retrieveUserQuotaSummary")) { + return new Response(JSON.stringify({ groups }), { status: 200, headers: { "content-type": "application/json" } }); + } + modelRequests += 1; + return new Response(JSON.stringify({ models }), { status: 200, headers: { "content-type": "application/json" } }); + }, + }); + } + + async function summaryWindows(groups: unknown[]) { + setSummaryResponse(groups); + const result = await probeAntigravityUsageQuota("synthetic-token", "synthetic-project"); + expect(result).toMatchObject({ kind: "available", source: "google-antigravity:retrieveUserQuotaSummary" }); + return result.kind === "available" ? result.quota.customWindows ?? [] : []; + } + + const label = (windows: Array<{ label: string; percent: number; modelId?: string }>) => + windows.map(window => `${window.label}=${window.percent}${window.modelId ? `@${window.modelId}` : ""}`); + + test.each([ + ["Gemini group first", [geminiGroup, claudeGroup, argonGroup]], + ["Argon group first", [argonGroup, geminiGroup, claudeGroup]], + ])("keeps Gem (Weekly) and a separate Argon window: %s", async (_name, groups) => { + const windows = await summaryWindows(groups); + expect(label(windows)).toEqual(["Gem=25", "Gem (Weekly)=12", "Cla (Weekly)=5", "Argon (Weekly)=25@gemini-4-argon"]); + expect(windows.at(-1)).toEqual({ label: "Argon (Weekly)", modelId: "gemini-4-argon", percent: 25, resetAt: Date.parse(argonReset) }); + // The summary already carries Argon; no model-list request is needed. + expect(modelRequests).toBe(0); + }); + + test("Argon buckets map 5h and weekly, skip other cadences and malformed readings, and stay first-seen", async () => { + const windows = await summaryWindows([ + { displayName: "Gemini 4 Argon", buckets: [ + { bucketId: "gemini-argon-daily", window: "daily", remainingFraction: 0.1 }, + { bucketId: "gemini-argon-5h", window: "5h", remainingFraction: "unknown" }, + { bucketId: "gemini-argon-5h", window: "5h", remainingFraction: 0.6 }, + { bucketId: "gemini-argon-weekly", window: "weekly", remainingFraction: 0.9 }, + ] }, + { displayName: "Gemini 4 Argon", buckets: [{ bucketId: "gemini-argon-weekly", window: "weekly", remainingFraction: 0.1 }] }, + ]); + expect(label(windows)).toEqual(["Argon=40@gemini-4-argon", "Argon (Weekly)=10@gemini-4-argon"]); + }); + + test("a bucket's own identity decides its family; the group decides only an inconclusive bucket", async () => { + expect(label(await summaryWindows([ + { displayName: "Gemini Models", buckets: [ + { bucketId: "gemini-weekly", window: "weekly", remainingFraction: 0.5 }, + { bucketId: "gemini-argon-wtu-weekly", window: "weekly", remainingFraction: 0.2 }, + ] }, + ]))).toEqual(["Gem (Weekly)=50", "Argon (Weekly)=80@gemini-4-argon"]); + + expect(label(await summaryWindows([ + { displayName: "Gemini 4 Argon", buckets: [ + { bucketId: "gemini-weekly", window: "weekly", remainingFraction: 0.5 }, + { bucketId: "claude-weekly", window: "weekly", remainingFraction: 0.4 }, + { bucketId: "weekly-1", window: "weekly", remainingFraction: 0.3 }, + ] }, + { displayName: "Claude and GPT models", buckets: [{ bucketId: "gemini-5h", window: "5h", remainingFraction: 0.9 }] }, + ]))).toEqual(["Gem=10", "Gem (Weekly)=50", "Cla (Weekly)=60", "Argon (Weekly)=70@gemini-4-argon"]); + + // Token boundaries: an unknown group with an "argonaut" bucket keeps its own label. + expect(label(await summaryWindows([ + { displayName: "Experimental", buckets: [{ bucketId: "argonaut-weekly", window: "weekly", remainingFraction: 0.5 }] }, + ]))).toEqual(["Experimental (Weekly)=50"]); + }); + + test("model-list fallback never lets an Argon model seed the Gem family", async () => { + setModelsResponse({ + "gemini-4-argon-high": { displayName: "Gemini 4 Argon High", quotaInfo: { remainingFraction: 0.2 } }, + "gemini-3.8-flash-high": { quotaInfo: { remainingFraction: 0.9 } }, + }); + const first = await probeAntigravityUsageQuota("synthetic-token", "synthetic-project"); + expect(first.kind).toBe("available"); + if (first.kind !== "available") return; + expect(first.quota.customWindows?.filter(window => window.modelId === undefined)).toEqual([{ label: "Gem", percent: 10 }]); + expect(first.quota.customWindows).toContainEqual({ label: "gemini-4-argon-high", modelId: "gemini-4-argon-high", percent: 80 }); + + // An Argon-only listing keeps its exact row (even one labelled like the pool) and invents no Gem evidence. + setModelsResponse({ "gemini-4-argon": { quotaInfo: { remainingFraction: 0.2 } } }); + const only = await probeAntigravityUsageQuota("synthetic-token", "synthetic-project"); + expect(only.kind).toBe("available"); + if (only.kind !== "available") return; + expect(only.quota.customWindows).toEqual([{ label: "gemini-4-argon", modelId: "gemini-4-argon", percent: 80 }]); + }); + + test("an exhausted Argon pool changes no routing, exhaustion, notification or reset decision", async () => { + const windows = await summaryWindows([ + geminiGroup, + claudeGroup, + { ...argonGroup, buckets: [{ ...argonGroup.buckets[0], resetTime: "2026-10-19T00:00:00Z", remainingFraction: 0 }] }, + ]); + const quota = { updatedAt: Date.now(), customWindows: windows }; + const withoutArgon = { updatedAt: Date.now(), customWindows: windows.filter(window => window.modelId === undefined) }; + expect(windows.some(window => window.modelId === "gemini-4-argon" && window.percent === 100)).toBe(true); + + for (const model of [undefined, "gemini-4-argon-high", "gemini-3.8-flash-high", "claude-sonnet-4-6"]) { + expect(cachedProviderQuotaIsExhausted(quota, Date.now(), model)).toBe(false); + setCachedProviderAccountQuotaForTests("google-antigravity", "first", quota); + setCachedProviderAccountQuotaForTests("google-antigravity", "second", withoutArgon); + expect(accountHeadroomPercent("google-antigravity", "first", model)) + .toBe(accountHeadroomPercent("google-antigravity", "second", model)); + expect(accountResetTimestamp("google-antigravity", "first", model, Date.parse("2026-10-17T00:00:00Z"))) + .toBe(accountResetTimestamp("google-antigravity", "second", model, Date.parse("2026-10-17T00:00:00Z"))); + } + expect(providerWindowObservations(quota).map(observation => observation.window)) + .toEqual(["custom:Gem", "custom:Gem (Weekly)", "custom:Cla (Weekly)"]); + + // Family-less reset time: with no Gem weekly window, an Argon weekly row must not stand in for it. + setCachedProviderAccountQuotaForTests("google-antigravity", "first", { + updatedAt: Date.now(), + customWindows: [ + { label: "Gem", percent: 10, resetAt: Date.parse("2026-10-18T05:00:00Z") }, + { label: "Argon (Weekly)", modelId: "gemini-4-argon", percent: 10, resetAt: Date.parse("2026-10-19T00:00:00Z") }, + ], + }); + expect(accountResetTimestamp("google-antigravity", "first", undefined, Date.parse("2026-10-17T00:00:00Z"))) + .toBe(Date.parse("2026-10-18T05:00:00Z")); + }); + + test("an Argon-only summary is an available display report that carries no routing evidence", async () => { + const windows = await summaryWindows([argonGroup]); + expect(label(windows)).toEqual(["Argon (Weekly)=25@gemini-4-argon"]); + // A usable summary suppresses the model-list fallback, as for any other summary. + expect(modelRequests).toBe(0); + setCachedProviderAccountQuotaForTests("google-antigravity", "first", { updatedAt: Date.now(), customWindows: windows }); + for (const model of [undefined, "gemini-4-argon-high", "claude-sonnet-4-6"]) { + expect(accountHeadroomPercent("google-antigravity", "first", model)).toBeNull(); + expect(accountResetTimestamp("google-antigravity", "first", model, Date.parse("2026-10-17T00:00:00Z"))).toBeNull(); + } + expect(rankAccountsByResetFirst("google-antigravity", ["first", "second"])).toEqual(["first", "second"]); + expect(rankAccountsByResetFirst("google-antigravity", ["second", "first"])).toEqual(["second", "first"]); + }); +}); From fea2fdbbc11eec5eed38d99d2a9b17eb8514b37e Mon Sep 17 00:00:00 2001 From: JUN Date: Sun, 18 Oct 2026 04:30:19 +0900 Subject: [PATCH 2/2] docs(structure): state the Antigravity group fallback rules exactly --- structure/providers-and-adapters.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/structure/providers-and-adapters.md b/structure/providers-and-adapters.md index 67784a65f0d..6fc19e30b8a 100644 --- a/structure/providers-and-adapters.md +++ b/structure/providers-and-adapters.md @@ -578,7 +578,7 @@ Antigravity discovery in `src/providers/antigravity-models.ts` collapses complet Antigravity catalog rows carry the observed complete effort map from `src/providers/antigravity-models.ts` through the in-memory cache. `src/providers/antigravity-effort-families.ts` uses that evidence to group public rows and project suffix allowlists into the base selection, including both final catalog merge callers. Internal retained suffix rows remain available to combo resolution. Authoritative new-model reconciliation normalizes both the prior baseline and policy input before detecting arrivals: an existing suffix family is not a new model. All-disabled tiers transfer a base disable once; a known base keeps the operator's later choice. Genuinely new families still follow new-model policy. Saved defaults, combo targets, suffix disables and provider selections remain unchanged. A discovered ladder advertises only its wire-backed efforts unless explicitly overridden; unknown single-wire models gain no invented ladder. 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. Antigravity model entitlement is per account: one account may list Claude 4.6, another only the Claude 5.5 suffix families, and Gemini 4 Argon only on Ultra/Enterprise. `ANTIGRAVITY_MODELS` is therefore a static superset (Gemini 3.8 Flash, Gemini 4 Argon, Claude Opus 5.5, Claude Sonnet 5.5), and `ANTIGRAVITY_KNOWN_PICKER_MODELS` keeps dropped bases known to routing, discovery collapse and usage identity. The catalog exposes only what the active account's authoritative discovery returns. `src/providers/antigravity-account-roster.ts` records each account's discovered roster (in memory, six-hour freshness, scoped to config home); `src/oauth/generic-account-failover.ts` ranks Antigravity accounts by it the way it ranks Kiro listings: a listing account wins, a known non-listing account is skipped while another remains, and an account without a fresh roster is unknown and stays usable. Gemini 4 Argon requests and Antigravity model discovery send the Antigravity CLI User-Agent; every other model keeps the IDE User-Agent. -`retrieveUserQuotaSummary` reports Gemini 4 Argon as its own group; `src/providers/quota/antigravity.ts` decides each bucket's family from its own id/name (Argon before Gemini, then Claude/3P/GPT), falls back to the group display name only for an inconclusive bucket, and never reads Argon from a group description. Argon buckets become `Argon`/`Argon (Weekly)` windows tagged with the canonical pool model `gemini-4-argon`, so they never fill or replace Gem/Cla in either group order and stay diagnostic like the model windows below; an Argon-only summary is a display report with no routing evidence, and the compact GUI summary and tray aggregation treat it like other diagnostic-only data. When the summary endpoint has no usable result, the same file preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries, where an Argon model never seeds Gem. Model windows carry the exact CCA wire ID and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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. Paused reads retain only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication. +`retrieveUserQuotaSummary` reports Gemini 4 Argon as its own group; `src/providers/quota/antigravity.ts` decides each bucket's family from its own id/name (Argon before Gemini, then Claude/3P/GPT), and only for an inconclusive bucket falls back to the group: Argon from the group display name alone (never its description), Gemini and Claude/3P/GPT from the existing display-name-plus-description rules. Argon buckets become `Argon`/`Argon (Weekly)` windows tagged with the canonical pool model `gemini-4-argon`, so they never fill or replace Gem/Cla in either group order and stay diagnostic like the model windows below; an Argon-only summary is a display report with no routing evidence, and the compact GUI summary and tray aggregation treat it like other diagnostic-only data. When the summary endpoint has no usable result, the same file preserves bounded per-model `quotaInfo` measurements from `fetchAvailableModels` alongside the existing first-seen Gem/Cla family summaries, where an Argon model never seeds Gem. Model windows carry the exact CCA wire ID and distinct labels of at most 128 characters for display and diagnostics; GUI summaries and native compact rows preserve the discriminator, retain details, and exclude model readings from headline and severity selection; account and provider routing ignore these rows and continue to use family summaries, and `src/quota/window-mapping.ts` keeps them out of quota-reset observation so one pool reset notifies once. 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. Paused reads retain only current last-good measurements without probing; `src/providers/quota/account-row.ts` preserves private currentness callbacks through account-row projection until management publication. Account-row projection is loaded with `import()` after each account probe in `src/providers/quota.ts`; its cache dependencies do not create a static return edge into the quota facade.