fix: canonicalize openrouter native model keys

This commit is contained in:
Peter Steinberger
2026-03-12 16:50:31 +00:00
parent 115f24819e
commit 0b34671de3
7 changed files with 149 additions and 14 deletions

View File

@@ -273,6 +273,29 @@ describe("models list/status", () => {
expect(runtime.log.mock.calls[0]?.[0]).toBe("zai/glm-4.7");
});
it("models list plain keeps canonical OpenRouter native ids", async () => {
loadConfig.mockReturnValue({
agents: { defaults: { model: "openrouter/hunter-alpha" } },
});
const runtime = makeRuntime();
modelRegistryState.models = [
{
provider: "openrouter",
id: "openrouter/hunter-alpha",
name: "Hunter Alpha",
input: ["text"],
baseUrl: "https://openrouter.ai/api/v1",
contextWindow: 1048576,
},
];
modelRegistryState.available = modelRegistryState.models;
await modelsListCommand({ plain: true }, runtime);
expect(runtime.log).toHaveBeenCalledTimes(1);
expect(runtime.log.mock.calls[0]?.[0]).toBe("openrouter/hunter-alpha");
});
it.each(["z.ai", "Z.AI", "z-ai"] as const)(
"models list provider filter normalizes %s alias",
async (provider) => {

View File

@@ -110,6 +110,45 @@ describe("models set + fallbacks", () => {
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("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);
expect(writeConfigFile).toHaveBeenCalledTimes(1);
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();

View File

@@ -2,6 +2,7 @@ import { buildModelAliasIndex, resolveModelRefFromString } from "../../agents/mo
import type { OpenClawConfig } from "../../config/config.js";
import { logConfigUpdated } from "../../config/logging.js";
import { resolveAgentModelFallbackValues, toAgentModelListLike } from "../../config/model-input.js";
import type { AgentModelEntryConfig } from "../../config/types.agent-defaults.js";
import type { RuntimeEnv } from "../../runtime.js";
import { loadModelsConfig } from "./load-config.js";
import {
@@ -11,6 +12,7 @@ import {
modelKey,
resolveModelTarget,
resolveModelKeysFromEntries,
upsertCanonicalModelConfigEntry,
updateConfig,
} from "./shared.js";
@@ -79,11 +81,10 @@ export async function addFallbackCommand(
) {
const updated = await updateConfig((cfg) => {
const resolved = resolveModelTarget({ raw: modelRaw, cfg });
const targetKey = modelKey(resolved.provider, resolved.model);
const nextModels = { ...cfg.agents?.defaults?.models } as Record<string, unknown>;
if (!nextModels[targetKey]) {
nextModels[targetKey] = {};
}
const nextModels = {
...cfg.agents?.defaults?.models,
} as Record<string, AgentModelEntryConfig>;
const targetKey = upsertCanonicalModelConfigEntry(nextModels, resolved);
const existing = getFallbacks(cfg, params.key);
const existingKeys = resolveModelKeysFromEntries({ cfg, entries: existing });
if (existingKeys.includes(targetKey)) {

View File

@@ -2,6 +2,7 @@ import { listAgentIds } from "../../agents/agent-scope.js";
import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
import {
buildModelAliasIndex,
legacyModelKey,
modelKey,
parseModelRef,
resolveModelRefFromString,
@@ -14,6 +15,7 @@ import {
} from "../../config/config.js";
import { formatConfigIssueLines } from "../../config/issue-format.js";
import { toAgentModelListLike } from "../../config/model-input.js";
import type { AgentModelEntryConfig } from "../../config/types.agent-defaults.js";
import type { AgentModelConfig } from "../../config/types.agents-shared.js";
import { normalizeAgentId } from "../../routing/session-key.js";
@@ -163,6 +165,25 @@ export function resolveKnownAgentId(params: {
export type PrimaryFallbackConfig = { primary?: string; fallbacks?: string[] };
export function upsertCanonicalModelConfigEntry(
models: Record<string, AgentModelEntryConfig>,
params: { provider: string; model: string },
) {
const key = modelKey(params.provider, params.model);
const legacyKey = legacyModelKey(params.provider, params.model);
if (!models[key]) {
if (legacyKey && models[legacyKey]) {
models[key] = models[legacyKey];
} else {
models[key] = {};
}
}
if (legacyKey) {
delete models[legacyKey];
}
return key;
}
export function mergePrimaryFallbackConfig(
existing: PrimaryFallbackConfig | undefined,
patch: { primary?: string; fallbacks?: string[] },
@@ -184,12 +205,10 @@ export function applyDefaultModelPrimaryUpdate(params: {
field: "model" | "imageModel";
}): OpenClawConfig {
const resolved = resolveModelTarget({ raw: params.modelRaw, cfg: params.cfg });
const key = `${resolved.provider}/${resolved.model}`;
const nextModels = { ...params.cfg.agents?.defaults?.models };
if (!nextModels[key]) {
nextModels[key] = {};
}
const nextModels = {
...params.cfg.agents?.defaults?.models,
} as Record<string, AgentModelEntryConfig>;
const key = upsertCanonicalModelConfigEntry(nextModels, resolved);
const defaults = params.cfg.agents?.defaults ?? {};
const existing = toAgentModelListLike(