From e21e83db97eff84fa358984e0fee2ce11327fbce Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 11 May 2026 10:37:44 +0100 Subject: [PATCH] fix(config): normalize Gemini provider catalog writes --- CHANGELOG.md | 1 + src/config/io.write-prepare.test.ts | 58 +++++++++++++++++++++++++++++ src/config/io.write-prepare.ts | 46 ++++++++++++++++++++++- 3 files changed, 104 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d46736a44c..7d349beb547 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai - Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while converting manifest catalog rows into emitted provider config, so `google/gemini-3.1-pro-preview` is used for testing instead of `google/gemini-3-pro-preview`. - Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids in configured proxy/provider-auth model catalogs, so regenerated config keeps testing `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`. - Google/Gemini: normalize retired nested Gemini 3 Pro Preview ids while onboarding provider catalog presets, so setup-emitted proxy configs test `google/gemini-3.1-pro-preview` instead of `google/gemini-3-pro-preview`. +- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows during generic config writes, so unrelated config changes keep testing `google/gemini-3.1-pro-preview`. - Models: keep configured fallback chains ahead of configured primary models for override selections with duplicate model ids, preventing fallback jumps to the wrong provider. Fixes #80562. - Native apps: advertise the Gateway protocol compatibility range so chat and node sessions can connect to v3 gateways after additive v4 client updates. - Gateway/agents: keep stale `sessions_send` ACP manager and `web_fetch` runtime chunks importable after package updates, preventing live gateways from breaking before restart. Fixes #78804. Thanks @Gomesy72. diff --git a/src/config/io.write-prepare.test.ts b/src/config/io.write-prepare.test.ts index 3ecee8cc905..7a5048ce0f4 100644 --- a/src/config/io.write-prepare.test.ts +++ b/src/config/io.write-prepare.test.ts @@ -223,6 +223,64 @@ describe("config io write prepare", () => { expect(persisted.gateway?.port).toBe(18888); }); + it("normalizes retired Google provider catalog refs during unrelated config writes", () => { + const makeModel = (id: string, name: string) => ({ + id, + name, + reasoning: true, + input: ["text" as const], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 1_048_576, + maxTokens: 65_536, + }); + const sourceConfig: OpenClawConfig = { + models: { + providers: { + google: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [makeModel("google/gemini-3-pro-preview", "Gemini 3 Pro")], + }, + kilocode: { + baseUrl: "https://kilocode.test/v1", + models: [makeModel("google/gemini-3-pro-preview", "Gemini via Kilo")], + }, + }, + }, + gateway: { port: 18789 }, + }; + const runtimeConfig: OpenClawConfig = { + models: { + providers: { + google: { + baseUrl: "https://generativelanguage.googleapis.com/v1beta", + models: [makeModel("google/gemini-3.1-pro-preview", "Gemini 3 Pro")], + }, + kilocode: { + baseUrl: "https://kilocode.test/v1", + models: [makeModel("google/gemini-3.1-pro-preview", "Gemini via Kilo")], + }, + }, + }, + gateway: { port: 18789 }, + }; + const persisted = resolvePersistCandidateForWrite({ + runtimeConfig, + sourceConfig, + nextConfig: { + ...runtimeConfig, + gateway: { port: 18888 }, + }, + }) as OpenClawConfig; + + expect(persisted.models?.providers?.google?.models).toEqual([ + makeModel("google/gemini-3.1-pro-preview", "Gemini 3 Pro"), + ]); + expect(persisted.models?.providers?.kilocode?.models).toEqual([ + makeModel("google/gemini-3.1-pro-preview", "Gemini via Kilo"), + ]); + expect(persisted.gateway?.port).toBe(18888); + }); + it("allows explicit unsets to remove authored agent provider params", () => { const sourceConfig: OpenClawConfig = { agents: { diff --git a/src/config/io.write-prepare.ts b/src/config/io.write-prepare.ts index edf10e9a488..ce9ae9ec8cd 100644 --- a/src/config/io.write-prepare.ts +++ b/src/config/io.write-prepare.ts @@ -1,4 +1,5 @@ import { isDeepStrictEqual } from "node:util"; +import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-shared.js"; import { isRecord } from "../utils.js"; import { applyMergePatch } from "./merge-patch.js"; import { normalizeAgentModelMapForConfig, normalizeAgentModelRefForConfig } from "./model-input.js"; @@ -354,6 +355,49 @@ function normalizeAgentDefaultModelRefsForWrite(config: unknown): unknown { return next; } +function normalizeModelProviderCatalogRefsForWrite(config: unknown): unknown { + const providers = getPathValue(config, ["models", "providers"]); + if (!isRecord(providers)) { + return config; + } + + let mutated = false; + const nextProviders: Record = { ...providers }; + for (const [provider, providerConfig] of Object.entries(providers)) { + if (!isRecord(providerConfig) || !Array.isArray(providerConfig.models)) { + continue; + } + + let providerMutated = false; + const models = providerConfig.models.map((model) => { + if (!isRecord(model) || typeof model.id !== "string") { + return model; + } + const trimmed = model.id.trim(); + if (!trimmed) { + return model; + } + const id = normalizeConfiguredProviderCatalogModelId(provider, trimmed); + if (id === model.id) { + return model; + } + providerMutated = true; + return { ...model, id }; + }); + + if (providerMutated) { + nextProviders[provider] = { ...providerConfig, models }; + mutated = true; + } + } + + return mutated ? setPathValue(config, ["models", "providers"], nextProviders) : config; +} + +function normalizeModelRefsForWrite(config: unknown): unknown { + return normalizeModelProviderCatalogRefsForWrite(normalizeAgentDefaultModelRefsForWrite(config)); +} + function preserveUntouchedIncludes(params: { patch: unknown; rootAuthoredConfig: unknown; @@ -524,7 +568,7 @@ export function resolvePersistCandidateForWrite(params: { persistedCandidate: withSchema, unsetPaths: params.unsetPaths, }); - return normalizeAgentDefaultModelRefsForWrite(withAuthoredParams); + return normalizeModelRefsForWrite(withAuthoredParams); } function readRootSchemaUri(value: unknown): string | undefined {