fix(config): normalize gemini subagent model writes

This commit is contained in:
Peter Steinberger
2026-05-13 10:41:56 +01:00
parent 6c92324c5f
commit 95901042d4
3 changed files with 116 additions and 28 deletions

View File

@@ -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 <id>`, 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.

View File

@@ -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);
});

View File

@@ -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"]),
),
),
);
}