diff --git a/CHANGELOG.md b/CHANGELOG.md index 59a990684a8..87c3c6c7b5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,14 +28,14 @@ Docs: https://docs.openclaw.ai ### Fixes -- OpenRouter/Anthropic: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed. - +- Providers/OpenRouter: inject `cache_control` on system prompts for OpenRouter Anthropic models to improve prompt-cache reuse. (#17473) Thanks @rrenamed. - Providers/OpenRouter: allow pass-through OpenRouter and Opencode model IDs in live model filtering so custom routed model IDs are treated as modern refs. (#14312) Thanks @Joly0. - Providers/OpenRouter: default reasoning to enabled when the selected model advertises `reasoning: true` and no session/directive override is set. (#22513) Thanks @zwffff. - Providers/OpenRouter: map `/think` levels to `reasoning.effort` in embedded runs while preserving explicit `reasoning.max_tokens` payloads. (#17236) Thanks @robbyczgw-cla. - Providers/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. - Providers/OpenRouter: preserve the required `openrouter/` prefix for OpenRouter-native model IDs during model-ref normalization. (#12942) Thanks @omair445. - Providers/OpenRouter: pass through provider routing parameters from model params.provider to OpenRouter request payloads for provider selection controls. (#17148) Thanks @carrotRakko. +- Providers/OpenRouter: preserve model allowlist entries containing OpenRouter preset paths (for example `openrouter/@preset/...`) by treating `/model ...@profile` auth-profile parsing as a suffix-only override. (#14120) Thanks @NotMainstream. - Cron/Auth: propagate auth-profile resolution to isolated cron sessions so provider API keys are resolved the same way as main sessions, fixing 401 errors when using providers configured via auth-profiles. (#20689) Thanks @lailoo. - Telegram/Webhook: keep webhook monitors alive until gateway abort signals fire, preventing false channel exits and immediate webhook auto-restart loops. - Telegram/Polling: retry recoverable setup-time network failures in monitor startup and await runner teardown before retry to avoid overlapping polling sessions. diff --git a/src/auto-reply/model.test.ts b/src/auto-reply/model.test.ts index de1fb6a8a85..d96bc863b04 100644 --- a/src/auto-reply/model.test.ts +++ b/src/auto-reply/model.test.ts @@ -36,6 +36,20 @@ describe("extractModelDirective", () => { expect(result.rawProfile).toBe("myprofile"); }); + it("keeps OpenRouter preset paths that include @ in the model name", () => { + const result = extractModelDirective("/model openrouter/@preset/kimi-2-5"); + expect(result.hasDirective).toBe(true); + expect(result.rawModel).toBe("openrouter/@preset/kimi-2-5"); + expect(result.rawProfile).toBeUndefined(); + }); + + it("still allows profile overrides after OpenRouter preset paths", () => { + const result = extractModelDirective("/model openrouter/@preset/kimi-2-5@work"); + expect(result.hasDirective).toBe(true); + expect(result.rawModel).toBe("openrouter/@preset/kimi-2-5"); + expect(result.rawProfile).toBe("work"); + }); + it("returns no directive for plain text", () => { const result = extractModelDirective("hello world"); expect(result.hasDirective).toBe(false); diff --git a/src/auto-reply/model.ts b/src/auto-reply/model.ts index 081070f3f9b..2341f805949 100644 --- a/src/auto-reply/model.ts +++ b/src/auto-reply/model.ts @@ -33,10 +33,16 @@ export function extractModelDirective( let rawModel = raw; let rawProfile: string | undefined; - if (raw?.includes("@")) { - const parts = raw.split("@"); - rawModel = parts[0]?.trim(); - rawProfile = parts.slice(1).join("@").trim() || undefined; + if (raw) { + const atIndex = raw.lastIndexOf("@"); + if (atIndex > 0) { + const candidateModel = raw.slice(0, atIndex).trim(); + const candidateProfile = raw.slice(atIndex + 1).trim(); + if (candidateModel && candidateProfile && !candidateProfile.includes("/")) { + rawModel = candidateModel; + rawProfile = candidateProfile; + } + } } const cleaned = match ? body.replace(match[0], " ").replace(/\s+/g, " ").trim() : body.trim();