fix(models): land #38947 from @davidemanuelDEV

Co-authored-by: davidemanuelDEV <davidemanuelDEV@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-07 18:54:12 +00:00
parent 2f59a3cff3
commit 231c1fa37a
3 changed files with 100 additions and 0 deletions

View File

@@ -248,6 +248,7 @@ Docs: https://docs.openclaw.ai
- Outbound/message target normalization: ignore empty legacy `to`/`channelId` fields when explicit `target` is provided so valid target-based sends no longer fail legacy-param validation; includes regression coverage. (#38944) Thanks @Narcooo.
- Models/auth token prompts: guard cancelled manual token prompts so `Symbol(clack:cancel)` values cannot be persisted into auth profiles; adds regression coverage for cancelled `models auth paste-token`. (#38951) Thanks @MumuTW.
- Gateway/loopback announce URLs: treat `http://` and `https://` aliases with the same loopback/private-network policy as websocket URLs so loopback cron announce delivery no longer fails secure URL validation. (#39064) Thanks @Narcooo.
- Models/default provider fallback: when the hardcoded default provider is removed from `models.providers`, resolve defaults from configured providers instead of reporting stale removed-provider defaults in status output. (#38947) Thanks @davidemanuelDEV.
## 2026.3.2

View File

@@ -481,6 +481,83 @@ describe("model-selection", () => {
});
expect(result).toEqual({ provider: "openai", model: "gpt-4" });
});
it("should prefer configured custom provider when default provider is not in models.providers", () => {
const cfg: Partial<OpenClawConfig> = {
models: {
providers: {
n1n: {
baseUrl: "https://n1n.example.com",
models: [
{
id: "gpt-5.4",
name: "GPT 5.4",
reasoning: false,
input: ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 4096,
},
],
},
},
},
};
const result = resolveConfiguredModelRef({
cfg: cfg as OpenClawConfig,
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
});
expect(result).toEqual({ provider: "n1n", model: "gpt-5.4" });
});
it("should keep default provider when it is in models.providers", () => {
const cfg: Partial<OpenClawConfig> = {
models: {
providers: {
anthropic: {
baseUrl: "https://api.anthropic.com",
models: [
{
id: "claude-opus-4-6",
name: "Claude Opus 4.6",
reasoning: true,
input: ["text", "image"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 200000,
maxTokens: 4096,
},
],
},
},
},
};
const result = resolveConfiguredModelRef({
cfg: cfg as OpenClawConfig,
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
});
expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
});
it("should fall back to hardcoded default when no custom providers have models", () => {
const cfg: Partial<OpenClawConfig> = {
models: {
providers: {
"empty-provider": {
baseUrl: "https://example.com",
models: [],
},
},
},
};
const result = resolveConfiguredModelRef({
cfg: cfg as OpenClawConfig,
defaultProvider: "anthropic",
defaultModel: "claude-opus-4-6",
});
expect(result).toEqual({ provider: "anthropic", model: "claude-opus-4-6" });
});
});
describe("resolveThinkingDefault", () => {

View File

@@ -317,6 +317,28 @@ export function resolveConfiguredModelRef(params: {
return resolved.ref;
}
}
// Before falling back to the hardcoded default, check if the default provider
// is actually available. If it isn't but other providers are configured, prefer
// the first configured provider's first model to avoid reporting a stale default
// from a removed provider. (See #38880)
const configuredProviders = params.cfg.models?.providers;
if (configuredProviders && typeof configuredProviders === "object") {
const hasDefaultProvider = Boolean(configuredProviders[params.defaultProvider]);
if (!hasDefaultProvider) {
const availableProvider = Object.entries(configuredProviders).find(
([, providerCfg]) =>
providerCfg &&
Array.isArray(providerCfg.models) &&
providerCfg.models.length > 0 &&
providerCfg.models[0]?.id,
);
if (availableProvider) {
const [providerName, providerCfg] = availableProvider;
const firstModel = providerCfg.models[0];
return { provider: providerName, model: firstModel.id };
}
}
}
return { provider: params.defaultProvider, model: params.defaultModel };
}