Errors

Every error follows the shape of the dialect you called, so your existing SDK error handling keeps working.

Two shapes#

The endpoint you call decides the shape. This holds for every failure — authentication, validation, rate limits and provider errors alike — so you only ever parse one format per integration.

/v1/chat/completions and other OpenAI-style routes
{
  "error": {
    "message": "Human-readable description of what went wrong.",
    "type": "not_found_error",
    "code": "not_found"
  }
}
/v1/messages
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "Human-readable description of what went wrong."
  }
}

Branch on type and code, not on the message

Messages are written for people and get reworded. type and code are stable identifiers — build your logic on those.

Status codes#

type · code, exactly as returned (source: STATUS_MAP)
FieldTypeDescription
400invalid_request_error · invalid_requestMalformed request, or content the provider refused. Retrying it unchanged fails identically.
401authentication_error · authentication_errorMissing, malformed or revoked key.
402insufficient_quota · insufficient_quotaOut of credit. Anthropic dialect reports this as billing_error.
403permission_error · permission_deniedValid key, but not entitled to this model.
404not_found_error · not_foundUnknown model or job ID. Usually a missing vendor prefix.
408timeout_error · timeoutThe request exceeded the blocking limit.
422invalid_request_error · invalid_requestUpstream rejected the request shape.
429rate_limit_error · rate_limit_exceededToo many requests, or too much usage.
500server_error · internal_errorOur side. Anthropic dialect reports api_error.
502server_error · bad_gatewayUpstream unreachable.
503server_error · service_unavailableUpstream unavailable. Anthropic dialect: overloaded_error.
529server_error · overloadedUpstream overloaded.

Which errors are worth retrying#

This is the distinction that matters most, and the one most often got wrong.

  • Retry: 429, 500, 502, 503. These are about capacity and timing — the same request can succeed a moment later.
  • Do not retry: 400, 401, 403, 404. These are about the request itself, and it will not become valid on its own.
  • Do not retry blindly: 402. Retrying will not create balance; surface it to whoever can top up.

Content refusals are 400, on purpose

When a provider blocks your prompt or an image, the status is 400 rather than 500. That is deliberate: 500tells every SDK "try again", so a refusal returned as 500 makes clients hammer a request that can never succeed.

Common errors in detail#

Model not found

{
  "error": {
    "message": "Model \"gpt-5.6-sol\" not found.",
    "type": "not_found_error",
    "code": "not_found"
  }
}

Almost always a missing vendor prefix. Use openai/gpt-5.6-sol, not gpt-5.6-sol. See Models.

Context window exceeded

{
  "error": {
    "message": "Input is ~164970 tokens, which exceeds the safe limit of 160000 tokens for this model. Shorten the input or use a model with a larger context window.",
    "type": "invalid_request_error",
    "code": "invalid_request"
  }
}

Caught before any provider is called, so this costs you nothing. Trim the conversation history or move to a model with a larger window.

Content blocked

{
  "error": {
    "message": "The request was blocked by the provider's content policy.",
    "type": "invalid_request_error",
    "code": "invalid_request"
  }
}

The message names which part was rejected — your text, an image you sent, or the model's own output — because the fix differs in each case. Output refusals in particular cannot be fixed by editing your input.

Insufficient balance

{
  "error": {
    "message": "Insufficient balance. Top up your wallet to continue.",
    "type": "insufficient_quota",
    "code": "insufficient_quota"
  }
}

Errors during a stream#

Once streaming has begun the status is already 200, so failures arrive as an event. See Streaming for a reader that handles them.

A retry helper#

Exponential backoff with jitter
const RETRYABLE = new Set([429, 500, 502, 503]);

async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (err) {
      const status = (err as { status?: number }).status;

      // Last attempt, or an error that will never fix itself: fail now rather
      // than burning the remaining attempts on a guaranteed failure.
      if (i === attempts - 1 || !status || !RETRYABLE.has(status)) throw err;

      // Jitter matters: without it, every client that failed together retries
      // together and recreates the spike that caused the failure.
      const backoff = 2 ** i * 1000;
      await new Promise((r) => setTimeout(r, backoff + Math.random() * 300));
    }
  }
  throw new Error("unreachable");
}