UNPKG

graphlit-client

Version:
125 lines (124 loc) 4.41 kB
/** * Internal types used by the streaming implementation * These are not exported to consumers of the library */ /** * Normalized error from an LLM provider. Carries structured metadata * so the retry layer can make decisions without parsing error messages. */ export class ProviderError extends Error { provider; statusCode; retryable; requestId; constructor(message, opts) { super(message, { cause: opts.cause }); this.name = "ProviderError"; this.provider = opts.provider; this.statusCode = opts.statusCode; this.retryable = opts.retryable; this.requestId = opts.requestId; } } /** * Detect common retryable server errors across providers. * Used as a catch-all after provider-specific error classification. */ export function isRetryableServerError(error) { const status = error.status ?? error.statusCode; if (typeof status === "number" && status >= 500) return true; const msg = (error.message || "").toLowerCase(); const type = (error.type || "").toLowerCase(); if (type === "api_error" || type === "server_error") return true; return (msg.includes("internal server error") || msg.includes("service unavailable") || msg.includes("bad gateway") || msg.includes("gateway timeout")); } /** * Extract an HTTP status code from common Apollo/network error shapes. */ export function getErrorStatusCode(error) { if (!error || typeof error !== "object") return undefined; const status = typeof error.status === "number" ? error.status : typeof error.statusCode === "number" ? error.statusCode : undefined; if (typeof status === "number") return status; const responseStatus = typeof error.response?.status === "number" ? error.response.status : undefined; if (typeof responseStatus === "number") return responseStatus; return (getErrorStatusCode(error.networkError) ?? getErrorStatusCode(error.cause)); } /** * Detect retryable GraphQL transport errors from Apollo RetryLink. * * RetryLink can receive either: * - an ApolloError with `networkError` * - a raw HttpLink `ServerError` with `statusCode` / `response.status` * - a nested network/cause error from undici/fetch */ export function isRetryableGraphQLTransportError(error, retryableStatusCodes = [429, 500, 502, 503, 504]) { if (!error) return false; const statusCode = getErrorStatusCode(error); if (typeof statusCode === "number") { return retryableStatusCodes.includes(statusCode); } if (Array.isArray(error.graphQLErrors) && error.graphQLErrors.length > 0) { return false; } return (!!error.networkError || isNetworkError(error) || isRetryableServerError(error) || isRetryableGraphQLTransportError(error.cause, retryableStatusCodes)); } /** * Detect rate-limit / overloaded errors across providers. */ export function isRateLimitError(error) { const status = error.status ?? error.statusCode; if (status === 429) return true; const type = (error.type || "").toLowerCase(); if (type === "rate_limit_error" || type === "overloaded_error") return true; if (error.code === "rate_limit_exceeded") return true; const msg = (error.message || "").toLowerCase(); return msg.includes("rate limit") || msg.includes("overloaded"); } /** * Detect transient network errors. * * Node's undici (built-in fetch) wraps TCP resets as: * TypeError: terminated * [cause]: Error: read ECONNRESET { code: 'ECONNRESET' } * * The ECONNRESET code lives on the nested `cause`, not the top-level error, * so we check both levels. */ export function isNetworkError(error) { const msg = error.message || ""; const code = error.code || ""; const causeCode = error.cause?.code || ""; return (msg.includes("fetch failed") || msg === "terminated" || code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNREFUSED" || causeCode === "ECONNRESET" || causeCode === "ETIMEDOUT" || causeCode === "ECONNREFUSED"); } /** Extract a request ID from a provider error, if present. */ export function extractRequestId(error) { return error.request_id ?? error.requestId ?? error.headers?.["x-request-id"]; }