fix(cli): normalize Gemini config mutation refs

This commit is contained in:
Peter Steinberger
2026-05-13 08:56:02 +01:00
parent 954407ab74
commit 22411e17cb
3 changed files with 185 additions and 19 deletions

View File

@@ -72,6 +72,7 @@ Docs: https://docs.openclaw.ai
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids returned by direct `openclaw models auth login --set-default` provider auth flows before writing config, so Gemini testing targets `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in per-agent config defaults and auth patches, so agent-specific emitted config keeps targeting `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in provider catalog rows when API-key onboarding only reapplies the agent default, so emitted config keeps testing `google/gemini-3.1-pro-preview`.
- Google/Gemini: normalize retired Gemini 3 Pro Preview ids in `config set` mutation output for agent overrides and provider catalog rows, so current config emits `google/gemini-3.1-pro-preview`.
- 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.

View File

@@ -479,6 +479,64 @@ describe("config cli", () => {
]);
});
it("normalizes agent-list model refs before writing config mutations", async () => {
const resolved: OpenClawConfig = {
agents: {
list: [
{
id: "tester",
model: { primary: "google/gemini-3-pro-preview" },
models: {
"google/gemini-3-pro-preview": { alias: "gemini" },
},
},
],
},
};
setSnapshot(resolved, resolved);
await runConfigCommand(["config", "set", "gateway.port", "18790"]);
expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
const agent = firstWrittenConfig().agents?.list?.[0];
expect(agent?.model).toEqual({ primary: "google/gemini-3.1-pro-preview" });
expect(agent?.models).toEqual({
"google/gemini-3.1-pro-preview": { alias: "gemini" },
});
});
it("normalizes provider catalog model refs before writing config mutations", async () => {
const resolved: OpenClawConfig = {
models: {
providers: {
google: {
api: "google-generative-ai",
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
models: [
{
id: "google/gemini-3-pro-preview",
name: "Gemini 3 Pro",
contextWindow: 1_048_576,
maxTokens: 65_536,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
reasoning: true,
},
],
},
},
},
};
setSnapshot(resolved, resolved);
await runConfigCommand(["config", "set", "gateway.port", "18790"]);
expect(mockWriteConfigFile).toHaveBeenCalledTimes(1);
expect(firstWrittenConfig().models?.providers?.google?.models?.[0]?.id).toBe(
"google/gemini-3.1-pro-preview",
);
});
it("rejects plugin install record config updates", async () => {
await expect(
runConfigCommand([

View File

@@ -1,6 +1,7 @@
import fs from "node:fs";
import type { Command } from "commander";
import JSON5 from "json5";
import { normalizeConfiguredProviderCatalogModelId } from "../agents/model-ref-shared.js";
import { readConfigFileSnapshot, replaceConfigFile } from "../config/config.js";
import { AUTO_MANAGED_CONFIG_META_PATHS } from "../config/io.meta.js";
import { formatConfigIssueLines, normalizeConfigIssues } from "../config/issue-format.js";
@@ -116,30 +117,136 @@ function normalizeAgentDefaultModelValueForConfigMutation(value: unknown): unkno
return next;
}
function normalizeAgentListModelRefsForConfigMutation(value: unknown): unknown {
if (!Array.isArray(value)) {
return value;
}
let mutated = false;
const next = value.map((agent) => {
if (!isPlainRecord(agent)) {
return agent;
}
let nextAgent = agent;
if (Object.prototype.hasOwnProperty.call(agent, "model")) {
const model = normalizeAgentDefaultModelValueForConfigMutation(agent.model);
if (model !== agent.model) {
nextAgent = { ...nextAgent, model };
mutated = true;
}
}
if (isPlainRecord(agent.models)) {
const models = normalizeAgentModelMapForConfig(agent.models);
if (models !== agent.models) {
nextAgent = { ...nextAgent, models };
mutated = true;
}
}
return nextAgent;
});
return mutated ? next : value;
}
function normalizeProviderCatalogModelsForConfigMutation(
provider: string,
models: unknown,
): unknown {
if (!Array.isArray(models)) {
return models;
}
let mutated = false;
const next = models.map((model) => {
if (!isPlainRecord(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;
}
mutated = true;
return { ...model, id };
});
return mutated ? next : models;
}
function normalizeModelProviderRefsForConfigMutation(
providers: NonNullable<OpenClawConfig["models"]>["providers"] | undefined,
): unknown {
if (!isPlainRecord(providers)) {
return providers;
}
let mutated = false;
const nextProviders: Record<string, unknown> = { ...providers };
for (const [provider, providerConfig] of Object.entries(providers)) {
if (!isPlainRecord(providerConfig)) {
continue;
}
const models = normalizeProviderCatalogModelsForConfigMutation(provider, providerConfig.models);
if (models === providerConfig.models) {
continue;
}
nextProviders[provider] = { ...providerConfig, models };
mutated = true;
}
return mutated ? nextProviders : providers;
}
function normalizeConfigMutationModelRefs(cfg: OpenClawConfig): OpenClawConfig {
const defaults = cfg.agents?.defaults;
if (!defaults) {
return cfg;
}
const agentList = cfg.agents?.list;
const providers = cfg.models?.providers;
const normalizedAgentList = normalizeAgentListModelRefsForConfigMutation(agentList);
const normalizedProviders = normalizeModelProviderRefsForConfigMutation(providers) as
| typeof providers
| undefined;
return {
...cfg,
agents: {
...cfg.agents,
defaults: {
...defaults,
...(defaults.model !== undefined
? {
model: normalizeAgentDefaultModelValueForConfigMutation(
defaults.model,
) as typeof defaults.model,
}
: undefined),
...(defaults.models !== undefined
? { models: normalizeAgentModelMapForConfig(defaults.models) }
: undefined),
},
},
...(defaults || normalizedAgentList !== agentList
? {
agents: {
...cfg.agents,
...(defaults
? {
defaults: {
...defaults,
...(defaults.model !== undefined
? {
model: normalizeAgentDefaultModelValueForConfigMutation(
defaults.model,
) as typeof defaults.model,
}
: undefined),
...(defaults.models !== undefined
? { models: normalizeAgentModelMapForConfig(defaults.models) }
: undefined),
},
}
: undefined),
...(normalizedAgentList !== agentList
? { list: normalizedAgentList as typeof agentList }
: undefined),
},
}
: undefined),
...(normalizedProviders !== providers
? {
models: {
...cfg.models,
providers: normalizedProviders,
},
}
: undefined),
};
}