From 95901042d4ff2ea4335cf8677aee3d51ab784ae2 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 13 May 2026 10:41:56 +0100 Subject: [PATCH] fix(config): normalize gemini subagent model writes --- CHANGELOG.md | 1 + src/config/io.write-prepare.test.ts | 57 +++++++++++++++++++ src/config/io.write-prepare.ts | 86 +++++++++++++++++++---------- 3 files changed, 116 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 97d835bcdc9..c6d42bbc15a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ Docs: https://docs.openclaw.ai - Google/Gemini: canonicalize provider-qualified retired Gemini 3 Pro Preview refs during Google forward-compatible model resolution, so emitted config uses `google/gemini-3.1-pro-preview` for Gemini 3.1 testing. - Google/Gemini: normalize proxy-prefixed retired Gemini 3 Pro Preview catalog rows, so emitted configs use `google/gemini-3.1-pro-preview` for Gemini 3.1 testing. - Google/Gemini: normalize retired Gemini 3 Pro Preview ids inside per-agent model overrides before writing config, so agent-specific config emits `google/gemini-3.1-pro-preview` for Gemini 3.1 testing. +- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in subagent, heartbeat, compaction, and subagent-tool model config during writes, so current config keeps emitting `google/gemini-3.1-pro-preview`. - Docs/subagents: document `agents.defaults.subagents.announceTimeoutMs` in the sub-agent and configuration references. (#75509) Thanks @akrimm702. - Cron: add direct `cron.get`, `openclaw cron get `, and agent-tool `get` support for inspecting one stored cron job by id. (#75117) Thanks @samzong. - Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC. diff --git a/src/config/io.write-prepare.test.ts b/src/config/io.write-prepare.test.ts index 15bebb23b7a..f4c3a8e1f04 100644 --- a/src/config/io.write-prepare.test.ts +++ b/src/config/io.write-prepare.test.ts @@ -177,6 +177,17 @@ describe("config io write prepare", () => { primary: "google/gemini-3-pro-preview", fallbacks: ["google/gemini-3-pro-preview", "openai/gpt-5.5"], }, + heartbeat: { model: "google/gemini-3-pro-preview" }, + subagents: { + model: { + primary: "google/gemini-3-pro-preview", + fallbacks: ["google/gemini-3-pro-preview"], + }, + }, + compaction: { + model: "google/gemini-3-pro-preview", + memoryFlush: { model: "google/gemini-3-pro-preview" }, + }, models: { "google/gemini-3-pro-preview": { alias: "Gemini", @@ -190,6 +201,8 @@ describe("config io write prepare", () => { primary: "google/gemini-3-pro-preview", fallbacks: ["google/gemini-3-pro-preview"], }, + heartbeat: { model: "google/gemini-3-pro-preview" }, + subagents: { model: "google/gemini-3-pro-preview" }, models: { "google/gemini-3-pro-preview": { alias: "Ops Gemini", @@ -198,6 +211,14 @@ describe("config io write prepare", () => { }, ], }, + tools: { + subagents: { + model: { + primary: "google/gemini-3-pro-preview", + fallbacks: ["google/gemini-3-pro-preview"], + }, + }, + }, gateway: { port: 18789 }, }; const runtimeConfig: OpenClawConfig = { @@ -207,6 +228,17 @@ describe("config io write prepare", () => { primary: "google/gemini-3.1-pro-preview", fallbacks: ["google/gemini-3.1-pro-preview", "openai/gpt-5.5"], }, + heartbeat: { model: "google/gemini-3.1-pro-preview" }, + subagents: { + model: { + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview"], + }, + }, + compaction: { + model: "google/gemini-3.1-pro-preview", + memoryFlush: { model: "google/gemini-3.1-pro-preview" }, + }, models: { "google/gemini-3.1-pro-preview": { alias: "Gemini", @@ -220,6 +252,8 @@ describe("config io write prepare", () => { primary: "google/gemini-3.1-pro-preview", fallbacks: ["google/gemini-3.1-pro-preview"], }, + heartbeat: { model: "google/gemini-3.1-pro-preview" }, + subagents: { model: "google/gemini-3.1-pro-preview" }, models: { "google/gemini-3.1-pro-preview": { alias: "Ops Gemini", @@ -228,6 +262,14 @@ describe("config io write prepare", () => { }, ], }, + tools: { + subagents: { + model: { + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview"], + }, + }, + }, gateway: { port: 18789 }, }; const persisted = resolvePersistCandidateForWrite({ @@ -243,6 +285,15 @@ describe("config io write prepare", () => { primary: "google/gemini-3.1-pro-preview", fallbacks: ["google/gemini-3.1-pro-preview", "openai/gpt-5.5"], }); + expect(persisted.agents?.defaults?.heartbeat?.model).toBe("google/gemini-3.1-pro-preview"); + expect(persisted.agents?.defaults?.subagents?.model).toEqual({ + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview"], + }); + expect(persisted.agents?.defaults?.compaction?.model).toBe("google/gemini-3.1-pro-preview"); + expect(persisted.agents?.defaults?.compaction?.memoryFlush?.model).toBe( + "google/gemini-3.1-pro-preview", + ); expect(persisted.agents?.defaults?.models).toEqual({ "google/gemini-3.1-pro-preview": { alias: "Gemini", @@ -252,11 +303,17 @@ describe("config io write prepare", () => { primary: "google/gemini-3.1-pro-preview", fallbacks: ["google/gemini-3.1-pro-preview"], }); + expect(persisted.agents?.list?.[0]?.heartbeat?.model).toBe("google/gemini-3.1-pro-preview"); + expect(persisted.agents?.list?.[0]?.subagents?.model).toBe("google/gemini-3.1-pro-preview"); expect(persisted.agents?.list?.[0]?.models).toEqual({ "google/gemini-3.1-pro-preview": { alias: "Ops Gemini", }, }); + expect(persisted.tools?.subagents?.model).toEqual({ + primary: "google/gemini-3.1-pro-preview", + fallbacks: ["google/gemini-3.1-pro-preview"], + }); expect(persisted.gateway?.port).toBe(18888); }); diff --git a/src/config/io.write-prepare.ts b/src/config/io.write-prepare.ts index d15c4d6946e..5db977d784b 100644 --- a/src/config/io.write-prepare.ts +++ b/src/config/io.write-prepare.ts @@ -333,23 +333,53 @@ function normalizeAgentModelConfigForWrite(value: unknown): unknown { return mutated ? next : value; } -function normalizeAgentDefaultModelRefsForWrite(config: unknown): unknown { - const defaults = getPathValue(config, ["agents", "defaults"]); - if (!isRecord(defaults)) { +const AGENT_MODEL_CONFIG_KEYS = [ + "model", + "imageModel", + "imageGenerationModel", + "videoGenerationModel", + "musicGenerationModel", + "pdfModel", +] as const; + +function normalizeModelConfigPathForWrite(config: unknown, path: string[]): unknown { + const value = getPathValue(config, path); + if (value === undefined) { + return config; + } + const normalizedModel = normalizeAgentModelConfigForWrite(value); + return normalizedModel !== value ? setPathValue(config, path, normalizedModel) : config; +} + +function normalizeModelStringPathForWrite(config: unknown, path: string[]): unknown { + const value = getPathValue(config, path); + if (typeof value !== "string") { + return config; + } + const normalized = normalizeAgentModelRefForConfig(value); + return normalized !== value ? setPathValue(config, path, normalized) : config; +} + +function normalizeAgentModelRefsAtPathForWrite(config: unknown, path: string[]): unknown { + const agent = getPathValue(config, path); + if (!isRecord(agent)) { return config; } let next = config; - if (Object.prototype.hasOwnProperty.call(defaults, "model")) { - const normalizedModel = normalizeAgentModelConfigForWrite(defaults.model); - if (normalizedModel !== defaults.model) { - next = setPathValue(next, ["agents", "defaults", "model"], normalizedModel); - } + for (const key of AGENT_MODEL_CONFIG_KEYS) { + next = normalizeModelConfigPathForWrite(next, [...path, key]); } - if (isRecord(defaults.models)) { - const normalizedModels = normalizeAgentModelMapForConfig(defaults.models); - if (normalizedModels !== defaults.models) { - next = setPathValue(next, ["agents", "defaults", "models"], normalizedModels); + next = normalizeModelStringPathForWrite(next, [...path, "heartbeat", "model"]); + next = normalizeModelConfigPathForWrite(next, [...path, "subagents", "model"]); + next = normalizeModelStringPathForWrite(next, [...path, "compaction", "model"]); + next = normalizeModelStringPathForWrite(next, [...path, "compaction", "memoryFlush", "model"]); + + const models = getPathValue(next, [...path, "models"]); + if (isRecord(models)) { + const normalizedModels = normalizeAgentModelMapForConfig(models); + if (normalizedModels !== models) { + next = setPathValue(next, [...path, "models"], normalizedModels); } } return next; @@ -367,27 +397,23 @@ function normalizeAgentListModelRefsForWrite(config: unknown): unknown { return agent; } - let nextAgent = agent; - if (Object.prototype.hasOwnProperty.call(agent, "model")) { - const normalizedModel = normalizeAgentModelConfigForWrite(agent.model); - if (normalizedModel !== agent.model) { - nextAgent = { ...nextAgent, model: normalizedModel }; - mutated = true; - } + const normalized = normalizeAgentModelRefsAtPathForWrite({ agent }, ["agent"]) as { + agent: unknown; + }; + if (normalized.agent !== agent) { + mutated = true; + return normalized.agent; } - if (isRecord(agent.models)) { - const normalizedModels = normalizeAgentModelMapForConfig(agent.models); - if (normalizedModels !== agent.models) { - nextAgent = { ...nextAgent, models: normalizedModels }; - mutated = true; - } - } - return nextAgent; + return agent; }); return mutated ? setPathValue(config, ["agents", "list"], nextList) : config; } +function normalizeToolsModelRefsForWrite(config: unknown): unknown { + return normalizeModelConfigPathForWrite(config, ["tools", "subagents", "model"]); +} + function normalizeModelProviderCatalogRefsForWrite(config: unknown): unknown { const providers = getPathValue(config, ["models", "providers"]); if (!isRecord(providers)) { @@ -429,7 +455,11 @@ function normalizeModelProviderCatalogRefsForWrite(config: unknown): unknown { function normalizeModelRefsForWrite(config: unknown): unknown { return normalizeModelProviderCatalogRefsForWrite( - normalizeAgentListModelRefsForWrite(normalizeAgentDefaultModelRefsForWrite(config)), + normalizeToolsModelRefsForWrite( + normalizeAgentListModelRefsForWrite( + normalizeAgentModelRefsAtPathForWrite(config, ["agents", "defaults"]), + ), + ), ); }