import type { ModelProtocol } from "../protocol/canonical.js";
import {
MAX_OUTPUT_REACHED_PATTERN,
MULTIMODAL_PROCESSOR_PATTERN,
PROMPT_TOO_LONG_ANTHROPIC_PATTERN,
PROMPT_TOO_LONG_OPENAI_PATTERN,
REQUEST_TOO_LARGE_PATTERN,
type CanonicalModelError,
type CanonicalModelErrorCode,
} from "../protocol/errors.js";
export function normalizeModelError(
provider: string,
protocol: ModelProtocol,
error: unknown,
status?: number,
): CanonicalModelError {
const raw = error;
const record = isRecord(error) ? error : undefined;
const nestedError = record && isRecord(record.error) ? record.error : undefined;
const source = nestedError ?? record;
const message =
readString(source?.message) ??
(error instanceof Error ? error.message : undefined) ??
"Model provider request failed.";
const semanticCode = classifySemanticError(message, status, protocol);
const code: CanonicalModelErrorCode | (string & {}) =
semanticCode ?? readString(source?.code) ?? readString(source?.type) ?? statusCodeToCode(status);
const result: CanonicalModelError = {
provider,
protocol,
code,
status,
message,
retryable: isRetryable(status, code),
raw,
};
if (code === "prompt_too_long") {
result.recoverableViaCompact = true;
}
if (MULTIMODAL_PROCESSOR_PATTERN.test(message)) {
result.recoverableViaImageStrip = true;
}
return result;
}
function classifySemanticError(
message: string,
status: number | undefined,
protocol: ModelProtocol,
): CanonicalModelErrorCode | undefined {
if (PROMPT_TOO_LONG_ANTHROPIC_PATTERN.test(message)) {
return "prompt_too_long";
}
if (PROMPT_TOO_LONG_OPENAI_PATTERN.test(message)) {
return "prompt_too_long";
}
if (REQUEST_TOO_LARGE_PATTERN.test(message)) {
return "request_too_large";
}
if (MAX_OUTPUT_REACHED_PATTERN.test(message)) {
return "max_output_reached";
}
if (status === 413) {
return protocol === "anthropic" ? "request_too_large" : "request_too_large";
}
return undefined;
}
function isRetryable(status: number | undefined, code: string): boolean {
if (status === 408 || status === 409 || status === 429 || (status !== undefined && status >= 500)) {
return true;
}
return ["rate_limit_error", "overloaded_error", "timeout", "server_error"].includes(code);
}
function statusCodeToCode(status: number | undefined): string {
if (status === 401 || status === 403) {
return "auth_error";
}
if (status === 429) {
return "rate_limit_error";
}
if (status !== undefined && status >= 500) {
return "server_error";
}
return "provider_error";
}
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}