mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-30 13:41:07 +00:00
Summary: - The branch passes runtime config into the model config write helper, updates `openclaw models set` to resolve aliases source-first then runtime-fallback, and adds regression tests plus a changelog entry. - Reproducibility: yes. I did not execute the CLI in this read-only review, but the current-main source path a ... ing against source config while runtime defaults can be the only place the displayed `sonnet` alias exists. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(models): preserve authored aliases for set - PR branch already contained follow-up commit before automerge: fix(models): resolve set aliases from runtime config [AI-assisted] Validation: - ClawSweeper review passed for head29138ac5d0. - Required merge gates passed before the squash merge. Prepared head SHA:29138ac5d0Review: https://github.com/openclaw/openclaw/pull/83262#issuecomment-4472495568 Co-authored-by: JARVIS-Glasses <284122573+JARVIS-Glasses@users.noreply.github.com> Co-authored-by: IWhatsskill <284122573+IWhatsskill@users.noreply.github.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com> Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com> Approved-by: takhoffman Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
177 lines
4.9 KiB
TypeScript
177 lines
4.9 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
|
const mocks = vi.hoisted(() => ({
|
|
currentConfig: {} as Record<string, unknown>,
|
|
writtenConfig: undefined as Record<string, unknown> | undefined,
|
|
}));
|
|
|
|
vi.mock("./models/shared.js", async () => {
|
|
const actual = await vi.importActual<typeof import("./models/shared.js")>("./models/shared.js");
|
|
return {
|
|
...actual,
|
|
updateConfig: async (
|
|
mutator: (
|
|
cfg: Record<string, unknown>,
|
|
context: {
|
|
runtimeConfig: Record<string, unknown>;
|
|
},
|
|
) => Record<string, unknown>,
|
|
) => {
|
|
const sourceConfig = structuredClone(mocks.currentConfig);
|
|
const runtimeConfig = structuredClone(mocks.currentConfig);
|
|
const next = mutator(sourceConfig, { runtimeConfig });
|
|
mocks.writtenConfig = next;
|
|
return next;
|
|
},
|
|
};
|
|
});
|
|
|
|
import { modelsFallbacksAddCommand } from "./models/fallbacks.js";
|
|
import { modelsSetCommand } from "./models/set.js";
|
|
|
|
function mockConfigSnapshot(config: Record<string, unknown> = {}) {
|
|
mocks.currentConfig = config;
|
|
mocks.writtenConfig = undefined;
|
|
}
|
|
|
|
function makeRuntime() {
|
|
return { log: vi.fn(), error: vi.fn(), exit: vi.fn() };
|
|
}
|
|
|
|
function getWrittenConfig() {
|
|
if (!mocks.writtenConfig) {
|
|
throw new Error("expected config write");
|
|
}
|
|
return mocks.writtenConfig;
|
|
}
|
|
|
|
function expectWrittenPrimaryModel(model: string) {
|
|
const written = getWrittenConfig();
|
|
expect(written.agents).toEqual({
|
|
defaults: {
|
|
model: { primary: model },
|
|
models: { [model]: {} },
|
|
},
|
|
});
|
|
}
|
|
|
|
describe("models set + fallbacks", () => {
|
|
beforeEach(() => {
|
|
mocks.currentConfig = {};
|
|
mocks.writtenConfig = undefined;
|
|
});
|
|
|
|
it("normalizes z.ai provider in models set", async () => {
|
|
mockConfigSnapshot({});
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("z.ai/glm-4.7", runtime);
|
|
|
|
expectWrittenPrimaryModel("zai/glm-4.7");
|
|
});
|
|
|
|
it("normalizes z-ai provider in models fallbacks add", async () => {
|
|
mockConfigSnapshot({ agents: { defaults: { model: { fallbacks: [] } } } });
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsFallbacksAddCommand("z-ai/glm-4.7", runtime);
|
|
|
|
const written = getWrittenConfig();
|
|
expect(written.agents).toEqual({
|
|
defaults: {
|
|
model: { fallbacks: ["zai/glm-4.7"] },
|
|
models: { "zai/glm-4.7": {} },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("preserves primary when adding fallbacks to string defaults.model", async () => {
|
|
mockConfigSnapshot({ agents: { defaults: { model: "openai/gpt-4.1-mini" } } });
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsFallbacksAddCommand("anthropic/claude-opus-4-6", runtime);
|
|
|
|
const written = getWrittenConfig();
|
|
expect(written.agents).toEqual({
|
|
defaults: {
|
|
model: {
|
|
primary: "openai/gpt-4.1-mini",
|
|
fallbacks: ["anthropic/claude-opus-4-6"],
|
|
},
|
|
models: { "anthropic/claude-opus-4-6": {} },
|
|
},
|
|
});
|
|
});
|
|
|
|
it("normalizes provider casing in models set", async () => {
|
|
mockConfigSnapshot({});
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("Z.AI/glm-4.7", runtime);
|
|
|
|
expectWrittenPrimaryModel("zai/glm-4.7");
|
|
});
|
|
|
|
it("keeps canonical OpenRouter native ids in models set", async () => {
|
|
mockConfigSnapshot({});
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("openrouter/hunter-alpha", runtime);
|
|
|
|
expectWrittenPrimaryModel("openrouter/hunter-alpha");
|
|
});
|
|
|
|
it("normalizes retired Google Gemini preview ids in models set", async () => {
|
|
mockConfigSnapshot({});
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("google/gemini-3-pro-preview", runtime);
|
|
|
|
expectWrittenPrimaryModel("google/gemini-3.1-pro-preview");
|
|
});
|
|
|
|
it("migrates legacy duplicated OpenRouter keys on write", async () => {
|
|
mockConfigSnapshot({
|
|
agents: {
|
|
defaults: {
|
|
models: {
|
|
"openrouter/openrouter/hunter-alpha": {
|
|
params: { thinking: "high" },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("openrouter/hunter-alpha", runtime);
|
|
|
|
const written = getWrittenConfig();
|
|
expect(written.agents).toEqual({
|
|
defaults: {
|
|
model: { primary: "openrouter/hunter-alpha" },
|
|
models: {
|
|
"openrouter/hunter-alpha": {
|
|
params: { thinking: "high" },
|
|
},
|
|
},
|
|
},
|
|
});
|
|
});
|
|
|
|
it("rewrites string defaults.model to object form when setting primary", async () => {
|
|
mockConfigSnapshot({ agents: { defaults: { model: "openai/gpt-4.1-mini" } } });
|
|
const runtime = makeRuntime();
|
|
|
|
await modelsSetCommand("anthropic/claude-opus-4-6", runtime);
|
|
|
|
const written = getWrittenConfig();
|
|
expect(written.agents).toEqual({
|
|
defaults: {
|
|
model: { primary: "anthropic/claude-opus-4-6" },
|
|
models: { "anthropic/claude-opus-4-6": {} },
|
|
},
|
|
});
|
|
});
|
|
});
|