This commit is contained in:
Milofax 2026-01-30 11:55:32 +00:00 committed by GitHub
commit 761eace390
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 66 additions and 1 deletions

View File

@ -37,6 +37,7 @@ export type AuthProfileFailureReason =
| "rate_limit" | "rate_limit"
| "billing" | "billing"
| "timeout" | "timeout"
| "provider_unavailable"
| "unknown"; | "unknown";
/** Per-profile usage statistics for round-robin and cooldown tracking */ /** Per-profile usage statistics for round-robin and cooldown tracking */

View File

@ -47,6 +47,38 @@ describe("failover-error", () => {
expect(err?.status).toBe(400); 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", () => { it("describes non-Error values consistently", () => {
const described = describeFailoverError(123); const described = describeFailoverError(123);
expect(described.message).toBe("123"); expect(described.message).toBe("123");

View File

@ -50,6 +50,8 @@ export function resolveFailoverStatus(reason: FailoverReason): number | undefine
return 408; return 408;
case "format": case "format":
return 400; return 400;
case "provider_unavailable":
return 404;
default: default:
return undefined; return undefined;
} }

View File

@ -38,4 +38,16 @@ describe("classifyFailoverReason", () => {
"rate_limit", "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");
});
}); });

View File

@ -396,6 +396,12 @@ const ERROR_PATTERNS = {
"messages.1.content.1.tool_use.id", "messages.1.content.1.tool_use.id",
"invalid request format", "invalid request format",
], ],
providerUnavailable: [
"no endpoints found",
/no .* endpoints? available/,
"model is currently unavailable",
"model not available",
],
} as const; } as const;
const IMAGE_DIMENSION_ERROR_RE = const IMAGE_DIMENSION_ERROR_RE =
@ -496,6 +502,10 @@ export function isAuthAssistantError(msg: AssistantMessage | undefined): boolean
return isAuthErrorMessage(msg.errorMessage ?? ""); return isAuthErrorMessage(msg.errorMessage ?? "");
} }
export function isProviderUnavailableErrorMessage(raw: string): boolean {
return matchesErrorPatterns(raw, ERROR_PATTERNS.providerUnavailable);
}
export function classifyFailoverReason(raw: string): FailoverReason | null { export function classifyFailoverReason(raw: string): FailoverReason | null {
if (isImageDimensionErrorMessage(raw)) return null; if (isImageDimensionErrorMessage(raw)) return null;
if (isImageSizeError(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 (isBillingErrorMessage(raw)) return "billing";
if (isTimeoutErrorMessage(raw)) return "timeout"; if (isTimeoutErrorMessage(raw)) return "timeout";
if (isAuthErrorMessage(raw)) return "auth"; if (isAuthErrorMessage(raw)) return "auth";
if (isProviderUnavailableErrorMessage(raw)) return "provider_unavailable";
return null; return null;
} }

View File

@ -1,3 +1,10 @@
export type EmbeddedContextFile = { path: string; content: string }; 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";