Skip to content

Commit 572dd67

Browse files
committed
fix(models): repair provider-wrapped session overrides
1 parent fe23f88 commit 572dd67

5 files changed

Lines changed: 122 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
6969
- Models/config: explain missing `models.providers.<provider>.models[]` registration when a model exists only in `agents.defaults.models`, instead of returning a bare unknown-model error. Fixes #80089.
7070
- MCP/tools: prefix bundle MCP server/tool fragments that would start with digits, keeping generated tool names valid for Moonshot/Kimi and other strict providers. Fixes #79179.
7171
- Models/OpenRouter: treat `403 API key budget limit exceeded` as billing so model fallback advances instead of retrying the exhausted primary. Fixes #60191. Thanks @omgitsgela.
72+
- Models/OpenRouter: repair stale session overrides that lost the outer `openrouter/` provider wrapper, so sessions return to the configured OpenRouter model instead of failing as an unknown direct-provider model. Fixes #78161. Thanks @hjamal7-bit.
7273
- Kimi Code: use Kimi's stable `kimi-for-coding` API model id in bundled catalog, onboarding, and docs while normalizing legacy `kimi-code` and `k2p5` refs. Fixes #79965.
7374
- Volcengine/Kimi: strip provider-unsupported tool schema length and item constraint keywords for direct and coding-plan models so hosted Kimi runs do not reject message tools with `minLength`. Fixes #38817.
7475
- DeepSeek: backfill V4 `reasoning_content` replay fields for unowned OpenAI-compatible proxy providers, preventing follow-up request failures outside the bundled DeepSeek and OpenRouter routes. Fixes #79608.

