diff --git a/src/agents/model-fallback.test.ts b/src/agents/model-fallback.test.ts index b9786f4b8..17a8835c7 100644 --- a/src/agents/model-fallback.test.ts +++ b/src/agents/model-fallback.test.ts @@ -144,6 +144,26 @@ describe("runWithModelFallback", () => { expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); }); + it("falls back on lowercase credential errors", async () => { + const cfg = makeCfg(); + const run = vi + .fn() + .mockRejectedValueOnce(new Error("no api key found for profile openai")) + .mockResolvedValueOnce("ok"); + + const result = await runWithModelFallback({ + cfg, + provider: "openai", + model: "gpt-4.1-mini", + run, + }); + + expect(result.result).toBe("ok"); + expect(run).toHaveBeenCalledTimes(2); + expect(run.mock.calls[1]?.[0]).toBe("anthropic"); + expect(run.mock.calls[1]?.[1]).toBe("claude-haiku-3-5"); + }); + it("appends the configured primary as a last fallback", async () => { const cfg = makeCfg({ agents: { diff --git a/src/agents/pi-embedded-helpers.test.ts b/src/agents/pi-embedded-helpers.test.ts index 4d9872ebf..55c85eb7d 100644 --- a/src/agents/pi-embedded-helpers.test.ts +++ b/src/agents/pi-embedded-helpers.test.ts @@ -5,6 +5,7 @@ import { buildBootstrapContextFiles, classifyFailoverReason, formatAssistantErrorText, + isAuthErrorMessage, isBillingErrorMessage, isCloudCodeAssistFormatError, isCompactionFailureError, @@ -122,6 +123,23 @@ describe("isBillingErrorMessage", () => { }); }); +describe("isAuthErrorMessage", () => { + it("matches credential validation errors", () => { + const samples = [ + 'No credentials found for profile "anthropic:claude-cli".', + "No API key found for profile openai.", + ]; + for (const sample of samples) { + expect(isAuthErrorMessage(sample)).toBe(true); + } + }); + + it("ignores unrelated errors", () => { + expect(isAuthErrorMessage("rate limit exceeded")).toBe(false); + expect(isAuthErrorMessage("billing issue detected")).toBe(false); + }); +}); + describe("isFailoverErrorMessage", () => { it("matches auth/rate/billing/timeout", () => { const samples = [ @@ -140,6 +158,8 @@ describe("isFailoverErrorMessage", () => { describe("classifyFailoverReason", () => { it("returns a stable reason", () => { expect(classifyFailoverReason("invalid api key")).toBe("auth"); + expect(classifyFailoverReason("no credentials found")).toBe("auth"); + expect(classifyFailoverReason("no api key found")).toBe("auth"); expect(classifyFailoverReason("429 too many requests")).toBe("rate_limit"); expect(classifyFailoverReason("resource has been exhausted")).toBe( "rate_limit",