diff --git a/docs-site/src/content/docs/reference/configuration/providers.md b/docs-site/src/content/docs/reference/configuration/providers.md index 48b71d4e255..a164a4ab1f3 100644 --- a/docs-site/src/content/docs/reference/configuration/providers.md +++ b/docs-site/src/content/docs/reference/configuration/providers.md @@ -408,7 +408,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity | `directGeminiWireRenames?` | `boolean` | Google only. Applies in AI Studio mode, including custom Google-compatible URLs. Omitted or `true` keeps the `-tiered` wire rename for Gemini Flash ids (`gemini-3.7-flash` -> `gemini-3.7-flash-tiered`); `false` sends the requested bare ids to the wire unchanged. Vertex and canonical keyed OpenCode Zen routes preserve the requested model ID regardless of this setting; Cloud Code Assist routing is unchanged. Set `false` when the configured upstream still serves the bare ids. | | `project?` | `string` | Vertex or Antigravity Cloud Code Assist project id. Vertex API keys use this project endpoint when set; without it they use express mode, even if `GOOGLE_CLOUD_PROJECT` is set. ADC falls back to `GOOGLE_CLOUD_PROJECT` / `GCLOUD_PROJECT`. `"auto"` makes each Vertex API key resolve its own project (a successful lookup is cached per key; a key whose lookup fails is probed again on a later request), so one key pool can span projects; ADC rejects `"auto"`. | | — | — | 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 summary is requested with the Antigravity CLI User-Agent, the only client family for which Google includes the Gemini 4 Argon quota group. If it 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. Non-Argon inference and the model-list quota fallback keep 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. API-key project endpoints default to `global` and ignore `GOOGLE_CLOUD_LOCATION`; ADC requires this field or the `GOOGLE_CLOUD_LOCATION` fallback. Gemini 4 Argon supports `global`, `us`, and `eu`. | | `mcpServers?` | `Record` | Cursor only: stdio or Streamable HTTP MCP servers. | diff --git a/src/adapters/client-fingerprint.ts b/src/adapters/client-fingerprint.ts index 081061be552..0765726d36d 100644 --- a/src/adapters/client-fingerprint.ts +++ b/src/adapters/client-fingerprint.ts @@ -78,7 +78,8 @@ const ANTIGRAVITY_CLI_PLATFORM = "darwin/arm64"; * Verified 2026-10-14 against live CCA: `gemini-4-argon-{low,medium,high}` answer only to this * family and return 404 "Requested entity was not found" to the IDE UA above, while * `gemini-3.7-flash-tiered` and `gemini-3.1-pro-low` 404 under this one. So it is used ONLY for - * Argon requests and for model discovery, never as a general replacement for the IDE UA. + * Argon requests, model discovery and the quota summary (whose Argon group is likewise CLI-only), + * never as a general replacement for the IDE UA. * * `GOOGLE_ANTIGRAVITY_USER_AGENT` deliberately does not apply here: that override names the IDE * header, and letting it replace this one would re-lock Argon for anyone who set it. diff --git a/src/providers/quota/antigravity.ts b/src/providers/quota/antigravity.ts index fb84d282f39..4a110ed5e18 100644 --- a/src/providers/quota/antigravity.ts +++ b/src/providers/quota/antigravity.ts @@ -1,4 +1,4 @@ -import { antigravityUserAgent } from "../../adapters/client-fingerprint"; +import { antigravityCliUserAgent, antigravityUserAgent } from "../../adapters/client-fingerprint"; import { DestinationDnsResolutionError } from "../../lib/destination-policy"; import { PinnedHttpError, type PinnedHttpErrorCode } from "../../lib/pinned-http"; import { ProviderOutboundPolicyError, providerOutboundPost, providerRedirectError, type ProviderOutboundDependencies } from "../../lib/provider-outbound"; @@ -303,7 +303,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId: // An optional caller signal (account import) cancels every request of this probe, including the // 403 retry and the models fallback, alongside the per-request deadline. const requestSignal = () => signal ? AbortSignal.any([signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)]) : AbortSignal.timeout(REQUEST_TIMEOUT_MS); - const fetchQuota = (url: string, userAgent = antigravityUserAgent()) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, { + const fetchQuota = (url: string, userAgent: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, { headers: { Accept: "application/json", "Content-Type": "application/json", "User-Agent": userAgent, Authorization: `Bearer ${accessToken}`, @@ -312,7 +312,9 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId: }, antigravityOutboundDependencies); let summaryFailure: QuotaFailureCode | undefined; try { - let response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL); + // The summary includes the Gemini 4 Argon group only for the CLI client family; under the + // IDE UA the same account reports just the Gemini and Claude groups, so Argon usage is lost. + let response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL, antigravityCliUserAgent()); if (response.status === 403) { // Some valid accounts reject the IDE fingerprint only for quota accounting. try { await response.body?.cancel(); } catch { /* Best-effort release before retry. */ } @@ -330,7 +332,7 @@ export async function probeAntigravityUsageQuota(accessToken: string, projectId: } if (signal?.aborted) return unavailableAntigravityQuota(summaryFailure ?? "transport_error"); try { - const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL); + const response = await fetchQuota(ANTIGRAVITY_QUOTA_MODELS_URL, antigravityUserAgent()); if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_MODELS_URL)) { return unavailableAntigravityQuota(antigravityUnavailableFailure(summaryFailure, "redirect_blocked")); } diff --git a/structure/transports/inventory.md b/structure/transports/inventory.md index 37699c0a913..05560c912e8 100644 --- a/structure/transports/inventory.md +++ b/structure/transports/inventory.md @@ -274,7 +274,7 @@ Pool quota producers and account commands follow the [bounded raw-observation co ## Account quota failure diagnostics -`src/providers/quota/antigravity.ts` retries a quota-summary 403 once with `User-Agent: antigravity/1.0`, releasing the first response body and preserving the bearer, project and pinned accounting endpoint. A 401 is not retried; redirects remain blocked, and a repeated 403 remains unavailable with `access_denied`. Other retry failures retain the models fallback, whose User-Agent remains the IDE fingerprint, as do discovery and inference. +`src/providers/quota/antigravity.ts` requests the quota summary with the Antigravity CLI User-Agent, because Cloud Code Assist includes the Gemini 4 Argon group only for the CLI client family; under the IDE fingerprint the same account reports only the Gemini and Claude groups. It retries a quota-summary 403 once with `User-Agent: antigravity/1.0`, releasing the first response body and preserving the bearer, project and pinned accounting endpoint. A 401 is not retried; redirects remain blocked, and a repeated 403 remains unavailable with `access_denied`. Other retry failures retain the models fallback, whose User-Agent remains the IDE fingerprint, as does non-Argon inference. Antigravity account quota probes expose only a closed `quotaFailure` category when the read is unavailable. Typed transport failures, rejected destinations, redirects, denied access, rate limits and unusable bodies are distinguished; successful fallback clears the earlier failure. The last attempted endpoint determines the diagnosis. When the credential side throws before the probe (missing account, failed refresh, no project) the category is `account_unavailable`, or `timeout` when that failure was a request deadline. A 401/403 category does not change account health, entitlement or routing eligibility. diff --git a/tests/providers/provider-antigravity-quota-retry.test.ts b/tests/providers/provider-antigravity-quota-retry.test.ts index ac3ea59a1bd..27c403a1cad 100644 --- a/tests/providers/provider-antigravity-quota-retry.test.ts +++ b/tests/providers/provider-antigravity-quota-retry.test.ts @@ -1,5 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { antigravityUserAgent } from "../../src/adapters/client-fingerprint"; +import { antigravityCliUserAgent, antigravityUserAgent } from "../../src/adapters/client-fingerprint"; import { probeAntigravityUsageQuota, setAntigravityAccountQuotaTransportForTests } from "../../src/providers/quota/antigravity"; import { PROXY_ENV_KEYS } from "../../src/lib/proxy-env"; @@ -35,7 +35,7 @@ function transport(responses: Array) { return calls; } -function expectedCall(url: string, userAgent = antigravityUserAgent()) { +function expectedCall(url: string, userAgent = url === summaryUrl ? antigravityCliUserAgent() : antigravityUserAgent()) { return { url, userAgent, authorization: "Bearer test-access", body: JSON.stringify({ project: "test-project" }) }; } @@ -57,12 +57,38 @@ describe("Antigravity quota summary 403 compatibility retry (#5940)", () => { }); } - test("successful IDE summary needs no retry", async () => { + test("successful CLI summary needs no retry", async () => { const calls = transport([Response.json(summary)]); expect((await probeAntigravityUsageQuota("test-access", "test-project")).kind).toBe("available"); expect(calls).toEqual([expectedCall(summaryUrl)]); }); + test("summary is requested with the CLI fingerprint so the Argon group is reported", async () => { + // Live CCA shape: the Argon group is present only for the CLI client family. + const liveSummary = { groups: [ + { displayName: "Gemini Models", buckets: [ + { bucketId: "gemini-weekly", window: "weekly", resetTime: "2026-11-03T05:10:46Z", remainingFraction: 1 }, + { bucketId: "gemini-5h", window: "5h", resetTime: "2026-10-27T10:10:46Z", remainingFraction: 1 }, + ] }, + { displayName: "Claude and GPT models", buckets: [ + { bucketId: "3p-weekly", window: "weekly", resetTime: "2026-11-03T05:10:46Z", remainingFraction: 1 }, + { bucketId: "3p-5h", window: "5h", resetTime: "2026-10-27T10:10:46Z", remainingFraction: 1 }, + ] }, + { displayName: "Gemini 4 Argon", buckets: [ + { bucketId: "gemini-argon-wtu-weekly", displayName: "Gemini 4 Argon Weekly Limit", window: "weekly", resetTime: "2026-11-01T11:46:46Z", remainingFraction: 0.75 }, + ] }, + ] }; + const calls = transport([Response.json(liveSummary)]); + const result = await probeAntigravityUsageQuota("test-access", "test-project"); + expect(calls[0]?.userAgent).toStartWith("antigravity/cli/"); + expect(result).toMatchObject({ kind: "available", source: "google-antigravity:retrieveUserQuotaSummary" }); + if (result.kind !== "available") { + throw new Error("expected available quota"); + } + expect(result.quota.customWindows?.map(window => window.label)).toEqual(["Gem", "Gem (Weekly)", "Cla", "Cla (Weekly)", "Argon (Weekly)"]); + expect(result.quota.customWindows?.at(-1)).toMatchObject({ label: "Argon (Weekly)", modelId: "gemini-4-argon", percent: 25 }); + }); + test("401 is not retried", async () => { const calls = transport([new Response(null, { status: 401 })]); expect(await probeAntigravityUsageQuota("test-access", "test-project")).toMatchObject({ kind: "unavailable", failure: "access_denied" });