Skip to content

Google Antigravity preset permanently reports discovery failure by probing unsupported GET /models #723

Description

@luvs01

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:

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:

  1. Set the built-in google-antigravity registry entry to liveModels: false and use its maintained static catalog.
  2. Ensure existing OAuth configurations without an explicit override inherit the registry value.
  3. Clear or suppress discovery-failure state and skip the futile generic probe.
  4. 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:
    { 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:
    // 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:
    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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions