diff --git a/CHANGELOG.md b/CHANGELOG.md index e975296d6..0452e772b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## 2026.1.15 (unreleased) -- TBD +- Fix: guard model fallback against undefined provider/model values. (#954) — thanks @roshanasingh4. ## 2026.1.14-1 diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index 160deb7e1..381da47ff 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -213,6 +213,34 @@ describe("runWithModelFallback", () => { expect(calls).toEqual([{ provider: "anthropic", model: "claude-opus-4-5" }]); }); + it("defaults provider/model when missing (regression #946)", async () => { + const cfg = makeCfg({ + agents: { + defaults: { + model: { + primary: "openai/gpt-4.1-mini", + fallbacks: [], + }, + }, + }, + }); + + const calls: Array<{ provider: string; model: string }> = []; + + const result = await runWithModelFallback({ + cfg, + provider: undefined as unknown as string, + model: undefined as unknown as string, + run: async (provider, model) => { + calls.push({ provider, model }); + return "ok"; + }, + }); + + expect(result.result).toBe("ok"); + expect(calls).toEqual([{ provider: "openai", model: "gpt-4.1-mini" }]); + }); + it("falls back on missing API key errors", async () => { const cfg = makeCfg(); const run = vi diff --git a/src/agents/model-fallback.ts b/src/agents/model-fallback.ts index f3d7e7d52..469f45a2c 100644 --- a/src/agents/model-fallback.ts +++ b/src/agents/model-fallback.ts @@ -119,8 +119,6 @@ function resolveFallbackCandidates(params: { /** Optional explicit fallbacks list; when provided (even empty), replaces agents.defaults.model.fallbacks. */ fallbacksOverride?: string[]; }): ModelCandidate[] { - const provider = params.provider.trim() || DEFAULT_PROVIDER; - const model = params.model.trim() || DEFAULT_MODEL; const primary = params.cfg ? resolveConfiguredModelRef({ cfg: params.cfg, @@ -128,11 +126,15 @@ function resolveFallbackCandidates(params: { defaultModel: DEFAULT_MODEL, }) : null; + const defaultProvider = primary?.provider ?? DEFAULT_PROVIDER; + const defaultModel = primary?.model ?? DEFAULT_MODEL; + const provider = String(params.provider ?? "").trim() || defaultProvider; + const model = String(params.model ?? "").trim() || defaultModel; const aliasIndex = buildModelAliasIndex({ cfg: params.cfg ?? {}, - defaultProvider: DEFAULT_PROVIDER, + defaultProvider, }); - const allowlist = buildAllowedModelKeys(params.cfg, DEFAULT_PROVIDER); + const allowlist = buildAllowedModelKeys(params.cfg, defaultProvider); const seen = new Set(); const candidates: ModelCandidate[] = []; @@ -160,7 +162,7 @@ function resolveFallbackCandidates(params: { for (const raw of modelFallbacks) { const resolved = resolveModelRefFromString({ raw: String(raw ?? ""), - defaultProvider: DEFAULT_PROVIDER, + defaultProvider, aliasIndex, }); if (!resolved) continue;