src / errors.ts
/* -------------------------------------------------------------------------- */
/* Provider error classification + friendly messaging */
/* */
/* Table-driven: adding a recognized failure mode = one row below. No control */
/* flow changes required. */
/* -------------------------------------------------------------------------- */
export type ProviderErrorInfo = {
status?: number;
code?: string;
providerMessage?: string;
apiMessage?: string;
};
/** Explicit provider error codes -> user-friendly message. */
const CODE_MESSAGES: Record<string, (pm?: string) => string> = {
context_length_exceeded: () => "Prompt too long: exceeds the model's context limit. Start a new chat or reduce context.",
capacity_exhausted: () => "Server out of memory. Try again later or reduce context size.",
authentication_error: () => "Invalid or missing API key. Check your settings.",
model_not_found: (pm) => `Model not found. Check the model name in settings.${pm ? ` (${pm})` : ""}`,
engine_recovering: () => "Server is recovering. Try again in a few seconds.",
engine_failed: () => "Server engine failed. The server needs to be restarted.",
frontend_overloaded: () => "Server is overloaded. Try again in a few seconds.",
resource_timeout: () => "Server ran out of memory while processing. Try again or reduce context.",
mask_timeout: () => "Server internal timeout. Try again.",
runtime_busy: () => "Server is busy. Try again in a few seconds.",
runtime_unavailable: () => "Server runtime unavailable. Try again in a few seconds.",
request_timeout: () => "Request timed out. Try reducing context or increasing timeout.",
request_too_large: () => "Request body too large. Reduce the prompt/request size.",
server_shutdown: () => "Server is shutting down. Try again later.",
forbidden: (pm) => `Access forbidden. ${pm ?? "Check server --allowed-host / --allowed-origin settings."}`,
model_result_invalid: () => "Model produced an invalid result. Try again.",
};
/** HTTP status codes -> user-friendly message (consulted when no explicit code is present). */
const STATUS_MESSAGES: Record<number, (pm?: string) => string> = {
401: () => "Invalid or missing API key. Check your settings.",
403: (pm) => `Access forbidden. ${pm ?? ""}`,
404: (pm) => `Not found. ${pm ?? "Check the model name."}`,
413: () => "Request too large.",
429: (pm) => `Rate limit exceeded. Try again later.${pm ? ` (${pm})` : ""}`,
500: (pm) => `Server error. ${pm ?? "Try again later."}`,
503: (pm) => `Server unavailable. ${pm ?? "Try again later."}`,
504: () => "Request timed out.",
};
/** Narrow an unknown value to a plain object record, or null. */
function asPlainObject(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
/** Extract a non-empty string, else undefined. */
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
/** Extract a finite number, else undefined. */
function readNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
/**
* Best-effort extraction of structured error info from an opaque thrown value.
* Probes the common envelope shapes used by OpenAI-compatible gateways. Operates purely on
* `unknown` with guarded readers — no `any` escapes at the boundary.
*/
export function extractProviderError(error: unknown): ProviderErrorInfo {
const root = asPlainObject(error);
const response = asPlainObject(root?.response);
const responseData = asPlainObject(response?.data);
const errorObj =
asPlainObject(root?.error) ?? asPlainObject(responseData?.error) ?? asPlainObject(response?.error);
const status = readNumber(root?.status) ?? readNumber(response?.status) ?? readNumber(root?.code);
const apiMessage = readString(root?.message);
const providerMessage =
readString(errorObj?.message) ?? readString(responseData?.message) ?? apiMessage;
const code = readString(errorObj?.code) ?? readString(asPlainObject(responseData?.error)?.code);
return { status, code, providerMessage, apiMessage };
}
/** Map classified error info onto a concise, actionable message. */
export function formatErrorMessage(info: ProviderErrorInfo): string {
if (info.code && CODE_MESSAGES[info.code]) return CODE_MESSAGES[info.code](info.providerMessage);
if (info.status != null && STATUS_MESSAGES[info.status]) return STATUS_MESSAGES[info.status](info.providerMessage);
return `API error: ${info.providerMessage ?? info.apiMessage ?? "Unknown error"}`;
}
src / errors.ts
/* -------------------------------------------------------------------------- */
/* Provider error classification + friendly messaging */
/* */
/* Table-driven: adding a recognized failure mode = one row below. No control */
/* flow changes required. */
/* -------------------------------------------------------------------------- */
export type ProviderErrorInfo = {
status?: number;
code?: string;
providerMessage?: string;
apiMessage?: string;
};
/** Explicit provider error codes -> user-friendly message. */
const CODE_MESSAGES: Record<string, (pm?: string) => string> = {
context_length_exceeded: () => "Prompt too long: exceeds the model's context limit. Start a new chat or reduce context.",
capacity_exhausted: () => "Server out of memory. Try again later or reduce context size.",
authentication_error: () => "Invalid or missing API key. Check your settings.",
model_not_found: (pm) => `Model not found. Check the model name in settings.${pm ? ` (${pm})` : ""}`,
engine_recovering: () => "Server is recovering. Try again in a few seconds.",
engine_failed: () => "Server engine failed. The server needs to be restarted.",
frontend_overloaded: () => "Server is overloaded. Try again in a few seconds.",
resource_timeout: () => "Server ran out of memory while processing. Try again or reduce context.",
mask_timeout: () => "Server internal timeout. Try again.",
runtime_busy: () => "Server is busy. Try again in a few seconds.",
runtime_unavailable: () => "Server runtime unavailable. Try again in a few seconds.",
request_timeout: () => "Request timed out. Try reducing context or increasing timeout.",
request_too_large: () => "Request body too large. Reduce the prompt/request size.",
server_shutdown: () => "Server is shutting down. Try again later.",
forbidden: (pm) => `Access forbidden. ${pm ?? "Check server --allowed-host / --allowed-origin settings."}`,
model_result_invalid: () => "Model produced an invalid result. Try again.",
};
/** HTTP status codes -> user-friendly message (consulted when no explicit code is present). */
const STATUS_MESSAGES: Record<number, (pm?: string) => string> = {
401: () => "Invalid or missing API key. Check your settings.",
403: (pm) => `Access forbidden. ${pm ?? ""}`,
404: (pm) => `Not found. ${pm ?? "Check the model name."}`,
413: () => "Request too large.",
429: (pm) => `Rate limit exceeded. Try again later.${pm ? ` (${pm})` : ""}`,
500: (pm) => `Server error. ${pm ?? "Try again later."}`,
503: (pm) => `Server unavailable. ${pm ?? "Try again later."}`,
504: () => "Request timed out.",
};
/** Narrow an unknown value to a plain object record, or null. */
function asPlainObject(value: unknown): Record<string, unknown> | null {
return value !== null && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
/** Extract a non-empty string, else undefined. */
function readString(value: unknown): string | undefined {
return typeof value === "string" && value.length > 0 ? value : undefined;
}
/** Extract a finite number, else undefined. */
function readNumber(value: unknown): number | undefined {
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
}
/**
* Best-effort extraction of structured error info from an opaque thrown value.
* Probes the common envelope shapes used by OpenAI-compatible gateways. Operates purely on
* `unknown` with guarded readers — no `any` escapes at the boundary.
*/
export function extractProviderError(error: unknown): ProviderErrorInfo {
const root = asPlainObject(error);
const response = asPlainObject(root?.response);
const responseData = asPlainObject(response?.data);
const errorObj =
asPlainObject(root?.error) ?? asPlainObject(responseData?.error) ?? asPlainObject(response?.error);
const status = readNumber(root?.status) ?? readNumber(response?.status) ?? readNumber(root?.code);
const apiMessage = readString(root?.message);
const providerMessage =
readString(errorObj?.message) ?? readString(responseData?.message) ?? apiMessage;
const code = readString(errorObj?.code) ?? readString(asPlainObject(responseData?.error)?.code);
return { status, code, providerMessage, apiMessage };
}
/** Map classified error info onto a concise, actionable message. */
export function formatErrorMessage(info: ProviderErrorInfo): string {
if (info.code && CODE_MESSAGES[info.code]) return CODE_MESSAGES[info.code](info.providerMessage);
if (info.status != null && STATUS_MESSAGES[info.status]) return STATUS_MESSAGES[info.status](info.providerMessage);
return `API error: ${info.providerMessage ?? info.apiMessage ?? "Unknown error"}`;
}