src/agents/agent-command.live-model-switch.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ vi.mock("../sessions/level-overrides.js", () => ({
241241

242242
vi.mock("../sessions/model-overrides.js", () => ({
243243
applyModelOverrideToSessionEntry: () => ({ updated: false }),
244+
repairProviderWrappedModelOverride: () => ({ updated: false }),
244245
}));
245246

246247
vi.mock("../sessions/send-policy.js", () => ({

src/agents/agent-command.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import {
2424
} from "../routing/session-key.js";
2525
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
2626
import { applyVerboseOverride } from "../sessions/level-overrides.js";
27-
import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js";
27+
import {
28+
applyModelOverrideToSessionEntry,
29+
repairProviderWrappedModelOverride,
30+
} from "../sessions/model-overrides.js";
2831
import { resolveSendPolicy } from "../sessions/send-policy.js";
2932
import { createLazyImportLoader } from "../shared/lazy-promise.js";
3033
import { normalizeOptionalString } from "../shared/string-coerce.js";
@@ -778,6 +781,19 @@ async function agentCommandInternal(
778781

779782
if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) {
780783
const entry = sessionEntry;
784+
const repaired = repairProviderWrappedModelOverride({
785+
entry,
786+
defaultProvider,
787+
defaultModel,
788+
});
789+
if (repaired.updated) {
790+
await persistSessionEntry({
791+
sessionStore,
792+
sessionKey,
793+
storePath,
794+
entry,
795+
});
796+
}
781797
const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider;
782798
const overrideModel = sessionEntry.modelOverride?.trim();
783799
if (overrideModel) {

src/sessions/model-overrides.test.ts

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { describe, expect, it } from "vitest";
22
import type { SessionEntry } from "../config/sessions.js";
3-
import { applyModelOverrideToSessionEntry } from "./model-overrides.js";
3+
import {
4+
applyModelOverrideToSessionEntry,
5+
repairProviderWrappedModelOverride,
6+
} from "./model-overrides.js";
47

58
function applyOpenAiSelection(entry: SessionEntry) {
69
return applyModelOverrideToSessionEntry({
@@ -173,3 +176,57 @@ describe("applyModelOverrideToSessionEntry", () => {
173176
expect(withFlagEntry.liveModelSwitchPending).toBe(true);
174177
});
175178
});
179+
180+
describe("repairProviderWrappedModelOverride", () => {
181+
it("restores a provider-wrapped override from aligned runtime model fields", () => {
182+
const before = Date.now() - 5_000;
183+
const entry: SessionEntry = {
184+
sessionId: "sess-openrouter-repair-runtime",
185+
updatedAt: before,
186+
providerOverride: "anthropic",
187+
modelOverride: "claude-haiku-4.5",
188+
modelOverrideSource: "user",
189+
modelProvider: "openrouter",
190+
model: "anthropic/claude-haiku-4.5",
191+
contextTokens: 200_000,
192+
};
193+
194+
const result = repairProviderWrappedModelOverride({
195+
entry,
196+
defaultProvider: "openai",
197+
defaultModel: "gpt-5.4",
198+
});
199+
200+
expect(result.updated).toBe(true);
201+
expect(entry.providerOverride).toBe("openrouter");
202+
expect(entry.modelOverride).toBe("anthropic/claude-haiku-4.5");
203+
expect(entry.modelOverrideSource).toBe("user");
204+
expect(entry.modelProvider).toBeUndefined();
205+
expect(entry.model).toBeUndefined();
206+
expect(entry.contextTokens).toBeUndefined();
207+
expect((entry.updatedAt ?? 0) > before).toBe(true);
208+
});
209+
210+
it("clears a provider-wrapped override that matches the configured default", () => {
211+
const before = Date.now() - 5_000;
212+
const entry: SessionEntry = {
213+
sessionId: "sess-openrouter-repair-default",
214+
updatedAt: before,
215+
providerOverride: "anthropic",
216+
modelOverride: "claude-haiku-4.5",
217+
modelOverrideSource: "user",
218+
};
219+
220+
const result = repairProviderWrappedModelOverride({
221+
entry,
222+
defaultProvider: "openrouter",
223+
defaultModel: "anthropic/claude-haiku-4.5",
224+
});
225+
226+
expect(result.updated).toBe(true);
227+
expect(entry.providerOverride).toBeUndefined();
228+
expect(entry.modelOverride).toBeUndefined();
229+
expect(entry.modelOverrideSource).toBeUndefined();
230+
expect((entry.updatedAt ?? 0) > before).toBe(true);
231+
});
232+
});

src/sessions/model-overrides.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,48 @@ export function applyModelOverrideToSessionEntry(params: {
125125

126126
return { updated };
127127
}
128+
129+
function wrappedOverrideModel(provider: string, model: string): string {
130+
return `${provider}/${model}`;
131+
}
132+
133+
export function repairProviderWrappedModelOverride(params: {
134+
entry: SessionEntry;
135+
defaultProvider: string;
136+
defaultModel?: string;
137+
}): { updated: boolean } {
138+
const overrideProvider = normalizeOptionalString(params.entry.providerOverride);
139+
const overrideModel = normalizeOptionalString(params.entry.modelOverride);
140+
if (!overrideProvider || !overrideModel) {
141+
return { updated: false };
142+
}
143+
144+
const wrappedModel = wrappedOverrideModel(overrideProvider, overrideModel);
145+
const runtimeProvider = normalizeOptionalString(params.entry.modelProvider);
146+
const runtimeModel = normalizeOptionalString(params.entry.model);
147+
if (runtimeProvider && runtimeModel === wrappedModel && runtimeProvider !== overrideProvider) {
148+
return applyModelOverrideToSessionEntry({
149+
entry: params.entry,
150+
selection: {
151+
provider: runtimeProvider,
152+
model: runtimeModel,
153+
isDefault:
154+
runtimeProvider === params.defaultProvider && runtimeModel === params.defaultModel,
155+
},
156+
selectionSource: params.entry.modelOverrideSource === "auto" ? "auto" : "user",
157+
});
158+
}
159+
160+
if (params.defaultProvider !== overrideProvider && params.defaultModel === wrappedModel) {
161+
return applyModelOverrideToSessionEntry({
162+
entry: params.entry,
163+
selection: {
164+
provider: params.defaultProvider,
165+
model: params.defaultModel,
166+
isDefault: true,
167+
},
168+
});
169+
}
170+
171+
return { updated: false };
172+
}

0 commit comments

Comments
 (0)