GlexirDocs
Dashboard

SDK

Errors & retries

When a request fails, the SDK throws an error class you can check with instanceof. Before throwing, it retries failures that are likely to be temporary. Every retried POST reuses the same idempotency key, so the gateway never runs or bills it twice.

TypeScript
import Glexir, { APIConnectionError, APIError, RateLimitError } from "@glexir/sdk";

const glexir = new Glexir({ apiKey: process.env.GLEXIR_API_KEY });

try {
  const response = await glexir.responses.create({ model: "gpt-5.4", input: "Hello" });
  console.log(response.output_text);
} catch (err) {
  if (err instanceof RateLimitError) {
    console.log("Rate limited after retries:", err.code, err.requestID);
  } else if (err instanceof APIConnectionError) {
    console.log("Network problem:", err.message); // also covers timeouts
  } else if (err instanceof APIError) {
    console.log(err.status, err.code, err.message, err.requestID);
  } else {
    throw err;
  }
}

Check connection errors before APIError

APIConnectionError, APIConnectionTimeoutError and APIUserAbortError are subclasses of APIError with status set to undefined. Check for them before a general instanceof APIError branch, or check err.status !== undefined.

Error classes

All errors the SDK creates extend GlexirError, which extends Error. Each error's name is its class name.

Output
GlexirError
├── APIError
│   ├── BadRequestError            400
│   ├── AuthenticationError        401
│   ├── PermissionDeniedError      403
│   ├── NotFoundError              404
│   ├── ConflictError              409
│   ├── UnprocessableEntityError   422
│   ├── RateLimitError             429
│   ├── InternalServerError        500 and above
│   ├── APIConnectionError         status undefined
│   │   └── APIConnectionTimeoutError
│   └── APIUserAbortError          status undefined
├── LengthFinishReasonError
├── ContentFilterFinishReasonError
└── StrictSchemaError

All of these classes are exported from @glexir/sdk, along with the APIErrorBody type.

APIError

APIError is thrown for any non-2xx response, and for error events inside a stream.

APIError fields
statusnumber | undefinedoptional
The HTTP status. undefined for connection errors, aborts, and errors that arrive mid-stream.
messagestringoptional
The status followed by the API's message, for example "400 Unknown model". If the body is not JSON, the raw body text is used. With no body: "<status> status code (no body)".
codestring | null | undefinedoptional
The machine-readable error.code, for example "rate_limit_exceeded". Branch on this, not on the message.
typestring | undefinedoptional
The error.type, for example "invalid_request_error" or "insufficient_quota".
paramstring | null | undefinedoptional
The request parameter the error is about, if any.
errorAPIErrorBody | undefinedoptional
The whole error object from the body: message, type, code, param and any extra fields.
headersHeaders | undefinedoptional
The response headers. Undefined for connection errors and aborts.
requestIDstring | null | undefinedoptional
The x-request-id header. Include it when you contact support.

APIError.generate(status, body, message, headers) is the static factory the SDK uses to build the right subclass from a response. You don't need to call it, but it is handy in tests to create the same errors the SDK throws.

The gateway returns errors in OpenAI's shape:

JSON
{
  "error": {
    "message": "Rate limit reached: 60 requests per minute for your account tier.",
    "type": "rate_limit_error",
    "param": null,
    "code": "rate_limit_exceeded"
  }
}

Status codes

StatusClassRetried
400BadRequestErrorNo
401AuthenticationErrorNo
402APIErrorNo
403PermissionDeniedErrorNo
404NotFoundErrorNo
408APIErrorYes
409ConflictErrorYes
413APIErrorNo
422UnprocessableEntityErrorNo
429RateLimitErrorYes
500 and aboveInternalServerErrorYes
Any other statusAPIErrorNo

Statuses without their own class, such as 402 (out of credit or over a spend limit) and 413 (request or file too large), throw a plain APIError. Check err.status and err.code for them:

TypeScript
import Glexir, { APIError } from "@glexir/sdk";

const glexir = new Glexir({ apiKey: process.env.GLEXIR_API_KEY });

try {
  await glexir.responses.create({ model: "gpt-5.4", input: "Hello" });
} catch (err) {
  if (err instanceof APIError && err.status === 402) {
    // err.code is "credit_balance_exhausted" or "spend_limit_exceeded"
    console.log("Add credit or raise your spend limit:", err.message);
  } else {
    throw err;
  }
}

Every status and code the gateway returns is listed in HTTP error codes.

Connection errors, timeouts and aborts

