From 036eb75a308f2aafe000c58230b531d29f538cec Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 10 May 2026 08:20:00 +0100 Subject: [PATCH] fix: normalize merged gemini model config --- CHANGELOG.md | 1 + ...els-config.applies-config-env-vars.test.ts | 58 +++++++++++++++++++ src/agents/models-config.plan.ts | 7 ++- .../models-config.providers.normalize.ts | 20 +++++++ src/agents/models-config.providers.ts | 5 +- 5 files changed, 88 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b4df103eab..ff45005d72f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -65,6 +65,7 @@ Docs: https://docs.openclaw.ai - OpenAI/Codex: point gateway missing-key recovery and wizard docs at the canonical `openai/gpt-5.5` plus Codex OAuth route, and fix trajectory export errors so they suggest the valid `openclaw sessions` command. - Google/Gemini: normalize retired `google/gemini-3-pro-preview` primary, fallback, and model-map refs during config load and unrelated config writes so saved config keeps targeting Gemini 3.1 Pro Preview. - Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside emitted Google provider model config, so regenerated models.json rows test `google/gemini-3.1-pro-preview`. +- Google/Gemini: normalize retired Gemini 3 Pro Preview ids preserved from existing merged models.json providers so config emission keeps targeting `google/gemini-3.1-pro-preview`. - GitHub Copilot: mint short-lived Copilot API tokens with the same `vscode-chat` integration identity used by runtime requests, and refresh legacy cached tokens missing that identity so image-capable Copilot models no longer inherit the `copilot-language-server` scope. Fixes #79946, #80074. Thanks @TurboTheTurtle. - Plugins/doctor: drop stale managed npm install records when `openclaw doctor --fix` removes npm packages that shadow bundled plugins, so the rebuilt registry no longer resurrects the removed package metadata. - Discord/voice: reuse or suppress late realtime consult tool calls without stealing newer speaker context or speaking forced fallback answers twice. diff --git a/src/agents/models-config.applies-config-env-vars.test.ts b/src/agents/models-config.applies-config-env-vars.test.ts index bd036dcfb2d2..447dc16db91f 100644 --- a/src/agents/models-config.applies-config-env-vars.test.ts +++ b/src/agents/models-config.applies-config-env-vars.test.ts @@ -188,6 +188,64 @@ describe("models-config", () => { expect(observedSnapshot).toBe(pluginMetadataSnapshot); }); + it("normalizes retired Gemini ids preserved from existing models.json rows", async () => { + const plan = await planOpenClawModelsJsonWithDeps( + { + cfg: { models: { mode: "merge", providers: {} } }, + agentDir: "/tmp/openclaw-models-config-env-vars-test", + env: {}, + existingRaw: "", + existingParsed: { + providers: { + google: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + api: "google-generative-ai", + apiKey: "GOOGLE_API_KEY", // pragma: allowlist secret + models: [ + { + id: "gemini-3-pro-preview", + name: "Gemini 3 Pro", + input: ["text"], + }, + ], + }, + }, + }, + }, + { + resolveImplicitProviders: async () => ({ + openai: { + baseUrl: "https://api.openai.com/v1", + api: "openai-responses", + apiKey: "OPENAI_API_KEY", // pragma: allowlist secret + models: [ + { + id: "gpt-5.5", + name: "GPT-5.5", + input: ["text"], + reasoning: true, + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 400000, + maxTokens: 128000, + }, + ], + }, + }), + }, + ); + + expect(plan.action).toBe("write"); + if (plan.action !== "write") { + throw new Error("Expected models.json write plan"); + } + const parsed = JSON.parse(plan.contents) as { + providers?: Record }>; + }; + expect(parsed.providers?.google?.models?.map((model) => model.id)).toEqual([ + "gemini-3.1-pro-preview", + ]); + }); + it("uses config env.vars entries for implicit provider discovery without mutating process.env", async () => { await withTempEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR], async () => { unsetEnv(["OPENROUTER_API_KEY", TEST_ENV_VAR]); diff --git a/src/agents/models-config.plan.ts b/src/agents/models-config.plan.ts index f5fdefb3a646..f21dcb30067b 100644 --- a/src/agents/models-config.plan.ts +++ b/src/agents/models-config.plan.ts @@ -9,6 +9,7 @@ import { import { applyNativeStreamingUsageCompat, enforceSourceManagedProviderSecrets, + normalizeProviderCatalogModelsForConfig, normalizeProviders, resolveImplicitProviders, type ProviderConfig, @@ -167,13 +168,15 @@ export async function planOpenClawModelsJsonWithDeps( providers: normalizedProviders, secretRefManagedProviders, }); + const normalizedMergedProviders = + normalizeProviderCatalogModelsForConfig(mergedProviders) ?? mergedProviders; const secretEnforcedProviders = enforceSourceManagedProviderSecrets({ - providers: mergedProviders, + providers: normalizedMergedProviders, sourceProviders: params.sourceConfigForSecrets?.models?.providers, sourceSecretDefaults: params.sourceConfigForSecrets?.secrets?.defaults, secretRefManagedProviders, - }) ?? mergedProviders; + }) ?? normalizedMergedProviders; const finalProviders = applyNativeStreamingUsageCompat(secretEnforcedProviders); const nextContents = `${JSON.stringify({ providers: finalProviders }, null, 2)}\n`; diff --git a/src/agents/models-config.providers.normalize.ts b/src/agents/models-config.providers.normalize.ts index 0de3801a85a9..8de920d242ca 100644 --- a/src/agents/models-config.providers.normalize.ts +++ b/src/agents/models-config.providers.normalize.ts @@ -84,6 +84,26 @@ function normalizeProviderModelsForConfig( : { provider, mutated }; } +export function normalizeProviderCatalogModelsForConfig( + providers: ModelsConfig["providers"], +): ModelsConfig["providers"] { + if (!providers) { + return providers; + } + + let mutated = false; + const next: Record = {}; + for (const [providerKey, provider] of Object.entries(providers)) { + const normalized = normalizeProviderModelsForConfig(providerKey, provider); + if (normalized.mutated) { + mutated = true; + } + next[providerKey] = normalized.provider; + } + + return mutated ? next : providers; +} + export function normalizeProviders(params: { providers: ModelsConfig["providers"]; agentDir: string; diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 44664601979b..9ffe9ca3b859 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -1,5 +1,8 @@ export { resolveImplicitProviders } from "./models-config.providers.implicit.js"; -export { normalizeProviders } from "./models-config.providers.normalize.js"; +export { + normalizeProviderCatalogModelsForConfig, + normalizeProviders, +} from "./models-config.providers.normalize.js"; export type { ProviderConfig } from "./models-config.providers.secrets.js"; export { applyNativeStreamingUsageCompat } from "./models-config.providers.policy.js"; export { enforceSourceManagedProviderSecrets } from "./models-config.providers.source-managed.js";