Files
WallMuse/packages/provider-adapters/src/errors.ts
2026-05-09 09:12:41 +00:00

138 lines
4.9 KiB
TypeScript

import type { ProviderCode, ProviderError, ProviderErrorCategory } from "./types";
type ErrorLike = {
name?: string;
message?: string;
status?: number;
statusCode?: number;
code?: string;
response?: {
status?: number;
data?: unknown;
};
cause?: unknown;
};
export class NormalizedProviderError extends Error {
readonly providerError: ProviderError;
constructor(providerError: ProviderError) {
super(providerError.message);
this.name = "NormalizedProviderError";
this.providerError = providerError;
}
}
export function normalizeProviderError(provider: ProviderCode | string, error: unknown): ProviderError {
if (error instanceof NormalizedProviderError) {
return error.providerError;
}
const statusCode = getStatusCode(error);
const providerCode = getProviderErrorCode(error, statusCode);
const message = getProviderErrorMessage(error);
const category = categorizeProviderError(statusCode, providerCode, message);
return {
provider,
category,
code: providerCode,
message,
retryable: isRetryable(category, statusCode),
...(statusCode !== undefined ? { statusCode } : {}),
raw: error
};
}
export function providerHttpError(provider: ProviderCode | string, statusCode: number, body: unknown): ProviderError {
const code = getBodyErrorCode(body) ?? `HTTP_${statusCode}`;
const message = getBodyErrorMessage(body) ?? `Provider request failed with HTTP ${statusCode}`;
const category = categorizeProviderError(statusCode, code, message);
return {
provider,
category,
code,
message,
statusCode,
retryable: isRetryable(category, statusCode),
raw: body
};
}
function getStatusCode(error: unknown): number | undefined {
const value = error as ErrorLike;
return value?.statusCode ?? value?.status ?? value?.response?.status;
}
function getProviderErrorCode(error: unknown, statusCode?: number): string {
const value = error as ErrorLike;
const fromBody = getBodyErrorCode(value?.response?.data) ?? getBodyErrorCode(error);
return fromBody ?? value?.code ?? (statusCode ? `HTTP_${statusCode}` : "PROVIDER_ERROR");
}
function getProviderErrorMessage(error: unknown): string {
const value = error as ErrorLike;
return getBodyErrorMessage(value?.response?.data) ?? getBodyErrorMessage(error) ?? value?.message ?? "Provider request failed";
}
function getBodyErrorCode(body: unknown): string | undefined {
if (!body || typeof body !== "object") return undefined;
const record = body as Record<string, unknown>;
const nested = record.error && typeof record.error === "object" ? (record.error as Record<string, unknown>) : undefined;
const code = nested?.code ?? nested?.type ?? record.code ?? record.error_code;
return typeof code === "string" ? code : undefined;
}
function getBodyErrorMessage(body: unknown): string | undefined {
if (!body || typeof body !== "object") return undefined;
const record = body as Record<string, unknown>;
const nested = record.error && typeof record.error === "object" ? (record.error as Record<string, unknown>) : undefined;
const message = nested?.message ?? record.message ?? record.error_msg ?? record.detail;
return typeof message === "string" ? message : undefined;
}
function categorizeProviderError(statusCode: number | undefined, code: string, message: string): ProviderErrorCategory {
const normalized = `${code} ${message}`.toLowerCase();
if (statusCode === 401 || normalized.includes("invalid api key") || normalized.includes("unauthorized")) {
return "authentication";
}
if (statusCode === 403 || normalized.includes("permission") || normalized.includes("forbidden")) {
return normalized.includes("policy") || normalized.includes("safety") ? "content_policy" : "permission";
}
if (statusCode === 408 || normalized.includes("timeout") || normalized.includes("aborted")) {
return "timeout";
}
if (statusCode === 429 || normalized.includes("rate limit") || normalized.includes("too many request")) {
return "rate_limit";
}
if (normalized.includes("quota") || normalized.includes("insufficient") || normalized.includes("balance")) {
return "quota";
}
if (normalized.includes("content policy") || normalized.includes("safety") || normalized.includes("sensitive")) {
return "content_policy";
}
if (statusCode === 400 || statusCode === 422) {
return "bad_request";
}
if (statusCode === 404) {
return "not_found";
}
if (statusCode && statusCode >= 500) {
return "provider_unavailable";
}
if (normalized.includes("fetch failed") || normalized.includes("econn") || normalized.includes("network")) {
return "network";
}
return statusCode ? "provider_error" : "unknown";
}
function isRetryable(category: ProviderErrorCategory, statusCode?: number): boolean {
if (category === "rate_limit" || category === "timeout" || category === "network" || category === "provider_unavailable") {
return true;
}
return typeof statusCode === "number" && statusCode >= 500;
}