Skip to content
Open
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
7 changes: 6 additions & 1 deletion src/adapters/google.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
? resolveAntigravityEffortWireModel(
parsed.modelId,
mapReasoningEffort(provider, parsed.modelId, parsed.options.reasoning),
provider.baseUrl,
).wireModelId
: resolveDirectGeminiWireModelId(parsed.modelId);
const { systemInstruction, contents } = messagesToGeminiFormat(parsed, routedModelId);
Expand Down Expand Up @@ -450,7 +451,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
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);
const { wireModelId, thinkingLevel } = resolveAntigravityEffortWireModel(
parsed.modelId,
mappedEffort,
provider.baseUrl,
);
antigravityModel = wireModelId;
antigravitySession = sessionId;
// Effort → thinkingConfig for CCA (CLIProxyAPI proven: request.generationConfig.thinkingConfig).
Expand Down
6 changes: 5 additions & 1 deletion src/codex/catalog/provider-fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import {
type CapturedServiceTierAdapterAuthority,
} from "../../providers/service-tier";
import { effectiveGoogleMode, getProviderRegistryEntry, providerMatchesRegistryTransport } from "../../providers/registry";
import { parseAntigravityAvailableModels } from "../../providers/antigravity-models";
import { parseAntigravityAvailableModels, registerAntigravityDiscoveredWireModels } from "../../providers/antigravity-models";
import { applyProviderContextCap, providerContextCap } from "../../providers/context-cap";
import { routedSlug, slugEquals, slugsEquivalent } from "../../providers/slug-codec";
import { CODEX_GPT5_IDENTITY_LINE } from "../../adapters/identity";
Expand Down Expand Up @@ -1374,6 +1374,10 @@ async function fetchProviderModelsWithAuth(
if (!setCached(name, forCache, Date.now(), cacheGeneration)) {
return observed(withConfiguredRetention(configured), "degraded");
}
registerAntigravityDiscoveredWireModels(prov.baseUrl, antigravity, {
provider: name,
cacheGeneration,
});
markProviderDiscoveryOk(name, live.length);
return observed(withConfiguredRetention(forCache, { warnDrops: true }), "authoritative");
}
Expand Down
129 changes: 112 additions & 17 deletions src/providers/antigravity-models.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { isValidModelDiscoveryModelId, MODEL_DISCOVERY_MAX_MODELS } from "./model-discovery-limits";
import { isModelCacheGenerationCurrent } from "../codex/model-cache";

// Google Antigravity (Cloud Code Assist) bundled model list.
//
Expand Down Expand Up @@ -74,8 +75,12 @@ const ANTIGRAVITY_DISCOVERY_EFFORTS = ["low", "medium", "high"] as const;

