Skip to content
Closed
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 @@ -286,6 +286,7 @@ Providers can expose a built-in shorthand, such as `agy` for `google-antigravity
| `directGeminiWireRenames?` | `boolean` | Google only. Applies only to direct AI Studio requests. 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 preserves the requested model ID, and 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. |
| — | — | 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. |
| `location?` | `string` | Vertex location; environment fallback is `GOOGLE_CLOUD_LOCATION`. |
| `mcpServers?` | `Record<string, CursorMcpServerConfig>` | Cursor only: stdio or Streamable HTTP MCP servers. |
| `desktopExecutor?` | `DesktopExecutorConfig` | Cursor only: external computer-use and record-screen commands. |
Expand Down
1 change: 1 addition & 0 deletions scripts/test-layout/layout.json
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@
}
},
"explicit": {
"provider-antigravity-quota-retry.test.ts": "providers",
"deepseek-artifact-tool-schema.test.ts": "providers",
"client-config-export-output-limit.test.ts": "config",
"openai-chat-serialized-tool-call-scaling.test.ts": "adapters/openai",
Expand Down
11 changes: 8 additions & 3 deletions src/providers/quota/antigravity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,16 +253,21 @@ function antigravityUnavailableFailure(
}

export async function probeAntigravityUsageQuota(accessToken: string, projectId: string): Promise<AntigravityQuotaProbeResult> {
const fetchQuota = (url: string) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
const fetchQuota = (url: string, userAgent = antigravityUserAgent()) => providerOutboundPost("google-antigravity", { baseUrl: ANTIGRAVITY_ACCOUNT_QUOTA_BASE }, url, {
headers: {
Accept: "application/json", "Content-Type": "application/json",
"User-Agent": antigravityUserAgent(), Authorization: `Bearer ${accessToken}`,
"User-Agent": userAgent, Authorization: `Bearer ${accessToken}`,
},
body: JSON.stringify({ project: projectId }), signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
}, antigravityOutboundDependencies);
let summaryFailure: QuotaFailureCode | undefined;
try {
const response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
let response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL);
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. */ }
response = await fetchQuota(ANTIGRAVITY_QUOTA_SUMMARY_URL, "antigravity/1.0");
}
if (await providerRedirectError(response, ANTIGRAVITY_QUOTA_SUMMARY_URL)) return unavailableAntigravityQuota("redirect_blocked");
if (response.status === 401 || response.status === 403) return unavailableAntigravityQuota("access_denied");
if (response.ok) {
Expand Down
2 changes: 2 additions & 0 deletions structure/transports/inventory.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@ 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.

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. A 401/403 category does not change account health, entitlement or routing eligibility.

`src/providers/quota.ts` binds diagnoses to the probed credential/project and rechecks before cache reads and API projection. Reauthentication invalidates an old diagnosis independently of last-good quota bars. Private digests, callbacks and upstream error values are not serialized. The CLI and current/all-account dashboard views consume the same closed code; unknown codes and local management-read failures retain generic unavailable text. Codes are transient, never persisted quota evidence. Authenticated TUN field acceptance remains separate from deterministic transport coverage.
Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/test-layout-expected.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"provider-antigravity-quota-retry.test.ts": "providers",
"deepseek-artifact-tool-schema.test.ts": "providers",
"client-config-export-output-limit.test.ts": "config",
"openai-chat-serialized-tool-call-scaling.test.ts": "adapters/openai",
Expand Down
2 changes: 1 addition & 1 deletion tests/providers/provider-account-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -926,7 +926,7 @@ describe("google-antigravity per-account quota (#1082)", () => {
},
});
expect(await fetchProviderAccountQuotas("google-antigravity")).toEqual([{ accountId: idFor("a@example.com"), quota: null, unavailable: true, quotaFailure: status < 400 ? "redirect_blocked" : "access_denied" }]);
expect(posted).toEqual(fallback ? [summaryUrl, modelsUrl] : [summaryUrl]);
expect(posted).toEqual(fallback ? [summaryUrl, modelsUrl] : status === 403 ? [summaryUrl, summaryUrl] : [summaryUrl]);
expect(plainFetchCalls).toBe(0);
});
}
Expand Down
87 changes: 87 additions & 0 deletions tests/providers/provider-antigravity-quota-retry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { antigravityUserAgent } from "../../src/adapters/client-fingerprint";
import { probeAntigravityUsageQuota, setAntigravityAccountQuotaTransportForTests } from "../../src/providers/quota/antigravity";

import { PROXY_ENV_KEYS } from "../../src/lib/proxy-env";

const proxyKeys = PROXY_ENV_KEYS.flatMap(key => [key, key.toLowerCase()]);
const originalProxyEnv = Object.fromEntries(proxyKeys.map(key => [key, process.env[key]]));
beforeEach(() => { for (const key of proxyKeys) delete process.env[key]; });
afterEach(() => {
for (const key of proxyKeys) {
if (originalProxyEnv[key] === undefined) delete process.env[key];
else process.env[key] = originalProxyEnv[key];
}
});

const summaryUrl = "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary";
const modelsUrl = "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels";
const summary = { groups: [{ displayName: "Gemini", buckets: [{ window: "5h", remainingFraction: 0.6 }] }] };
const models = { models: { gemini: { quotaInfo: { remainingFraction: 0.75 } } } };

function transport(responses: Array<Response | Error>) {
const calls: Array<{ url: string; userAgent: string | null; authorization: string | null; body: string }> = [];
setAntigravityAccountQuotaTransportForTests({
resolveAddresses: async () => ({ hostname: "daily-cloudcode-pa.googleapis.com", addresses: [{ address: "142.250.0.1", family: 4 }], privateNetwork: false }),
pinnedPost: async (url, _address, body, _signal, options) => {
const headers = new Headers(options?.headers);
calls.push({ url, userAgent: headers.get("user-agent"), authorization: headers.get("authorization"), body });
const response = responses.shift();
if (!response) throw new Error("Unexpected extra quota request");
if (response instanceof Error) throw response;
return response;
},
});
return calls;
}

function expectedCall(url: string, userAgent = antigravityUserAgent()) {
return { url, userAgent, authorization: "Bearer test-access", body: JSON.stringify({ project: "test-project" }) };
}

afterEach(() => setAntigravityAccountQuotaTransportForTests(null));

describe("Antigravity quota summary 403 compatibility retry (#5940)", () => {
for (const cancelFails of [false, true]) {
test(`retries once with identical bearer and project, even when cancellation ${cancelFails ? "fails" : "succeeds"}`, async () => {
let cancelled = false;
const denied = new Response(new ReadableStream({ cancel() {
cancelled = true;
if (cancelFails) throw new Error("cancel failed");
} }), { status: 403 });
const calls = transport([denied, Response.json(summary)]);
const result = await probeAntigravityUsageQuota("test-access", "test-project");
expect(cancelled).toBe(true);
expect(calls).toEqual([expectedCall(summaryUrl), expectedCall(summaryUrl, "antigravity/1.0")]);
expect(result).toMatchObject({ kind: "available", source: "google-antigravity:retrieveUserQuotaSummary", quota: { customWindows: [{ label: "Gem", percent: 40 }] } });
});
}

test("successful IDE 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("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" });
expect(calls).toEqual([expectedCall(summaryUrl)]);
});

for (const status of [401, 403, 302]) {
test(`retry status ${status} stops without further requests`, async () => {
const calls = transport([new Response(null, { status: 403 }), new Response(null, { status, headers: { location: "https://redirect.example/" } })]);
expect(await probeAntigravityUsageQuota("test-access", "test-project")).toMatchObject({ kind: "unavailable", failure: status === 302 ? "redirect_blocked" : "access_denied" });
expect(calls).toEqual([expectedCall(summaryUrl), expectedCall(summaryUrl, "antigravity/1.0")]);
});
}

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(calls).toEqual([expectedCall(summaryUrl), expectedCall(summaryUrl, "antigravity/1.0"), expectedCall(modelsUrl)]);
});
}
});
2 changes: 1 addition & 1 deletion tests/providers/provider-quota.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3557,7 +3557,7 @@ describe("fetchProviderQuotaReports", () => {
pinnedPost: async url => { posted.push(url); return new Response(null, { status, headers: { location: modelsUrl } }); },
});
expect((await fetchProviderQuotaReports(config(), true)).reports).toEqual([]);
expect(posted).toEqual([summaryUrl]);
expect(posted).toEqual(status === 403 ? [summaryUrl, summaryUrl] : [summaryUrl]);
expect(plainFetchCalls).toEqual([]);
});
}
Expand Down
Loading