Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, CursorMcpServerConfig>` | Cursor only: stdio or Streamable HTTP MCP servers. |
Expand Down
3 changes: 2 additions & 1 deletion src/adapters/client-fingerprint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
10 changes: 6 additions & 4 deletions src/providers/quota/antigravity.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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}`,
Expand All @@ -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. */ }
Expand All @@ -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"));
}
Expand Down
2 changes: 1 addition & 1 deletion structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
32 changes: 29 additions & 3 deletions tests/providers/provider-antigravity-quota-retry.test.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -35,7 +35,7 @@ function transport(responses: Array<Response | Error>) {
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" }) };
}

Expand All @@ -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" });
Expand Down
Loading