Repository navigation
Google Antigravity preset permanently reports discovery failure by probing unsupported GET /models #723
Description
Client or integration
Other
Provider or upstream service
google-antigravity / Google Antigravity
OpenCodex version
2.7.43 (d1f544bbc22d25b9b2bd3c8e776fb8e3242b4ed5). Also reproduced on current dev (bfacc3bcf71077a77afc25d9b613644d3409750c).
Endpoint or capability
Model discovery and provider connection test
Current behaviour
The built-in google-antigravity preset has a maintained six-model catalog but does not declare liveModels: false.
After a successful OAuth login, model discovery sends:
GET https://daily-cloudcode-pa.googleapis.com/models
That endpoint returns HTTP 404. OpenCodex preserves the configured catalog, but /api/providers permanently reports discovery as failed. The Models dashboard therefore displays Discovery failed beside an otherwise usable provider, and the connection test can report the provider as unavailable even though inference and quota retrieval work.
A redacted live check confirmed that the three selected configured models remained available. A minimal inference request through google-antigravity/gemini-3.6-flash completed successfully.
Expected behaviour
A built-in provider without a generic GET /models endpoint should not be represented as a failed live-discovery provider or probed repeatedly through an unsupported endpoint.
The minimal safe behaviour is to treat Google Antigravity as a static-catalog provider. If live discovery is intended instead, it should use the provider-specific authenticated POST /v1internal:fetchAvailableModels contract and normalize its response before publishing models.
Minimal redacted request or reproduction
1. Install OpenCodex 2.7.43.
2. Complete the built-in google-antigravity OAuth login.
3. Open the Models dashboard.
4. Request GET /api/providers with the local management credential redacted.
5. Observe that the configured catalog remains available while discovery is failed/http/404.
6. Send a minimal inference request through a selected configured model and observe that it succeeds.Actual response or error
{
"name": "google-antigravity",
"liveModels": true,
"models": ["<six configured model IDs>"],
"discovery": {
"status": "failed",
"reason": "http",
"httpStatus": 404
}
}Sanitized service log:
Provider model discovery for "google-antigravity" failed with HTTP 404 [urlClass=provider-models, fallback=configured].
The configured fallback remains usable. This report concerns the deterministic false-failure state, unnecessary probe, and misleading connection result rather than an inference outage.
Upstream documentation
No public Google Antigravity model-discovery specification was found.
Google's official Gemini CLI shows OAuth Code Assist using the private v1internal POST API family:
https://github.com/google-gemini/gemini-cli/blob/3c1bb8c35d9b43128636cdee24af01bf717c66fb/packages/core/src/code_assist/server.ts#L73-L74
OpenCodex already calls POST /v1internal:fetchAvailableModels for Google Antigravity quota/model availability:
opencodex/src/providers/quota.ts
Lines 783 to 834 in d1f544b
| async function fetchAntigravityQuota(provider: string, config: OcxProviderConfig): Promise<ProviderQuotaReport | null> { | |
| const credential = getCredential("google-antigravity"); | |
| if (!credential?.projectId) return null; | |
| let accessToken: string; | |
| try { | |
| accessToken = await getValidAccessToken("google-antigravity"); | |
| } catch { | |
| return null; | |
| } | |
| const baseUrl = (config.baseUrl || "https://daily-cloudcode-pa.googleapis.com").replace(/\/+$/, ""); | |
| const response = await fetch(`${baseUrl}/v1internal:fetchAvailableModels`, { | |
| method: "POST", | |
| headers: { | |
| Accept: "application/json", | |
| "Content-Type": "application/json", | |
| "User-Agent": antigravityUserAgent(), | |
| Authorization: `Bearer ${accessToken}`, | |
| }, | |
| body: JSON.stringify({ project: credential.projectId }), | |
| signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), | |
| }); | |
| if (!response.ok) return null; | |
| const body = asRecord(await response.json().catch(() => null)); | |
| const models = asRecord(body?.models); | |
| if (!models) return null; | |
| const windows = new Map<string, ProviderQuotaWindow>(); | |
| for (const [modelId, rawModelInfo] of Object.entries(models)) { | |
| 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 customWindows = ["Gem", "Cla"].flatMap(label => { | |
| const window = windows.get(label); | |
| return window ? [window] : []; | |
| }); | |
| if (customWindows.length === 0) return null; | |
| return report(provider, "google-antigravity:fetchAvailableModels", { | |
| customWindows, | |
| updatedAt: Date.now(), | |
| }); |
Suggested mapping or implementation notes
Preferred minimal fix:
- Set the built-in
google-antigravityregistry entry toliveModels: falseand use its maintained static catalog. - Ensure existing OAuth configurations without an explicit override inherit the registry value.
- Clear or suppress discovery-failure state and skip the futile generic probe.
- Add regression coverage for registry derivation,
/api/providers, the configured catalog, and the connection test.
Changing only modelDiscovery.path is insufficient because the current discovery contract assumes a GET request, while the provider-specific endpoint is an authenticated POST request with a different response shape.
Additional context and attachments
Relevant source paths:
- Built-in preset:
opencodex/src/providers/registry.ts
Line 720 in d1f544b
{ id: "google-antigravity", label: "Google Antigravity", adapter: "google", baseUrl: "https://daily-cloudcode-pa.googleapis.com", authKind: "oauth", dashboardUrl: "https://antigravity.google", models: ANTIGRAVITY_MODELS, defaultModel: "gemini-3.6-flash", modelContextWindows: ANTIGRAVITY_MODEL_CONTEXT_WINDOWS, modelReasoningEfforts: ANTIGRAVITY_MODEL_EFFORTS, googleMode: "cloud-code-assist", jawcodeBundle: "google", extraMetadataAliases: ["antigravity", "gemini-antigravity"] }, - Generic model-discovery branch:
Lines 490 to 510 in d1f544b
// Generative Language API: API key goes in x-goog-api-key (never Authorization: Bearer), // models live under /v1beta (v1 misses preview models), and pageSize maxes at 1000 — // enough to list everything without a pageToken loop. Vertex/antigravity keep the // generic branch (they fall back to their static model lists). if (apiKey) headers["x-goog-api-key"] = apiKey; return { url: `${effectiveProvider.baseUrl}/v1beta/models?pageSize=1000`, headers }; } if (effectiveProvider.adapter === "anthropic") { const base = effectiveProvider.baseUrl.replace(/\/v1\/?$/, ""); headers["anthropic-version"] = "2023-06-01"; if (effectiveProvider.authMode === "oauth") { headers["anthropic-beta"] = ANTHROPIC_OAUTH_BETA; if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; } else if (apiKey) { if (effectiveProvider.apiKeyTransport === "bearer") headers["Authorization"] = `Bearer ${apiKey}`; else headers["x-api-key"] = apiKey; } return { url: `${base}/v1/models?limit=1000`, headers }; } if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; return { url: `${effectiveProvider.baseUrl}/models`, headers }; - Working inference path:
opencodex/src/adapters/google.ts
Lines 321 to 375 in d1f544b
if (!token) throw new Error("google-antigravity oauth token missing — run ocx login google-antigravity"); const base = provider.baseUrl?.trim(); if (!base) throw new Error("google-antigravity requires a non-empty baseUrl"); const url = `${base}/v1internal:${method}${streamParam}`; const project = provider.project; if (!project) throw new Error("Antigravity requires a discovered Cloud Code Assist project id (re-run `ocx login google-antigravity`)."); const sessionId = antigravitySessionId(parsed); const mappedEffort = mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning); const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(parsed.modelId, mappedEffort); antigravityModel = wireModelId; antigravitySession = sessionId; // Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig). // Suffix/compat IDs return thinkingLevel=undefined — the suffix IS the effort, no contradiction. if (thinkingLevel) { const gc = (body.generationConfig ?? {}) as Record<string, unknown>; gc.thinkingConfig = { thinkingLevel }; body.generationConfig = gc; } // Reasoning continuity: Gemini models re-inject cached thoughtSignatures; Claude-on-Antigravity // sanitizes signatures inline (no cache). Both guard against the upstream 400 on bad signatures. // The real Antigravity client puts the session id ONLY at `request.sessionId` (camelCase, // nested) — matching CLIProxyAPI `generateStableSessionID`. An extra top-level/snake_case // spelling is a non-first-party key, so we send the single canonical location. const draftRequest: Record<string, unknown> = { ...body, sessionId }; // Claude-on-Antigravity forces VALIDATED function calling (the real client always sets it). if (/claude/i.test(wireModelId)) { const existing = (draftRequest.toolConfig ?? {}) as Record<string, unknown>; const fcc = (existing.functionCallingConfig ?? {}) as Record<string, unknown>; draftRequest.toolConfig = { ...existing, functionCallingConfig: { ...fcc, mode: "VALIDATED" } }; } const compiled = compileGoogleWireBody(draftRequest); const request = compiled.body; restoreGoogleToolName = compiled.restoreToolName; // Compile names before replay: signatures are keyed by the exact provider-visible name. if (Array.isArray((request as { contents?: unknown[] }).contents)) { const contents = (request as { contents: unknown[] }).contents; if (antigravityUsesReplayCache(wireModelId)) { applyAntigravityReplay(wireModelId, sessionId, contents); } else { sanitizeAntigravityClaudeSignatures(contents); } } const envelope = { model: wireModelId, // The envelope's `userAgent` field is a protocol constant ("antigravity"), distinct from // the HTTP `User-Agent` header (the real CLI UA). CLIProxyAPI `geminiToAntigravity` hardcodes // the body field; only the header carries the versioned client string. userAgent: "antigravity", requestType: "agent", project, requestId: `agent-${crypto.randomUUID()}`, request, }; headers["User-Agent"] = ANTIGRAVITY_REQUEST_UA; headers["Authorization"] = `Bearer ${token}`;
This is distinct from #395, which concerns repeated logs for a custom provider, and from #358, which intentionally keeps genuine discovery failures visible even when fallback models exist.
Checks
- I searched existing provider and compatibility issues.
- The request and response were redacted.
- The expected behaviour is based on the working provider-specific contract already used by OpenCodex and the concrete requirement that a static-catalog provider not be reported as failed.
Activity
github-actions commentedon Jul 30, 2026
Automated translation bookkeeping — detected language: unknown.
lidge-jun commentedon Jul 31, 2026
Fixed on
devinc4b83693d.google-antigravitynow carriesliveModels: false(src/providers/registry.ts:799). The entry always shipped a staticANTIGRAVITY_MODELScatalog, but a missingliveModelsreads as live, so discovery built${baseUrl}/modelsagainst an endpoint that does not implement it and reported permanent failure. Inference worked the whole time, which is what made it confusing — the provider was fine and the dashboard said it was not.With the flag set, catalog fetch takes the static branch (
src/codex/catalog/provider-fetch.ts:451) and management reports discovery as disabled rather than failed (src/server/management/provider-routes.ts:84,:90).The regression test forces a credential-bearing path deliberately: the no-token OAuth fallback short-circuits before any probe, so a naive test would pass even with the flag removed. Ablated to confirm it fails without the fix (expected 0 probes, got 1).
Registry flag only. PR #744 by @luvs01 proposed this along with OAuth reconciliation and persisted provider-settings changes; those touch a maintainer security-review surface and were intentionally not included, so that PR stays open for the remainder.
Thanks @luvs01 for the diagnosis.