diff --git a/src/agents/auth-profiles/types.ts b/src/agents/auth-profiles/types.ts index b2d3c771a..a92618869 100644 --- a/src/agents/auth-profiles/types.ts +++ b/src/agents/auth-profiles/types.ts @@ -37,6 +37,7 @@ export type AuthProfileFailureReason = | "rate_limit" | "billing" | "timeout" + | "provider_unavailable" | "unknown"; /** Per-profile usage statistics for round-robin and cooldown tracking */ diff --git a/src/agents/failover-error.test.ts b/src/agents/failover-error.test.ts index a43ae289f..a71a355d1 100644 --- a/src/agents/failover-error.test.ts +++ b/src/agents/failover-error.test.ts @@ -47,6 +47,38 @@ describe("failover-error", () => { expect(err?.status).toBe(400); }); + it("infers provider_unavailable from OpenRouter no-endpoints error", () => { + expect( + resolveFailoverReasonFromError({ + status: 404, + message: "No endpoints found that support tool use", + }), + ).toBe("provider_unavailable"); + }); + + it("infers provider_unavailable from model-unavailable messages", () => { + expect( + resolveFailoverReasonFromError({ + message: "model is currently unavailable", + }), + ).toBe("provider_unavailable"); + expect( + resolveFailoverReasonFromError({ + message: "model not available for this request", + }), + ).toBe("provider_unavailable"); + }); + + it("coerces provider_unavailable errors with a 404 status", () => { + const err = coerceToFailoverError( + { message: "No endpoints found that support tool use", status: 404 }, + { provider: "openrouter", model: "deepseek/deepseek-chat-v3-0324" }, + ); + expect(err?.reason).toBe("provider_unavailable"); + expect(err?.status).toBe(404); + expect(err?.provider).toBe("openrouter"); + }); + it("describes non-Error values consistently", () => { const described = describeFailoverError(123); expect(described.message).toBe("123"); diff --git a/src/agents/failover-error.ts b/src/agents/failover-error.ts index 5026394f3..de3a1bce8 100644 --- a/src/agents/failover-error.ts +++ b/src/agents/failover-error.ts @@ -50,6 +50,8 @@ export function resolveFailoverStatus(reason: FailoverReason): number | undefine return 408; case "format": return 400; + case "provider_unavailable": + return 404; default: return undefined; } diff --git a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts index 749a52414..5038ac2dd 100644 --- a/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts +++ b/src/agents/pi-embedded-helpers.classifyfailoverreason.test.ts @@ -38,4 +38,16 @@ describe("classifyFailoverReason", () => { "rate_limit", ); }); + it("classifies OpenRouter no-endpoints errors as provider_unavailable", () => { + expect(classifyFailoverReason("No endpoints found that support tool use")).toBe( + "provider_unavailable", + ); + expect(classifyFailoverReason("No endpoints found that support this request")).toBe( + "provider_unavailable", + ); + }); + it("classifies model-unavailable errors as provider_unavailable", () => { + expect(classifyFailoverReason("model is currently unavailable")).toBe("provider_unavailable"); + expect(classifyFailoverReason("model not available")).toBe("provider_unavailable"); + }); }); diff --git a/src/agents/pi-embedded-helpers/errors.ts b/src/agents/pi-embedded-helpers/errors.ts index 849c4293e..03ef2f04e 100644 --- a/src/agents/pi-embedded-helpers/errors.ts +++ b/src/agents/pi-embedded-helpers/errors.ts @@ -396,6 +396,12 @@ const ERROR_PATTERNS = { "messages.1.content.1.tool_use.id", "invalid request format", ], + providerUnavailable: [ + "no endpoints found", + /no .* endpoints? available/, + "model is currently unavailable", + "model not available", + ], } as const; const IMAGE_DIMENSION_ERROR_RE = @@ -496,6 +502,10 @@ export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean return isAuthErrorMessage(msg.errorMessage ?? ""); } +export function isProviderUnavailableErrorMessage(raw: string): boolean { + return matchesErrorPatterns(raw, ERROR_PATTERNS.providerUnavailable); +} + export function classifyFailoverReason(raw: string): FailoverReason | null { if (isImageDimensionErrorMessage(raw)) return null; if (isImageSizeError(raw)) return null; @@ -505,6 +515,7 @@ export function classifyFailoverReason(raw: string): FailoverReason | null { if (isBillingErrorMessage(raw)) return "billing"; if (isTimeoutErrorMessage(raw)) return "timeout"; if (isAuthErrorMessage(raw)) return "auth"; + if (isProviderUnavailableErrorMessage(raw)) return "provider_unavailable"; return null; } diff --git a/src/agents/pi-embedded-helpers/types.ts b/src/agents/pi-embedded-helpers/types.ts index f76ee6dea..accac6459 100644 --- a/src/agents/pi-embedded-helpers/types.ts +++ b/src/agents/pi-embedded-helpers/types.ts @@ -1,3 +1,10 @@ export type EmbeddedContextFile = { path: string; content: string }; -export type FailoverReason = "auth" | "format" | "rate_limit" | "billing" | "timeout" | "unknown"; +export type FailoverReason = + | "auth" + | "format" + | "rate_limit" + | "billing" + | "timeout" + | "provider_unavailable" + | "unknown";