From 320b62265d560bca3bd9523d92388bf8fbc02a6f Mon Sep 17 00:00:00 2001 From: Phineas1500 <41450967+Phineas1500@users.noreply.github.com> Date: Sun, 22 Feb 2026 19:11:39 -0500 Subject: [PATCH] fix(models): synthesize antigravity Gemini 3.1 pro high/low models (#22899) * Models: add antigravity Gemini 3.1 forward-compat * models: propagate availability to Gemini 3.1 dot IDs * test(models): format Gemini 3.1 forward-compat test * test(models): type Gemini 3.1 forward-compat fixtures * models: add changelog note for antigravity gemini 3.1 forward-compat --------- Co-authored-by: Vincent Koc --- CHANGELOG.md | 1 + ...orward-compat.antigravity-gemini31.test.ts | 72 ++++++++++++++ src/agents/model-forward-compat.ts | 58 +++++++++++- src/commands/models.list.test.ts | 93 +++++++++++++++++++ src/commands/models/list.registry.ts | 14 ++- 5 files changed, 236 insertions(+), 2 deletions(-) create mode 100644 src/agents/model-forward-compat.antigravity-gemini31.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bc7f9e97a11..7cd44d31060 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -701,6 +701,7 @@ Docs: https://docs.openclaw.ai - Agents/Context: apply configured model `contextWindow` overrides after provider discovery so `lookupContextTokens()` honors operator config values (including discovery-failure paths). (#17404) Thanks @michaelbship and @vignesh07. - Agents/Context: derive `lookupContextTokens()` from auth-available model metadata and keep the smallest discovered context window for duplicate model ids, preventing cross-provider cache collisions from overestimating session context limits. (#17586) Thanks @githabideri and @vignesh07. - Agents/OpenAI: force `store=true` for direct OpenAI Responses/Codex runs to preserve multi-turn server-side conversation state, while leaving proxy/non-OpenAI endpoints unchanged. (#16803) Thanks @mark9232 and @vignesh07. +- Models/Antigravity: synthesize Gemini 3.1 Pro high/low model IDs (dash and dot forms) from Gemini 3 templates so `models list` and `/model` remain usable while upstream catalogs catch up. (#22492) Thanks @Phineas1500. - Memory/FTS: make `buildFtsQuery` Unicode-aware so non-ASCII queries (including CJK) produce keyword tokens instead of falling back to vector-only search. (#17672) Thanks @KinGP5471. - Auto-reply/Compaction: resolve `memory/YYYY-MM-DD.md` placeholders with timezone-aware runtime dates and append a `Current time:` line to memory-flush turns, preventing wrong-year memory filenames without making the system prompt time-variant. (#17603, #17633) Thanks @nicholaspapadam-wq and @vignesh07. - Auth/Cooldowns: auto-expire stale auth profile cooldowns when `cooldownUntil` or `disabledUntil` timestamps have passed, and reset `errorCount` so the next transient failure does not immediately escalate to a disproportionately long cooldown. Handles `cooldownUntil` and `disabledUntil` independently. (#3604) Thanks @nabbilkhan. diff --git a/src/agents/model-forward-compat.antigravity-gemini31.test.ts b/src/agents/model-forward-compat.antigravity-gemini31.test.ts new file mode 100644 index 00000000000..256d20cbf34 --- /dev/null +++ b/src/agents/model-forward-compat.antigravity-gemini31.test.ts @@ -0,0 +1,72 @@ +import type { Api, Model } from "@mariozechner/pi-ai"; +import { describe, expect, it } from "vitest"; +import { resolveForwardCompatModel } from "./model-forward-compat.js"; +import type { ModelRegistry } from "./pi-model-discovery.js"; + +function makeRegistry(): ModelRegistry { + const templates = new Map>(); + templates.set("google-antigravity/gemini-3-pro-high", { + id: "gemini-3-pro-high", + name: "Gemini 3 Pro High", + provider: "google-antigravity", + api: "google-antigravity", + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 64000, + reasoning: true, + } as Model); + templates.set("google-antigravity/gemini-3-pro-low", { + id: "gemini-3-pro-low", + name: "Gemini 3 Pro Low", + provider: "google-antigravity", + api: "google-antigravity", + input: ["text", "image"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 200000, + maxTokens: 64000, + reasoning: true, + } as Model); + + const registry = { + find: (provider: string, modelId: string) => templates.get(`${provider}/${modelId}`) ?? null, + } as unknown as ModelRegistry; + return registry; +} + +describe("resolveForwardCompatModel (google-antigravity Gemini 3.1)", () => { + it("resolves gemini-3-1-pro-high from gemini-3-pro-high template", () => { + const model = resolveForwardCompatModel( + "google-antigravity", + "gemini-3-1-pro-high", + makeRegistry(), + ); + expect(model?.provider).toBe("google-antigravity"); + expect(model?.id).toBe("gemini-3-1-pro-high"); + }); + + it("resolves gemini-3-1-pro-low from gemini-3-pro-low template", () => { + const model = resolveForwardCompatModel( + "google-antigravity", + "gemini-3-1-pro-low", + makeRegistry(), + ); + expect(model?.provider).toBe("google-antigravity"); + expect(model?.id).toBe("gemini-3-1-pro-low"); + }); + + it("supports dot-notation model ids", () => { + const high = resolveForwardCompatModel( + "google-antigravity", + "gemini-3.1-pro-high", + makeRegistry(), + ); + const low = resolveForwardCompatModel( + "google-antigravity", + "gemini-3.1-pro-low", + makeRegistry(), + ); + expect(high?.id).toBe("gemini-3.1-pro-high"); + expect(low?.id).toBe("gemini-3.1-pro-low"); + }); +}); diff --git a/src/agents/model-forward-compat.ts b/src/agents/model-forward-compat.ts index 600b52a01ee..93e6a57b855 100644 --- a/src/agents/model-forward-compat.ts +++ b/src/agents/model-forward-compat.ts @@ -26,6 +26,12 @@ const ANTIGRAVITY_OPUS_THINKING_TEMPLATE_MODEL_IDS = [ "claude-opus-4-5-thinking", "claude-opus-4.5-thinking", ] as const; +const ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID = "gemini-3-1-pro-high"; +const ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID = "gemini-3.1-pro-high"; +const ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID = "gemini-3-1-pro-low"; +const ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID = "gemini-3.1-pro-low"; +const ANTIGRAVITY_GEMINI_31_PRO_HIGH_TEMPLATE_MODEL_IDS = ["gemini-3-pro-high"] as const; +const ANTIGRAVITY_GEMINI_31_PRO_LOW_TEMPLATE_MODEL_IDS = ["gemini-3-pro-low"] as const; export const ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES = [ { @@ -34,10 +40,25 @@ export const ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES = [ "google-antigravity/claude-opus-4-5-thinking", "google-antigravity/claude-opus-4.5-thinking", ], + availabilityAliasIds: [] as const, }, { id: ANTIGRAVITY_OPUS_46_MODEL_ID, templatePrefixes: ["google-antigravity/claude-opus-4-5", "google-antigravity/claude-opus-4.5"], + availabilityAliasIds: [] as const, + }, +] as const; + +export const ANTIGRAVITY_GEMINI_31_FORWARD_COMPAT_CANDIDATES = [ + { + id: ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID, + templatePrefixes: ["google-antigravity/gemini-3-pro-high"], + availabilityAliasIds: [ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID], + }, + { + id: ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID, + templatePrefixes: ["google-antigravity/gemini-3-pro-low"], + availabilityAliasIds: [ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID], }, ] as const; @@ -278,6 +299,40 @@ function resolveAntigravityOpus46ForwardCompatModel( }); } +function resolveAntigravityGemini31ForwardCompatModel( + provider: string, + modelId: string, + modelRegistry: ModelRegistry, +): Model | undefined { + const normalizedProvider = normalizeProviderId(provider); + if (normalizedProvider !== "google-antigravity") { + return undefined; + } + + const trimmedModelId = modelId.trim(); + const lower = trimmedModelId.toLowerCase(); + const isGemini31High = + lower === ANTIGRAVITY_GEMINI_31_PRO_HIGH_MODEL_ID || + lower === ANTIGRAVITY_GEMINI_31_PRO_DOT_HIGH_MODEL_ID; + const isGemini31Low = + lower === ANTIGRAVITY_GEMINI_31_PRO_LOW_MODEL_ID || + lower === ANTIGRAVITY_GEMINI_31_PRO_DOT_LOW_MODEL_ID; + if (!isGemini31High && !isGemini31Low) { + return undefined; + } + + const templateIds = isGemini31High + ? [...ANTIGRAVITY_GEMINI_31_PRO_HIGH_TEMPLATE_MODEL_IDS] + : [...ANTIGRAVITY_GEMINI_31_PRO_LOW_TEMPLATE_MODEL_IDS]; + + return cloneFirstTemplateModel({ + normalizedProvider, + trimmedModelId, + templateIds, + modelRegistry, + }); +} + export function resolveForwardCompatModel( provider: string, modelId: string, @@ -288,6 +343,7 @@ export function resolveForwardCompatModel( resolveAnthropicOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveAnthropicSonnet46ForwardCompatModel(provider, modelId, modelRegistry) ?? resolveZaiGlm5ForwardCompatModel(provider, modelId, modelRegistry) ?? - resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) + resolveAntigravityOpus46ForwardCompatModel(provider, modelId, modelRegistry) ?? + resolveAntigravityGemini31ForwardCompatModel(provider, modelId, modelRegistry) ); } diff --git a/src/commands/models.list.test.ts b/src/commands/models.list.test.ts index 5b1f6f44547..8e9df0035b4 100644 --- a/src/commands/models.list.test.ts +++ b/src/commands/models.list.test.ts @@ -389,6 +389,99 @@ describe("models list/status", () => { }, ); + it.each([ + { + name: "high", + configuredModelId: "gemini-3-1-pro-high", + templateId: "gemini-3-pro-high", + templateName: "Gemini 3 Pro High", + expectedKey: "google-antigravity/gemini-3-1-pro-high", + }, + { + name: "low", + configuredModelId: "gemini-3-1-pro-low", + templateId: "gemini-3-pro-low", + templateName: "Gemini 3 Pro Low", + expectedKey: "google-antigravity/gemini-3-1-pro-low", + }, + ] as const)( + "models list resolves antigravity gemini 3.1 $name from gemini 3 template", + async ({ configuredModelId, templateId, templateName, expectedKey }) => { + const payload = await runGoogleAntigravityListCase({ + configuredModelId, + templateId, + templateName, + }); + expectAntigravityModel(payload, { + key: expectedKey, + available: false, + includesTags: true, + }); + }, + ); + + it.each([ + { + name: "high", + configuredModelId: "gemini-3-1-pro-high", + templateId: "gemini-3-pro-high", + templateName: "Gemini 3 Pro High", + expectedKey: "google-antigravity/gemini-3-1-pro-high", + }, + { + name: "low", + configuredModelId: "gemini-3-1-pro-low", + templateId: "gemini-3-pro-low", + templateName: "Gemini 3 Pro Low", + expectedKey: "google-antigravity/gemini-3-1-pro-low", + }, + ] as const)( + "models list marks synthesized antigravity gemini 3.1 $name as available when template is available", + async ({ configuredModelId, templateId, templateName, expectedKey }) => { + const payload = await runGoogleAntigravityListCase({ + configuredModelId, + templateId, + templateName, + available: true, + }); + expectAntigravityModel(payload, { + key: expectedKey, + available: true, + }); + }, + ); + + it.each([ + { + name: "high", + configuredModelId: "gemini-3.1-pro-high", + templateId: "gemini-3-pro-high", + templateName: "Gemini 3 Pro High", + expectedKey: "google-antigravity/gemini-3.1-pro-high", + }, + { + name: "low", + configuredModelId: "gemini-3.1-pro-low", + templateId: "gemini-3-pro-low", + templateName: "Gemini 3 Pro Low", + expectedKey: "google-antigravity/gemini-3.1-pro-low", + }, + ] as const)( + "models list marks dot-notation antigravity gemini 3.1 $name as available when template is available", + async ({ configuredModelId, templateId, templateName, expectedKey }) => { + const payload = await runGoogleAntigravityListCase({ + configuredModelId, + templateId, + templateName, + available: true, + }); + expectAntigravityModel(payload, { + key: expectedKey, + available: true, + }); + }, + ); + it("models list prefers registry availability over provider auth heuristics", async () => { const payload = await runGoogleAntigravityListCase({ configuredModelId: "claude-opus-4-6-thinking", diff --git a/src/commands/models/list.registry.ts b/src/commands/models/list.registry.ts index 42f75ca1bb9..b0daded3db7 100644 --- a/src/commands/models/list.registry.ts +++ b/src/commands/models/list.registry.ts @@ -8,6 +8,7 @@ import { resolveEnvApiKey, } from "../../agents/model-auth.js"; import { + ANTIGRAVITY_GEMINI_31_FORWARD_COMPAT_CANDIDATES, ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES, resolveForwardCompatModel, } from "../../agents/model-forward-compat.js"; @@ -117,6 +118,9 @@ export async function loadModelRegistry(cfg: OpenClawConfig) { for (const synthesized of synthesizedForwardCompat) { if (hasAvailableTemplate(availableKeys, synthesized.templatePrefixes)) { availableKeys.add(synthesized.key); + for (const aliasKey of synthesized.availabilityAliasKeys) { + availableKeys.add(aliasKey); + } } } } catch (err) { @@ -137,6 +141,7 @@ export async function loadModelRegistry(cfg: OpenClawConfig) { type SynthesizedForwardCompat = { key: string; templatePrefixes: readonly string[]; + availabilityAliasKeys: readonly string[]; }; function appendAntigravityForwardCompatModels( @@ -145,8 +150,12 @@ function appendAntigravityForwardCompatModels( ): { models: Model[]; synthesizedForwardCompat: SynthesizedForwardCompat[] } { const nextModels = [...models]; const synthesizedForwardCompat: SynthesizedForwardCompat[] = []; + const candidates = [ + ...ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES, + ...ANTIGRAVITY_GEMINI_31_FORWARD_COMPAT_CANDIDATES, + ]; - for (const candidate of ANTIGRAVITY_OPUS_46_FORWARD_COMPAT_CANDIDATES) { + for (const candidate of candidates) { const key = modelKey("google-antigravity", candidate.id); const hasForwardCompat = nextModels.some((model) => modelKey(model.provider, model.id) === key); if (hasForwardCompat) { @@ -162,6 +171,9 @@ function appendAntigravityForwardCompatModels( synthesizedForwardCompat.push({ key, templatePrefixes: candidate.templatePrefixes, + availabilityAliasKeys: candidate.availabilityAliasIds.map((id) => + modelKey("google-antigravity", id), + ), }); }