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>
This commit is contained in:
Dan Dodson
2026-02-22 12:07:33 -05:00
committed by GitHub
parent 91cb28ecef
commit 4cad674387
3 changed files with 33 additions and 5 deletions

View File

@@ -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: {

View File

@@ -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 };