From a31f4c57e5554a696fcf631bd77a97bbd04775ed Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 8 May 2026 11:17:54 +0100 Subject: [PATCH] fix: normalize Gemini auth config patches --- .../provider-auth-choice-helpers.test.ts | 53 ++++++++++++ src/plugins/provider-auth-choice-helpers.ts | 80 ++++++++++++++++++- 2 files changed, 130 insertions(+), 3 deletions(-) diff --git a/src/plugins/provider-auth-choice-helpers.test.ts b/src/plugins/provider-auth-choice-helpers.test.ts index e58e6061bf5..9d840a5b6a1 100644 --- a/src/plugins/provider-auth-choice-helpers.test.ts +++ b/src/plugins/provider-auth-choice-helpers.test.ts @@ -101,6 +101,59 @@ describe("applyProviderAuthConfigPatch", () => { }, }); }); + + it("normalizes retired Google Gemini model refs from provider config patches", () => { + const patch = { + agents: { + defaults: { + model: { + primary: "google/gemini-3-pro-preview", + fallbacks: ["google/gemini-3-pro-preview", "openai/gpt-5.5"], + }, + models: { + "google/gemini-3-pro-preview": { + alias: "gemini", + params: { thinking: "high" }, + }, + "google/gemini-3.1-pro-preview": { + params: { maxTokens: 12_000 }, + }, + }, + }, + }, + }; + + const next = applyProviderAuthConfigPatch({}, patch); + + expect(next.agents?.defaults?.model).toEqual({ + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview", "openai/gpt-5.5"], + }); + expect(next.agents?.defaults?.models).toEqual({ + "google/gemini-3.1-pro-preview": { + alias: "gemini", + params: { thinking: "high", maxTokens: 12_000 }, + }, + }); + }); + + it("normalizes retired Google Gemini keys when replacing provider model maps", () => { + const patch = { + agents: { + defaults: { + models: { + "google/gemini-3-pro-preview": {}, + }, + }, + }, + }; + + const next = applyProviderAuthConfigPatch(base, patch, { replaceDefaultModels: true }); + + expect(next.agents?.defaults?.models).toEqual({ + "google/gemini-3.1-pro-preview": {}, + }); + }); }); describe("applyDefaultModel", () => { diff --git a/src/plugins/provider-auth-choice-helpers.ts b/src/plugins/provider-auth-choice-helpers.ts index 02e8ba27591..84d5681781c 100644 --- a/src/plugins/provider-auth-choice-helpers.ts +++ b/src/plugins/provider-auth-choice-helpers.ts @@ -89,12 +89,86 @@ function mergeConfigPatch(base: T, patch: unknown): T { return next as T; } +function normalizeAgentModelConfigForWrite(value: unknown): unknown { + if (typeof value === "string") { + return normalizeAgentModelRefForConfig(value); + } + if (!isPlainRecord(value)) { + return value; + } + + const next: Record = { ...value }; + if (typeof next.primary === "string") { + next.primary = normalizeAgentModelRefForConfig(next.primary); + } + if (Array.isArray(next.fallbacks)) { + next.fallbacks = next.fallbacks.map((fallback) => + typeof fallback === "string" ? normalizeAgentModelRefForConfig(fallback) : fallback, + ); + } + return next; +} + +function mergeModelEntryConfig(existing: unknown, incoming: unknown): unknown { + if (!isPlainRecord(existing) || !isPlainRecord(incoming)) { + return incoming; + } + + const existingParams = isPlainRecord(existing.params) ? existing.params : undefined; + const incomingParams = isPlainRecord(incoming.params) ? incoming.params : undefined; + return { + ...existing, + ...incoming, + ...(existingParams || incomingParams + ? { params: { ...existingParams, ...incomingParams } } + : undefined), + }; +} + +function normalizeAgentModelMapForWrite(value: unknown): unknown { + if (!isPlainRecord(value)) { + return value; + } + + const next: Record = {}; + for (const [key, entry] of Object.entries(value)) { + const normalizedKey = normalizeAgentModelRefForConfig(key); + next[normalizedKey] = mergeModelEntryConfig(next[normalizedKey], entry); + } + return next; +} + +function normalizeConfigModelRefsForWrite(cfg: OpenClawConfig): OpenClawConfig { + const defaults = cfg.agents?.defaults; + if (!defaults) { + return cfg; + } + + const nextDefaults: NonNullable["defaults"]> = { + ...defaults, + }; + if (defaults.model !== undefined) { + nextDefaults.model = normalizeAgentModelConfigForWrite(defaults.model) as typeof defaults.model; + } + if (defaults.models !== undefined) { + nextDefaults.models = normalizeAgentModelMapForWrite(defaults.models) as typeof defaults.models; + } + + return { + ...cfg, + agents: { + ...cfg.agents, + defaults: nextDefaults, + }, + }; +} + export function applyProviderAuthConfigPatch( cfg: OpenClawConfig, patch: unknown, options?: { replaceDefaultModels?: boolean }, ): OpenClawConfig { - const merged = mergeConfigPatch(cfg, patch); + const merged = normalizeConfigModelRefsForWrite(mergeConfigPatch(cfg, patch)); if (!options?.replaceDefaultModels || !isPlainRecord(patch)) { return merged; } @@ -105,7 +179,7 @@ export function applyProviderAuthConfigPatch( return merged; } - return { + return normalizeConfigModelRefsForWrite({ ...merged, agents: { ...merged.agents, @@ -117,7 +191,7 @@ export function applyProviderAuthConfigPatch( >["models"], }, }, - }; + }); } export function applyDefaultModel(