diff --git a/CHANGELOG.md b/CHANGELOG.md index 8bb52f15b27..1bc6dfd1dd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -163,6 +163,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Gateway/OpenRouter: preserve stored session provider when model IDs are vendor-prefixed (for example, `anthropic/...`) so follow-up turns do not incorrectly route to direct provider APIs. (#22753) Thanks @dndodson. - Agents/Bootstrap: skip malformed bootstrap files with missing/invalid paths instead of crashing agent sessions; hooks using `filePath` (or non-string `path`) are skipped with a warning. (#22693, #22698) Thanks @arosstale. - Security/Agents: cap embedded Pi runner outer retry loop with a higher profile-aware dynamic limit (32-160 attempts) and return an explicit `retry_limit` error payload when retries never converge, preventing unbounded internal retry cycles (`GHSA-76m6-pj3w-v7mf`). - Telegram: detect duplicate bot-token ownership across Telegram accounts at startup/status time, mark secondary accounts as not configured with an explicit fix message, and block duplicate account startup before polling to avoid endless `getUpdates` conflict loops. diff --git a/src/gateway/session-utils.test.ts b/src/gateway/session-utils.test.ts index 6f08ca6455f..c33ff6f96e5 100644 --- a/src/gateway/session-utils.test.ts +++ b/src/gateway/session-utils.test.ts @@ -301,6 +301,28 @@ describe("resolveSessionModelRef", () => { expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" }); }); + test("preserves openrouter provider when model contains vendor prefix", () => { + const cfg = { + agents: { + defaults: { + model: { primary: "openrouter/minimax/minimax-m2.5" }, + }, + }, + } as OpenClawConfig; + + const resolved = resolveSessionModelRef(cfg, { + sessionId: "s-or", + updatedAt: Date.now(), + modelProvider: "openrouter", + model: "anthropic/claude-haiku-4.5", + }); + + expect(resolved).toEqual({ + provider: "openrouter", + model: "anthropic/claude-haiku-4.5", + }); + }); + test("falls back to override when runtime model is not recorded yet", () => { const cfg = { agents: { diff --git a/src/gateway/session-utils.ts b/src/gateway/session-utils.ts index fc4decaa688..4876bb95627 100644 --- a/src/gateway/session-utils.ts +++ b/src/gateway/session-utils.ts @@ -668,15 +668,20 @@ export function resolveSessionModelRef( const runtimeModel = entry?.model?.trim(); const runtimeProvider = entry?.modelProvider?.trim(); if (runtimeModel) { - const parsedRuntime = parseModelRef( - runtimeModel, - runtimeProvider || provider || DEFAULT_PROVIDER, - ); + if (runtimeProvider) { + // Provider is explicitly recorded — use it directly. Re-parsing the + // model string through parseModelRef would incorrectly split OpenRouter + // vendor-prefixed model names (e.g. model="anthropic/claude-haiku-4.5" + // with provider="openrouter") into { provider: "anthropic" }, discarding + // the stored OpenRouter provider and causing direct API calls to a + // provider the user has no credentials for. + return { provider: runtimeProvider, model: runtimeModel }; + } + const parsedRuntime = parseModelRef(runtimeModel, provider || DEFAULT_PROVIDER); if (parsedRuntime) { provider = parsedRuntime.provider; model = parsedRuntime.model; } else { - provider = runtimeProvider || provider; model = runtimeModel; } return { provider, model };