Skip to content

Commit 4cad674

Browse files
dndodsoncursoragentvincentkoc
authored
fix: preserve stored provider in resolveSessionModelRef for vendor-prefixed models (#22753)
* fix: preserve stored provider in resolveSessionModelRef for vendor-prefixed models When an OpenRouter model with a vendor prefix (e.g. "anthropic/claude-haiku-4.5") was successfully used and persisted to the session entry, the next call to resolveSessionModelRef would re-parse the model string through parseModelRef, which splits on the first slash and incorrectly extracts "anthropic" as the provider — discarding the stored "openrouter" provider entirely. This caused subsequent requests to attempt direct Anthropic API calls with an OpenRouter API key, producing "credit balance too low" billing errors. The fix trusts the explicitly stored modelProvider on the session entry and skips parseModelRef re-parsing when a provider is already recorded. parseModelRef is still used as a fallback when no provider is stored on the entry. Co-authored-by: Cursor <cursoragent@cursor.com> * Changelog: add OpenRouter note for #22753 --------- Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
1 parent 91cb28e commit 4cad674

3 files changed

Lines changed: 33 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ Docs: https://docs.openclaw.ai
163163

164164
### Fixes
165165

166+
- 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.
166167
- 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.
167168
- 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`).
168169
- 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.

src/gateway/session-utils.test.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,28 @@ describe("resolveSessionModelRef", () => {
301301
expect(resolved).toEqual({ provider: "openai-codex", model: "gpt-5.3-codex" });
302302
});
303303

304+
test("preserves openrouter provider when model contains vendor prefix", () => {
305+
const cfg = {
306+
agents: {
307+
defaults: {
308+
model: { primary: "openrouter/minimax/minimax-m2.5" },
309+
},
310+
},
311+
} as OpenClawConfig;
312+
313+
const resolved = resolveSessionModelRef(cfg, {
314+
sessionId: "s-or",
315+
updatedAt: Date.now(),
316+
modelProvider: "openrouter",
317+
model: "anthropic/claude-haiku-4.5",
318+
});
319+
320+
expect(resolved).toEqual({
321+
provider: "openrouter",
322+
model: "anthropic/claude-haiku-4.5",
323+
});
324+
});
325+
304326
test("falls back to override when runtime model is not recorded yet", () => {
305327
const cfg = {
306328
agents: {

src/gateway/session-utils.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -668,15 +668,20 @@ export function resolveSessionModelRef(
668668
const runtimeModel = entry?.model?.trim();
669669
const runtimeProvider = entry?.modelProvider?.trim();
670670
if (runtimeModel) {
671-
const parsedRuntime = parseModelRef(
672-
runtimeModel,
673-
runtimeProvider || provider || DEFAULT_PROVIDER,
674-
);
671+
if (runtimeProvider) {
672+
// Provider is explicitly recorded — use it directly. Re-parsing the
673+
// model string through parseModelRef would incorrectly split OpenRouter
674+
// vendor-prefixed model names (e.g. model="anthropic/claude-haiku-4.5"
675+
// with provider="openrouter") into { provider: "anthropic" }, discarding
676+
// the stored OpenRouter provider and causing direct API calls to a
677+
// provider the user has no credentials for.
678+
return { provider: runtimeProvider, model: runtimeModel };
679+
}
680+
const parsedRuntime = parseModelRef(runtimeModel, provider || DEFAULT_PROVIDER);
675681
if (parsedRuntime) {
676682
provider = parsedRuntime.provider;
677683
model = parsedRuntime.model;
678684
} else {
679-
provider = runtimeProvider || provider;
680685
model = runtimeModel;
681686
}
682687
return { provider, model };

0 commit comments

Comments
 (0)