Files
OmniRoute/open-sse/services/errorClassifier.ts
clousky2020 d6cbdc1580 feat: add ModelScope provider with circuit breaker and daily quota lock (#1430)
Integrated into release/v3.7.0. Thanks @clousky2020 for this massive and important contribution! 🎉 We've translated the deprecation comments to English for consistency, and it is now officially merged into the release branch. Great work on the ModelScope integration and Circuit Breaker!
2026-04-19 21:13:45 -03:00

139 lines
4.2 KiB
TypeScript

import {
isAccountDeactivated,
isCreditsExhausted,
isDailyQuotaExhausted,
isOAuthInvalidToken,
} from "./accountFallback.ts";
export function isEmptyContentResponse(responseBody: unknown): boolean {
if (!responseBody || typeof responseBody !== "object") return false;
const body = responseBody as Record<string, unknown>;
if (Array.isArray(body.choices)) {
const firstChoice = body.choices[0] as Record<string, unknown> | undefined;
if (!firstChoice) return true;
const message = firstChoice.message as Record<string, unknown> | undefined;
const delta = firstChoice.delta as Record<string, unknown> | undefined;
const content = message?.content ?? delta?.content;
const reasoningContent = message?.reasoning_content ?? delta?.reasoning_content;
const hasToolCalls =
(Array.isArray(message?.tool_calls) && (message.tool_calls as unknown[]).length > 0) ||
(Array.isArray(delta?.tool_calls) && (delta.tool_calls as unknown[]).length > 0);
const hasContent = content !== null && content !== undefined && content !== "";
const hasReasoning =
reasoningContent !== null && reasoningContent !== undefined && reasoningContent !== "";
return !hasContent && !hasReasoning && !hasToolCalls;
}
if (Array.isArray(body.content)) {
return body.content.length === 0;
}
if (typeof body.text === "string") {
return body.text.trim() === "";
}
if ("content" in body) {
const content = body.content;
return content === null || content === undefined || content === "";
}
return false;
}
export const PROVIDER_ERROR_TYPES = {
RATE_LIMITED: "rate_limited",
UNAUTHORIZED: "unauthorized",
ACCOUNT_DEACTIVATED: "account_deactivated",
FORBIDDEN: "forbidden",
SERVER_ERROR: "server_error",
QUOTA_EXHAUSTED: "quota_exhausted",
PROJECT_ROUTE_ERROR: "project_route_error",
CONTEXT_OVERFLOW: "context_overflow",
OAUTH_INVALID_TOKEN: "oauth_invalid_token",
EMPTY_CONTENT: "empty_content",
};
export const CONTEXT_OVERFLOW_SIGNALS = [
"context overflow",
"prompt too large",
"context window",
"maximum context",
"exceeds context",
"input too long",
"token limit",
"too many tokens",
"context length",
"exceed.*context",
"messages exceed",
];
export const CONTEXT_OVERFLOW_REGEX = new RegExp(CONTEXT_OVERFLOW_SIGNALS.join("|"), "i");
export function isContextOverflow(errorText: string): boolean {
return CONTEXT_OVERFLOW_REGEX.test(String(errorText || ""));
}
function responseBodyToString(responseBody: unknown): string {
if (typeof responseBody === "string") return responseBody;
if (responseBody !== null && typeof responseBody === "object") {
try {
return JSON.stringify(responseBody);
} catch {
return "";
}
}
return "";
}
export function classifyProviderError(statusCode: number, responseBody: unknown): string | null {
const bodyStr = responseBodyToString(responseBody);
const creditsExhausted = isCreditsExhausted(bodyStr);
const accountDeactivated = isAccountDeactivated(bodyStr);
const oauthInvalid = isOAuthInvalidToken(bodyStr);
if (creditsExhausted && [400, 402, 403, 429].includes(statusCode)) {
return PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED;
}
// 429: Check if it's a daily quota exhaustion (lock until tomorrow) vs regular rate limit
if (statusCode === 429) {
if (isDailyQuotaExhausted(bodyStr)) {
return PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED;
}
return PROVIDER_ERROR_TYPES.RATE_LIMITED;
}
if (statusCode === 401) {
if (oauthInvalid) {
return PROVIDER_ERROR_TYPES.OAUTH_INVALID_TOKEN;
}
return accountDeactivated
? PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED
: PROVIDER_ERROR_TYPES.UNAUTHORIZED;
}
if (statusCode === 402) return PROVIDER_ERROR_TYPES.QUOTA_EXHAUSTED;
if (statusCode === 403 && accountDeactivated) {
return PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED;
}
if (statusCode === 403) {
if (bodyStr.includes("has not been used in project")) {
return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR;
}
return PROVIDER_ERROR_TYPES.FORBIDDEN;
}
if (statusCode >= 500) return PROVIDER_ERROR_TYPES.SERVER_ERROR;
if (statusCode === 400 && isContextOverflow(bodyStr)) {
return PROVIDER_ERROR_TYPES.CONTEXT_OVERFLOW;
}
return null;
}