mirror of https://github.com/openclaw/openclaw.git
fix: canonicalize openrouter native model keys
This commit is contained in:
parent
115f24819e
commit
0b34671de3
|
|
@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Mattermost/reply media delivery: pass agent-scoped `mediaLocalRoots` through shared reply delivery so allowed local files upload correctly from button, slash-command, and model-picker replies. (#44021) Thanks @LyleLiu666.
|
||||
- Plugins/env-scoped roots: fix plugin discovery/load caches and provenance tracking so same-process `HOME`/`OPENCLAW_HOME` changes no longer reuse stale plugin state or misreport `~/...` plugins as untracked. (#44046) thanks @gumadeiras.
|
||||
- Gateway/session discovery: discover disk-only and retired ACP session stores under custom templated `session.store` roots so ACP reconciliation, session-id/session-label targeting, and run-id fallback keep working after restart. (#44176) thanks @gumadeiras.
|
||||
- Models/OpenRouter native ids: canonicalize native OpenRouter model keys across config writes, runtime lookups, fallback management, and `models list --plain`, and migrate legacy duplicated `openrouter/openrouter/...` config entries forward on write.
|
||||
|
||||
## 2026.3.11
|
||||
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@ describe("model-selection", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("modelKey", () => {
|
||||
it("keeps canonical OpenRouter native ids without duplicating the provider", () => {
|
||||
expect(modelKey("openrouter", "openrouter/hunter-alpha")).toBe("openrouter/hunter-alpha");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseModelRef", () => {
|
||||
it("should parse full model refs", () => {
|
||||
expect(parseModelRef("anthropic/claude-3-5-sonnet", "openai")).toEqual({
|
||||
|
|
@ -754,6 +760,28 @@ describe("model-selection", () => {
|
|||
expect(resolveAnthropicOpusThinking(cfg)).toBe("high");
|
||||
});
|
||||
|
||||
it("accepts legacy duplicated OpenRouter keys for per-model thinking", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"openrouter/openrouter/hunter-alpha": {
|
||||
params: { thinking: "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveThinkingDefault({
|
||||
cfg,
|
||||
provider: "openrouter",
|
||||
model: "openrouter/hunter-alpha",
|
||||
}),
|
||||
).toBe("high");
|
||||
});
|
||||
|
||||
it("accepts per-model params.thinking=adaptive", () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
|
|
|
|||
|
|
@ -43,7 +43,28 @@ function normalizeAliasKey(value: string): string {
|
|||
}
|
||||
|
||||
export function modelKey(provider: string, model: string) {
|
||||
return `${provider}/${model}`;
|
||||
const providerId = provider.trim();
|
||||
const modelId = model.trim();
|
||||
if (!providerId) {
|
||||
return modelId;
|
||||
}
|
||||
if (!modelId) {
|
||||
return providerId;
|
||||
}
|
||||
return modelId.toLowerCase().startsWith(`${providerId.toLowerCase()}/`)
|
||||
? modelId
|
||||
: `${providerId}/${modelId}`;
|
||||
}
|
||||
|
||||
export function legacyModelKey(provider: string, model: string): string | null {
|
||||
const providerId = provider.trim();
|
||||
const modelId = model.trim();
|
||||
if (!providerId || !modelId) {
|
||||
return null;
|
||||
}
|
||||
const rawKey = `${providerId}/${modelId}`;
|
||||
const canonicalKey = modelKey(providerId, modelId);
|
||||
return rawKey === canonicalKey ? null : rawKey;
|
||||
}
|
||||
|
||||
export function normalizeProviderId(provider: string): string {
|
||||
|
|
@ -610,9 +631,12 @@ export function resolveThinkingDefault(params: {
|
|||
}): ThinkLevel {
|
||||
const normalizedProvider = normalizeProviderId(params.provider);
|
||||
const modelLower = params.model.toLowerCase();
|
||||
const configuredModels = params.cfg.agents?.defaults?.models;
|
||||
const canonicalKey = modelKey(params.provider, params.model);
|
||||
const legacyKey = legacyModelKey(params.provider, params.model);
|
||||
const perModelThinking =
|
||||
params.cfg.agents?.defaults?.models?.[modelKey(params.provider, params.model)]?.params
|
||||
?.thinking;
|
||||
configuredModels?.[canonicalKey]?.params?.thinking ??
|
||||
(legacyKey ? configuredModels?.[legacyKey]?.params?.thinking : undefined);
|
||||
if (
|
||||
perModelThinking === "off" ||
|
||||
perModelThinking === "minimal" ||
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue