fix(models): repair provider-wrapped session overrides

This commit is contained in:
Peter Steinberger
2026-05-10 06:39:35 +01:00
parent fe23f8803d
commit 572dd675d8
5 changed files with 122 additions and 2 deletions

View File

@@ -69,6 +69,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.

View File

@@ -241,6 +241,7 @@ vi.mock("../sessions/level-overrides.js", () => ({
vi.mock("../sessions/model-overrides.js", () => ({
applyModelOverrideToSessionEntry: () => ({ updated: false }),
repairProviderWrappedModelOverride: () => ({ updated: false }),
}));
vi.mock("../sessions/send-policy.js", () => ({

View File

@@ -24,7 +24,10 @@ import {
} from "../routing/session-key.js";
import { defaultRuntime, type RuntimeEnv } from "../runtime.js";
import { applyVerboseOverride } from "../sessions/level-overrides.js";
import { applyModelOverrideToSessionEntry } from "../sessions/model-overrides.js";
import {
applyModelOverrideToSessionEntry,
repairProviderWrappedModelOverride,
} from "../sessions/model-overrides.js";
import { resolveSendPolicy } from "../sessions/send-policy.js";
import { createLazyImportLoader } from "../shared/lazy-promise.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
@@ -778,6 +781,19 @@ async function agentCommandInternal(
if (sessionEntry && sessionStore && sessionKey && hasStoredOverride) {
const entry = sessionEntry;
const repaired = repairProviderWrappedModelOverride({
entry,
defaultProvider,
defaultModel,
});
if (repaired.updated) {
await persistSessionEntry({
sessionStore,
sessionKey,
storePath,
entry,
});
}
const overrideProvider = sessionEntry.providerOverride?.trim() || defaultProvider;
const overrideModel = sessionEntry.modelOverride?.trim();
if (overrideModel) {

View File

@@ -1,6 +1,9 @@
import { describe, expect, it } from "vitest";
import type { SessionEntry } from "../config/sessions.js";
import { applyModelOverrideToSessionEntry } from "./model-overrides.js";
import {
applyModelOverrideToSessionEntry,
repairProviderWrappedModelOverride,
} from "./model-overrides.js";
function applyOpenAiSelection(entry: SessionEntry) {
return applyModelOverrideToSessionEntry({
@@ -173,3 +176,57 @@ describe("applyModelOverrideToSessionEntry", () => {
expect(withFlagEntry.liveModelSwitchPending).toBe(true);
});
});
describe("repairProviderWrappedModelOverride", () => {
it("restores a provider-wrapped override from aligned runtime model fields", () => {
const before = Date.now() - 5_000;
const entry: SessionEntry = {
sessionId: "sess-openrouter-repair-runtime",
updatedAt: before,
providerOverride: "anthropic",
modelOverride: "claude-haiku-4.5",
modelOverrideSource: "user",
modelProvider: "openrouter",
model: "anthropic/claude-haiku-4.5",
contextTokens: 200_000,
};
const result = repairProviderWrappedModelOverride({
entry,
defaultProvider: "openai",
defaultModel: "gpt-5.4",
});
expect(result.updated).toBe(true);
expect(entry.providerOverride).toBe("openrouter");
expect(entry.modelOverride).toBe("anthropic/claude-haiku-4.5");
expect(entry.modelOverrideSource).toBe("user");
expect(entry.modelProvider).toBeUndefined();
expect(entry.model).toBeUndefined();
expect(entry.contextTokens).toBeUndefined();
expect((entry.updatedAt ?? 0) > before).toBe(true);
});
it("clears a provider-wrapped override that matches the configured default", () => {
const before = Date.now() - 5_000;
const entry: SessionEntry = {
sessionId: "sess-openrouter-repair-default",
updatedAt: before,
providerOverride: "anthropic",
modelOverride: "claude-haiku-4.5",
modelOverrideSource: "user",
};
const result = repairProviderWrappedModelOverride({
entry,
defaultProvider: "openrouter",
defaultModel: "anthropic/claude-haiku-4.5",
});
expect(result.updated).toBe(true);
expect(entry.providerOverride).toBeUndefined();
expect(entry.modelOverride).toBeUndefined();
expect(entry.modelOverrideSource).toBeUndefined();
expect((entry.updatedAt ?? 0) > before).toBe(true);
});
});

View File

@@ -125,3 +125,48 @@ export function applyModelOverrideToSessionEntry(params: {
return { updated };
}
function wrappedOverrideModel(provider: string, model: string): string {
return `${provider}/${model}`;
}
export function repairProviderWrappedModelOverride(params: {
entry: SessionEntry;
defaultProvider: string;
defaultModel?: string;
}): { updated: boolean } {
const overrideProvider = normalizeOptionalString(params.entry.providerOverride);
const overrideModel = normalizeOptionalString(params.entry.modelOverride);
if (!overrideProvider || !overrideModel) {
return { updated: false };
}
const wrappedModel = wrappedOverrideModel(overrideProvider, overrideModel);
const runtimeProvider = normalizeOptionalString(params.entry.modelProvider);
const runtimeModel = normalizeOptionalString(params.entry.model);
if (runtimeProvider && runtimeModel === wrappedModel && runtimeProvider !== overrideProvider) {
return applyModelOverrideToSessionEntry({
entry: params.entry,
selection: {
provider: runtimeProvider,
model: runtimeModel,
isDefault:
runtimeProvider === params.defaultProvider && runtimeModel === params.defaultModel,
},
selectionSource: params.entry.modelOverrideSource === "auto" ? "auto" : "user",
});
}
if (params.defaultProvider !== overrideProvider && params.defaultModel === wrappedModel) {
return applyModelOverrideToSessionEntry({
entry: params.entry,
selection: {
provider: params.defaultProvider,
model: params.defaultModel,
isDefault: true,
},
});
}
return { updated: false };
}