fix: treat provider-unavailable errors as failover-eligible
OpenRouter returns 404 "No endpoints found that support tool use" when no provider endpoint supports the requested parameters (e.g. tools). This was not recognized as a failover reason, so configured fallback models were never tried. Add `provider_unavailable` to FailoverReason and match patterns like "no endpoints found" and "model is currently unavailable" so the fallback chain proceeds to the next candidate model. Closes #XXXX Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a109b7f1a9
commit
eb0457b30c
@ -37,6 +37,7 @@ export type AuthProfileFailureReason =
|
||||
| "rate_limit"
|
||||
| "billing"
|
||||
| "timeout"
|
||||
| "provider_unavailable"
|
||||
| "unknown";
|
||||
|
||||
/** Per-profile usage statistics for round-robin and cooldown tracking */
|
||||
|
||||
@ -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");
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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");
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
Reference in New Issue
Block a user