From d4eb40248a8ea1c6f8c76b1969b812d4e6d85b43 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 06:07:01 +0100 Subject: [PATCH] fix: normalize gemini 3 pro preview config --- CHANGELOG.md | 1 + extensions/google/manifest.test.ts | 18 ++++++++++ extensions/google/openclaw.plugin.json | 10 ++++++ src/agents/model-ref-shared.ts | 21 ++++++++---- src/agents/model-selection.test.ts | 12 +++++++ src/commands/model-picker.test.ts | 33 +++++++++++++++++-- src/flows/model-picker.ts | 14 ++++---- .../provider-model-id-normalize.test.ts | 1 + src/plugin-sdk/provider-model-id-normalize.ts | 2 +- 9 files changed, 96 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6b0de993b4..4d686d2bdc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Changes +- Google/Gemini: normalize retired `google/gemini-3-pro-preview` catalog selections to `google/gemini-3.1-pro-preview` before they are written to model config. - Control UI: read the Quick Settings exec policy badge from `tools.exec.security` instead of the non-schema `agents.defaults.exec.security` path, so configured `full`/`deny` values render accurately. Fixes #78311. Thanks @FriedBack. - Control UI/usage: add transcript-backed historical lineage rollups for rotated logical sessions, with current-instance vs historical-lineage scope controls and long-range presets so usage history stays visible after restarts and updates. Fixes #50701. Thanks @dev-gideon-llc and @BunsDev. - Agents/failover: harden state-aware lane suspension by persisting quota resume transitions, restoring configured lane concurrency, preserving non-quota failure reasons, and exporting model failover events through diagnostics OTLP. Thanks @BunsDev. diff --git a/extensions/google/manifest.test.ts b/extensions/google/manifest.test.ts index 0d16b6d7db1..663ad4edaa7 100644 --- a/extensions/google/manifest.test.ts +++ b/extensions/google/manifest.test.ts @@ -2,6 +2,14 @@ import { readFileSync } from "node:fs"; import { describe, expect, it } from "vitest"; type GoogleManifest = { + modelIdNormalization?: { + providers?: Record< + string, + { + aliases?: Record; + } + >; + }; modelCatalog?: { suppressions?: Array<{ provider?: string; @@ -83,4 +91,14 @@ describe("google manifest model catalog", () => { expect(suppressionRefs).not.toContain("google/gemini-2.5-pro"); expect(suppressionRefs).not.toContain("google/gemini-3.1-pro-preview"); }); + + it("normalizes retired Gemini 3 Pro aliases for all Google chat providers", () => { + const manifest = loadManifest(); + + for (const provider of GOOGLE_CHAT_PROVIDERS) { + expect(manifest.modelIdNormalization?.providers?.[provider]?.aliases).toMatchObject({ + "gemini-3-pro": "gemini-3.1-pro-preview", + }); + } + }); }); diff --git a/extensions/google/openclaw.plugin.json b/extensions/google/openclaw.plugin.json index 3289d21678d..24111760b7c 100644 --- a/extensions/google/openclaw.plugin.json +++ b/extensions/google/openclaw.plugin.json @@ -18,6 +18,16 @@ "gemini-3.1-flash-preview": "gemini-3-flash-preview" } }, + "google-gemini-cli": { + "aliases": { + "gemini-3-pro": "gemini-3.1-pro-preview", + "gemini-3-flash": "gemini-3-flash-preview", + "gemini-3.1-pro": "gemini-3.1-pro-preview", + "gemini-3.1-flash-lite": "gemini-3.1-flash-lite-preview", + "gemini-3.1-flash": "gemini-3-flash-preview", + "gemini-3.1-flash-preview": "gemini-3-flash-preview" + } + }, "google-vertex": { "aliases": { "gemini-3-pro": "gemini-3.1-pro-preview", diff --git a/src/agents/model-ref-shared.ts b/src/agents/model-ref-shared.ts index 501436ce8ad..cd54bcb4170 100644 --- a/src/agents/model-ref-shared.ts +++ b/src/agents/model-ref-shared.ts @@ -1,3 +1,4 @@ +import { normalizeGooglePreviewModelId } from "../plugin-sdk/provider-model-id-normalize.js"; import { normalizeProviderModelIdWithManifest } from "../plugins/manifest-model-id-normalization.js"; import type { PluginManifestRecord } from "../plugins/manifest-registry.js"; import { normalizeLowercaseStringOrEmpty } from "../shared/string-coerce.js"; @@ -32,19 +33,27 @@ export function normalizeStaticProviderModelId( manifestPlugins?: readonly Pick[]; } = {}, ): string { + const normalizedProvider = normalizeProviderId(provider); if (options.allowManifestNormalization === false) { - return model; + return normalizeBuiltInProviderModelId(normalizedProvider, model); } - return ( + const manifestModelId = normalizeProviderModelIdWithManifest({ - provider, + provider: normalizedProvider, plugins: options.manifestPlugins, context: { - provider, + provider: normalizedProvider, modelId: model, }, - }) ?? model - ); + }) ?? model; + return normalizeBuiltInProviderModelId(normalizedProvider, manifestModelId); +} + +function normalizeBuiltInProviderModelId(provider: string, model: string): string { + if (provider === "google" || provider === "google-gemini-cli" || provider === "google-vertex") { + return normalizeGooglePreviewModelId(model); + } + return model; } function parseStaticModelRef(raw: string, defaultProvider: string): StaticModelRef | null { diff --git a/src/agents/model-selection.test.ts b/src/agents/model-selection.test.ts index 78a1d6d6e7f..8d71de4de09 100644 --- a/src/agents/model-selection.test.ts +++ b/src/agents/model-selection.test.ts @@ -311,6 +311,18 @@ describe("model-selection", () => { defaultProvider: "google", expected: { provider: "google", model: "gemini-3-flash-preview" }, }, + { + name: "normalizes retired google gemini 3 pro preview ids", + variants: ["google/gemini-3-pro-preview", "gemini-3-pro-preview"], + defaultProvider: "google", + expected: { provider: "google", model: "gemini-3.1-pro-preview" }, + }, + { + name: "normalizes retired gemini cli 3 pro preview ids", + variants: ["google-gemini-cli/gemini-3-pro-preview"], + defaultProvider: "google", + expected: { provider: "google-gemini-cli", model: "gemini-3.1-pro-preview" }, + }, { name: "normalizes gemini 3.1 flash-lite ids", variants: ["google/gemini-3.1-flash-lite", "gemini-3.1-flash-lite"], diff --git a/src/commands/model-picker.test.ts b/src/commands/model-picker.test.ts index 72f9adfa5f2..0bbbabd1c5b 100644 --- a/src/commands/model-picker.test.ts +++ b/src/commands/model-picker.test.ts @@ -303,11 +303,38 @@ describe("promptDefaultModel", () => { expect(optionValues).toEqual([ "openai/gpt-5.5", "anthropic/claude-sonnet-4-6", - "google/gemini-3-pro-preview", + "google/gemini-3.1-pro-preview", "openai-codex/gpt-5.5", ]); }); + it("normalizes retired Google Gemini catalog rows before saving config", async () => { + loadModelCatalog.mockResolvedValue([ + { provider: "google", id: "gemini-3-pro-preview", name: "Gemini 3 Pro" }, + ]); + + const select = vi.fn(async (params) => params.options[0]?.value as never); + const prompter = makePrompter({ select }); + + const result = await promptDefaultModel({ + config: { agents: { defaults: {} } } as OpenClawConfig, + prompter, + allowKeep: false, + includeManual: false, + ignoreAllowlist: true, + }); + + expect(result.model).toBe("google/gemini-3.1-pro-preview"); + expect(select.mock.calls[0]?.[0]?.options).toEqual([ + expect.objectContaining({ value: "google/gemini-3.1-pro-preview" }), + ]); + expect(runProviderModelSelectedHook).toHaveBeenCalledWith( + expect.objectContaining({ + model: "google/gemini-3.1-pro-preview", + }), + ); + }); + it("uses configured provider models for default picker without loading the full catalog in replace mode", async () => { loadModelCatalog.mockResolvedValue([ { provider: "openai", id: "gpt-5.5", name: "GPT-5.5" }, @@ -1268,7 +1295,7 @@ describe("runtime model picker visibility", () => { expect(optionValues).toEqual([ "openai/gpt-5.5", "anthropic/claude-sonnet-4-6", - "google/gemini-3-pro-preview", + "google/gemini-3.1-pro-preview", ]); expect(call?.initialValues).toEqual(["openai/gpt-5.5"]); }); @@ -1504,7 +1531,7 @@ describe("applyModelFallbacksFromSelection", () => { }); expect(next.agents?.defaults?.model).toEqual({ primary: "anthropic/claude-opus-4-6", - fallbacks: ["google/gemini-3-pro-preview"], + fallbacks: ["google/gemini-3.1-pro-preview"], }); }); diff --git a/src/flows/model-picker.ts b/src/flows/model-picker.ts index 2f82facb8c6..87dadaf107c 100644 --- a/src/flows/model-picker.ts +++ b/src/flows/model-picker.ts @@ -13,6 +13,7 @@ import { buildModelAliasIndex, type ModelAliasIndex, modelKey, + normalizeModelRef, normalizeProviderId, resolveConfiguredModelRef, resolveModelRefFromString, @@ -231,11 +232,12 @@ function addModelSelectOption(params: { hasAuth: (provider: string) => boolean; literalPrefixProviders: Set; }) { - const key = modelKey(params.entry.provider, params.entry.id); + const normalizedRef = normalizeModelRef(params.entry.provider, params.entry.id); + const key = modelKey(normalizedRef.provider, normalizedRef.model); if ( params.seen.has(key) || HIDDEN_ROUTER_MODELS.has(key) || - !isModelPickerVisibleProvider(params.entry.provider) + !isModelPickerVisibleProvider(normalizedRef.provider) ) { return; } @@ -253,15 +255,15 @@ function addModelSelectOption(params: { if (aliases?.length) { hints.push(`alias: ${aliases.join(", ")}`); } - const routeHint = resolveModelRouteHint(params.entry.provider); + const routeHint = resolveModelRouteHint(normalizedRef.provider); if (routeHint) { hints.push(routeHint); } - if (!params.hasAuth(params.entry.provider)) { + if (!params.hasAuth(normalizedRef.provider)) { return; } - const label = params.literalPrefixProviders.has(normalizeProviderId(params.entry.provider)) - ? `${params.entry.provider}/${params.entry.id}` + const label = params.literalPrefixProviders.has(normalizeProviderId(normalizedRef.provider)) + ? formatLiteralProviderPrefixedModelRef(normalizedRef.provider, key) : key; params.options.push({ value: key, diff --git a/src/plugin-sdk/provider-model-id-normalize.test.ts b/src/plugin-sdk/provider-model-id-normalize.test.ts index 7b7db29e8ae..062622dbc42 100644 --- a/src/plugin-sdk/provider-model-id-normalize.test.ts +++ b/src/plugin-sdk/provider-model-id-normalize.test.ts @@ -4,6 +4,7 @@ import { normalizeGooglePreviewModelId } from "./provider-model-id-normalize.js" describe("provider model id normalization", () => { it("routes bare Gemini 3 Pro to the current Gemini 3.1 Pro preview", () => { expect(normalizeGooglePreviewModelId("gemini-3-pro")).toBe("gemini-3.1-pro-preview"); + expect(normalizeGooglePreviewModelId("gemini-3-pro-preview")).toBe("gemini-3.1-pro-preview"); expect(normalizeGooglePreviewModelId("gemini-3.1-pro")).toBe("gemini-3.1-pro-preview"); }); diff --git a/src/plugin-sdk/provider-model-id-normalize.ts b/src/plugin-sdk/provider-model-id-normalize.ts index 7e907503ca2..ceb7f0acf82 100644 --- a/src/plugin-sdk/provider-model-id-normalize.ts +++ b/src/plugin-sdk/provider-model-id-normalize.ts @@ -1,7 +1,7 @@ const ANTIGRAVITY_BARE_PRO_IDS = new Set(["gemini-3-pro", "gemini-3.1-pro", "gemini-3-1-pro"]); export function normalizeGooglePreviewModelId(id: string): string { - if (id === "gemini-3-pro") { + if (id === "gemini-3-pro" || id === "gemini-3-pro-preview") { return "gemini-3.1-pro-preview"; } if (id === "gemini-3-flash") {