ClassWhenmessage
APIConnectionErrorfetch threw (DNS failure, refused connection, reset) and retries ran out. The original error is on err.cause."Connection error."
APIConnectionTimeoutErrorThe last attempt hit the timeout."Request timed out."
APIUserAbortErrorYour signal fired, before or during a request or while waiting to retry. Never retried."Request was aborted."

Aborting a stream after its response has started behaves differently; see Aborting a stream.

Errors in streams

If the API sends an error event after a stream has started, iteration throws an APIError with status set to undefined. Its code, type, message and param come from the event. These errors are not retried, because the stream already started. Branch on err.code. See Errors during a stream.

Other errors

ClassThrown by
LengthFinishReasonErrorresponses.parse() when the response is incomplete because of max_output_tokens, and chat.completions.parse() when a choice has finish_reason: "length".
ContentFilterFinishReasonErrorThe same parse() methods when output was cut off by the content filter.
StrictSchemaErrortoStrictJsonSchema() when a schema cannot be made strict, for example an optional property that is not nullable, or a keyword such as oneOf.
GlexirErrorSetup and usage mistakes: no API key (from the constructor), toFile() without a filename, iterating a stream twice, or a stream event that is not valid JSON.

Retries

The SDK retries a request when:

  • fetch throws (a network error), or the attempt times out.
  • The response status is 408, 409, 429, or 500 and above.

It does not retry other statuses, an abort from your signal, or an error that arrives inside a stream that has already started.

The server can override the status check with an x-should-retry header: true forces a retry and false prevents one.

Number of retries

The default is 2 retries, so up to 3 attempts. Set maxRetries on the client, or per call:

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir({
  apiKey: process.env.GLEXIR_API_KEY,
  maxRetries: 5,
});

// No retries for this call.
await glexir.models.list({ maxRetries: 0 });

Each attempt sends x-glexir-retry-count (0, 1, 2, ...). When retries run out, the SDK throws the error from the last attempt.

Backoff

Before a retry, the SDK waits:

  1. The retry-after-ms header, in milliseconds, if present.
  2. Otherwise the retry-after header, as seconds or as an HTTP date.
  3. If neither header is present, or the value is negative or longer than 60 seconds, exponential backoff: min(0.5 × 2^n, 8) seconds, where n is the number of retries so far, reduced by a random 0 to 25%.

Network errors and timeouts always use exponential backoff.

RetryBase delayWith jitter
1st0.5 s0.375 to 0.5 s
2nd1 s0.75 to 1 s
3rd2 s1.5 to 2 s
4th4 s3 to 4 s
5th and later8 s6 to 8 s

Aborting your signal during the wait stops it and throws APIUserAbortError.

Idempotency

The SDK sends an Idempotency-Key header on every non-GET request (POST and DELETE). It generates one random UUID per method call and reuses it on every retry of that call. To set your own key, for example one tied to a job ID so a restarted process does not repeat work, pass idempotencyKey:

TypeScript
import Glexir from "@glexir/sdk";

const glexir = new Glexir({ apiKey: process.env.GLEXIR_API_KEY });

const response = await glexir.responses.create(
  { model: "gpt-5.4", input: "Summarize ticket 4812." },
  { idempotencyKey: "summarize-ticket-4812" },
);
console.log(response.output_text);

How the gateway uses the key

The gateway applies idempotency to POST requests. Keys are scoped to your organization and remembered for 24 hours.

  • First request with a key. The gateway runs it. If it succeeds, the status and body are stored. If it fails with any error response (4xx or 5xx), the gateway forgets the key, so a retry with the same key runs fresh. For example, after a 402 you can add credit and retry with the same key.
  • Same key, same request, finished. The gateway returns the stored status and body without calling the model again or billing you. The response carries idempotent-replayed: true.
  • Same key, still running. 409 with code idempotency_request_in_progress. This happens, for example, when an attempt timed out on your side while the gateway was still working on it. The SDK retries 409, so a later retry can get the stored result once the original finishes.
  • Same key, different request. 422 with code idempotency_key_reused (UnprocessableEntityError). Use a new key for a new request.
  • Same key, original was streamed. Streams cannot be replayed: 409 with code idempotency_stream_replay (ConflictError). The SDK retries it like any 409 before throwing.
  • Key longer than 255 characters. 400 (BadRequestError).

"Same request" means the same method, path and JSON body. For multipart uploads (files and audio) the gateway compares only the body size, so do not reuse a key for a different file.