function pickerModelIdForDiscoveredWireId(
wireId: string,
info: Record<string, unknown>,
available: ReadonlyMap<string, Record<string, unknown>>,
): string {
const displayModelId = antigravityDisplayModelId(info.displayName, wireId);
if (displayModelId) return displayModelId;

const explicitPickerId = Object.hasOwn(ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID, wireId)
? ANTIGRAVITY_PICKER_MODEL_BY_WIRE_ID[wireId]
: undefined;
Expand Down Expand Up @@ -240,6 +245,8 @@ export const ANTIGRAVITY_MODEL_INPUT_MODALITIES: Record<string, string[]> = {

export interface AntigravityAvailableModel {
id: string;
/** CCA model id used by the agent envelope when `id` comes from display metadata. */
wireModelId: string;
contextWindow?: number;
inputModalities?: string[];
}
Expand All @@ -254,6 +261,86 @@ function antigravityPositiveInteger(value: unknown): number | undefined {
return typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : undefined;
}

interface DiscoveredWireModelMapping {
readonly models: ReadonlyMap<string, string>;
readonly generation?: { provider: string; cacheGeneration: string };
}

const discoveredWireModelsByBaseUrl = new Map<string, DiscoveredWireModelMapping>();

function antigravityBaseUrlKey(baseUrl: string | undefined): string | undefined {
if (typeof baseUrl !== "string" || !baseUrl.trim()) return undefined;
const trimmed = baseUrl.trim().replace(/\/+$/, "");
try {
const url = new URL(trimmed);
url.hash = "";
url.search = "";
return url.toString().replace(/\/+$/, "").toLowerCase();
} catch {
return trimmed.toLowerCase();
}
}

/** Remember the wire ids returned by one live CCA discovery for request routing. */
export function registerAntigravityDiscoveredWireModels(
baseUrl: string | undefined,
models: readonly AntigravityAvailableModel[],
generation?: { provider: string; cacheGeneration: string },
): void {
const key = antigravityBaseUrlKey(baseUrl);
if (!key) return;
const wireModels = new Map<string, string>();
for (const model of models) wireModels.set(model.id, model.wireModelId);
discoveredWireModelsByBaseUrl.set(key, {
models: wireModels,
...(generation ? { generation } : {}),
});
}

function discoveredAntigravityWireModelId(
modelId: string,
baseUrl: string | undefined,
): string | undefined {
const key = antigravityBaseUrlKey(baseUrl);
if (!key) return undefined;
const mapping = discoveredWireModelsByBaseUrl.get(key);
if (!mapping) return undefined;
if (mapping.generation
&& !isModelCacheGenerationCurrent(mapping.generation.provider, mapping.generation.cacheGeneration)) {
discoveredWireModelsByBaseUrl.delete(key);
return undefined;
}
return mapping.models.get(modelId);
}

/**
* Convert the CCA display label used by `agy` into its public model selector.
*
* The wire id is authoritative for requests, while the label is authoritative for the
* user-facing selector when Google has renamed or re-tiered a model. Keep both instead
* of maintaining a provider-specific list of known model names.
*/
function antigravityDisplayModelId(displayName: unknown, wireId: string): string | undefined {
if (typeof displayName !== "string") return undefined;
const label = displayName.trim();
if (!label || label.length > 512) return undefined;
const slug = (replaceDots: boolean): string => label
.normalize("NFKC")
.toLowerCase()
.replace(replaceDots ? /\./g : /\s+/g, replaceDots ? "-" : " ")
.replace(/[^a-z0-9.-]+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "");
const preserved = slug(false);
const compact = slug(true);
if (!isValidModelDiscoveryModelId(preserved) && !isValidModelDiscoveryModelId(compact)) return undefined;
if (preserved === wireId || compact === wireId
|| preserved === `${wireId}-thinking` || compact === `${wireId}-thinking`) {
return wireId;
}
return isValidModelDiscoveryModelId(preserved) ? preserved : compact;
}

/**
* Extract the CCA models that are valid for agent requests. The endpoint also returns tab,
* command, commit-message, transcription, and standalone image-generation models; those are not
Expand Down Expand Up @@ -288,13 +375,6 @@ export function parseAntigravityAvailableModels(
}
}
}
// This model is exposed by Antigravity's agent chat surface even though it is grouped under
// image generation in the discovery response.
if (Array.isArray(body.imageGenerationModelIds)
&& body.imageGenerationModelIds.includes("gemini-3.1-flash-image")) {
if (ids.length >= limit) return null;
ids.push("gemini-3.1-flash-image");
}
// Newer CCA responses identify tiered Flash models through this index instead of
// adding their synthetic wire ids to agentModelSorts.
const tieredModelIds = antigravityRecord(body.tieredModelIds);
Expand All @@ -305,6 +385,12 @@ export function parseAntigravityAvailableModels(
|| !Object.hasOwn(models, id)
|| !antigravityRecord(models[id])
|| ids.length >= limit) return null;
const baseId = id.endsWith("-tiered") ? id.slice(0, -"-tiered".length) : id;
if (ids.some(agentId =>
agentId === id
|| agentId === baseId
|| ANTIGRAVITY_DISCOVERY_EFFORTS.some(effort => agentId === `${baseId}-${effort}`)
)) continue;
ids.push(id);
}
}
Expand All @@ -313,23 +399,18 @@ export function parseAntigravityAvailableModels(
for (const wireId of ids) {
const info = antigravityRecord(models[wireId]);
if (!info || available.has(wireId)) continue;
// Legacy compatibility aliases are deliberately routed to newer wire ids for saved
// selections. They are not safe as independently discovered picker rows.
const alias = Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, wireId)
? ANTIGRAVITY_MODEL_ALIASES[wireId]
: undefined;
if (alias && alias !== wireId) continue;
available.set(wireId, info);
}

const out: AntigravityAvailableModel[] = [];
const seen = new Set<string>();
for (const [wireId, info] of available) {
const id = pickerModelIdForDiscoveredWireId(wireId, available);
const id = pickerModelIdForDiscoveredWireId(wireId, info, available);
if (seen.has(id)) continue;
seen.add(id);
out.push({
id,
wireModelId: wireId,
...(antigravityPositiveInteger(info.maxTokens) ? { contextWindow: antigravityPositiveInteger(info.maxTokens) } : {}),
// Tri-state, deliberately not a ternary: `true` asserts image support,
// `false` asserts against it, and ABSENT is unknown. Collapsing absent into
Expand All @@ -347,7 +428,9 @@ export function parseAntigravityAvailableModels(
return out;
}

export function resolveAntigravityWireModelId(modelId: string): string {
export function resolveAntigravityWireModelId(modelId: string, baseUrl?: string): string {
const discovered = discoveredAntigravityWireModelId(modelId, baseUrl);
if (discovered) return discovered;
return Object.hasOwn(ANTIGRAVITY_MODEL_ALIASES, modelId)
? ANTIGRAVITY_MODEL_ALIASES[modelId]
: modelId;
Expand Down Expand Up @@ -380,7 +463,19 @@ export function retiredAntigravityFlashTier(modelId: string): string | undefined
export function resolveAntigravityEffortWireModel(
modelId: string,
effort?: string,
baseUrl?: string,
): { wireModelId: string; thinkingLevel?: string } {
const discoveredWireModelId = discoveredAntigravityWireModelId(modelId, baseUrl);
if (discoveredWireModelId && (discoveredWireModelId !== modelId || isAntigravitySuffixModelId(modelId))) {
const defaultLevel = ANTIGRAVITY_THINKING_LEVEL_MODELS[modelId];
return {
wireModelId: discoveredWireModelId,
...(defaultLevel
? { thinkingLevel: effort ? resolveAntigravityThinkingLevel(effort) ?? defaultLevel : defaultLevel }
: {}),
};
}
Comment thread
iF2007 marked this conversation as resolved.

// Rule 0: retired Flash id — Google has taken the wire id offline, so route to the
// current generation and carry the tier the retired id encoded. This runs BEFORE the
// suffix check because those ids are aliases, and rule 1 would drop the tier.
Expand All @@ -394,7 +489,7 @@ export function resolveAntigravityEffortWireModel(

// Rule 1: suffix/compat alias — suffix IS the effort.
if (isAntigravitySuffixModelId(modelId)) {
return { wireModelId: resolveAntigravityWireModelId(modelId) };
return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
}

// Rule 1b: single-wire-id Gemini model whose tiers ride on thinkingLevel. Without
Expand Down Expand Up @@ -424,7 +519,7 @@ export function resolveAntigravityEffortWireModel(
}

// Rule 5: everything else.
return { wireModelId: resolveAntigravityWireModelId(modelId) };
return { wireModelId: resolveAntigravityWireModelId(modelId, baseUrl) };
}


Expand Down
14 changes: 8 additions & 6 deletions tests/gemini-37-flash-migration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,8 @@ describe("3.7 reasoning control", () => {
});
});

describe("stale discovery cannot republish a retired model", () => {
test("a CCA payload still listing 3.6 tiers yields no retired picker row", () => {
describe("live discovery follows the CCA agent catalog", () => {
test("a CCA payload still listing 3.6 tiers preserves those live rows", () => {
const payload = {
models: Object.fromEntries(
["gemini-3.6-flash-low", "gemini-3.6-flash-medium", "gemini-3.6-flash-high", "gemini-3.7-flash"]
Expand All @@ -124,10 +124,12 @@ describe("stale discovery cannot republish a retired model", () => {
}],
};
const ids = parseAntigravityAvailableModels(payload)?.map(model => model.id) ?? [];
expect(ids).toContain("gemini-3.7-flash");
for (const retired of Object.keys(RETIRED_TIERS)) {
expect(ids).not.toContain(retired);
}
expect(ids).toEqual([
"gemini-3.6-flash-low",
"gemini-3.6-flash-medium",
"gemini-3.6-flash-high",
"gemini-3.7-flash",
]);
});
});

Expand Down
Loading
Loading