graphlit-client
Version:
Graphlit API Client for TypeScript
125 lines (124 loc) • 4.41 kB
JavaScript
/**
* 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"];
}