Files
2026-07-13 13:39:12 +08:00

202 lines
6.3 KiB
TypeScript

export type ErrorInfo = {
type: string;
code: string;
};
export type ConfiguredErrorReason =
| "auth_error"
| "quota_exhausted"
| "rate_limit_exceeded"
| "model_capacity"
| "server_error"
| "unknown";
export type ErrorRule = {
id: string;
text?: string;
status?: number;
reason?: ConfiguredErrorReason;
cooldownMs?: number;
backoff?: boolean;
};
// OpenAI-compatible error types mapping (client-facing)
export const ERROR_TYPES: Record<number, ErrorInfo> = {
400: { type: "invalid_request_error", code: "bad_request" },
401: { type: "authentication_error", code: "invalid_api_key" },
402: { type: "billing_error", code: "payment_required" },
403: { type: "permission_error", code: "insufficient_quota" },
404: { type: "invalid_request_error", code: "model_not_found" },
406: { type: "invalid_request_error", code: "model_not_supported" },
429: { type: "rate_limit_error", code: "rate_limit_exceeded" },
500: { type: "server_error", code: "internal_server_error" },
502: { type: "server_error", code: "bad_gateway" },
503: { type: "server_error", code: "service_unavailable" },
504: { type: "server_error", code: "gateway_timeout" },
};
// Default error messages per status code (client-facing)
export const DEFAULT_ERROR_MESSAGES: Record<number, string> = {
400: "Bad request",
401: "Invalid API key provided",
402: "Payment required",
403: "You exceeded your current quota",
404: "Model not found",
406: "Model not supported",
429: "Rate limit exceeded",
500: "Internal server error",
502: "Bad gateway - upstream provider error",
503: "Service temporarily unavailable",
504: "Gateway timeout",
};
// Exponential backoff config for rate limits.
// Preserve OmniRoute's existing 2-minute cap to avoid changing runtime behavior.
export const BACKOFF_CONFIG = {
base: 1000,
max: 2 * 60 * 1000,
maxLevel: 15,
};
export const TRANSIENT_COOLDOWN_MS = 5 * 1000;
// Cooldown durations (ms)
export const COOLDOWN_MS = {
unauthorized: 2 * 60 * 1000,
paymentRequired: 2 * 60 * 1000,
notFound: 2 * 60 * 1000,
notFoundLocal: 5 * 1000,
transientInitial: TRANSIENT_COOLDOWN_MS,
transientMax: 60 * 1000,
transient: TRANSIENT_COOLDOWN_MS,
requestNotAllowed: 5 * 1000,
rateLimit: 2 * 60 * 1000,
serviceUnavailable: 2 * 1000,
authExpired: 2 * 60 * 1000,
};
/**
* Shared rules for account fallback classification.
* Checked top-to-bottom: text rules first, then status rules.
*/
export const ERROR_RULES: ErrorRule[] = [
{
id: "no_credentials",
text: "no credentials",
cooldownMs: COOLDOWN_MS.notFound,
reason: "auth_error",
},
{
id: "request_not_allowed",
text: "request not allowed",
cooldownMs: COOLDOWN_MS.requestNotAllowed,
reason: "rate_limit_exceeded",
},
{
id: "improperly_formed_request",
text: "improperly formed request",
cooldownMs: 0,
reason: "model_capacity",
},
{ id: "rate_limit", text: "rate limit", backoff: true, reason: "rate_limit_exceeded" },
{
id: "too_many_requests",
text: "too many requests",
backoff: true,
reason: "rate_limit_exceeded",
},
{
id: "hour_quota_exceeded",
text: "hour quota",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_has_been_exceeded",
text: "quota has been exceeded",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_exceeded",
text: "quota exceeded",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_will_reset",
text: "quota will reset",
backoff: true,
reason: "quota_exhausted",
},
{
id: "capacity_exhausted",
text: "exhausted your capacity",
backoff: true,
reason: "quota_exhausted",
},
{
id: "quota_exhausted",
text: "quota exhausted",
backoff: true,
reason: "quota_exhausted",
},
{
id: "free_tier_exhausted",
text: "free tier of the model has been exhausted",
backoff: true,
reason: "quota_exhausted",
},
{ id: "capacity", text: "capacity", backoff: true, reason: "model_capacity" },
{ id: "overloaded", text: "overloaded", backoff: true, reason: "model_capacity" },
{ id: "high_demand", text: "high demand", backoff: true, reason: "model_capacity" },
{ id: "status_401", status: 401, cooldownMs: 0, reason: "auth_error" },
{ id: "status_402", status: 402, cooldownMs: 0, reason: "quota_exhausted" },
{ id: "status_403", status: 403, cooldownMs: 0, reason: "quota_exhausted" },
{ id: "status_404", status: 404, cooldownMs: COOLDOWN_MS.notFound, reason: "unknown" },
{ id: "status_406", status: 406, backoff: true, reason: "server_error" },
{ id: "status_408", status: 408, backoff: true, reason: "server_error" },
{ id: "status_429", status: 429, backoff: true, reason: "rate_limit_exceeded" },
{ id: "status_500", status: 500, backoff: true, reason: "server_error" },
{ id: "status_502", status: 502, backoff: true, reason: "server_error" },
{ id: "status_503", status: 503, backoff: true, reason: "server_error" },
{ id: "status_504", status: 504, backoff: true, reason: "server_error" },
];
function normalizeErrorMessage(message: unknown): string {
return String(message || "").toLowerCase();
}
export function getErrorInfo(statusCode: number): ErrorInfo {
return (
ERROR_TYPES[statusCode] ||
(statusCode >= 500
? { type: "server_error", code: "internal_server_error" }
: { type: "invalid_request_error", code: "" })
);
}
export function getDefaultErrorMessage(statusCode: number): string {
return DEFAULT_ERROR_MESSAGES[statusCode] || "An error occurred";
}
export function calculateBackoffCooldown(level = 0): number {
const safeLevel = Math.max(0, Math.floor(level));
const cooldown = BACKOFF_CONFIG.base * Math.pow(2, safeLevel);
return Math.min(cooldown, BACKOFF_CONFIG.max);
}
export function matchErrorRuleByText(message: unknown): ErrorRule | null {
const lower = normalizeErrorMessage(message);
if (!lower) return null;
return ERROR_RULES.find((rule) => rule.text && lower.includes(rule.text)) || null;
}
export function matchErrorRuleByStatus(statusCode: number): ErrorRule | null {
return ERROR_RULES.find((rule) => rule.status === statusCode) || null;
}
export function findMatchingErrorRule(statusCode: number, message: unknown): ErrorRule | null {
return matchErrorRuleByText(message) || matchErrorRuleByStatus(statusCode);
}