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.
{
"error": {
"message": "Human-readable description of what went wrong.",
"type": "not_found_error",
"code": "not_found"
}
}{
"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#
| Field | Type | Description |
|---|---|---|
400 | invalid_request_error · invalid_request | Malformed request, or content the provider refused. Retrying it unchanged fails identically. |
401 | authentication_error · authentication_error | Missing, malformed or revoked key. |
402 | insufficient_quota · insufficient_quota | Out of credit. Anthropic dialect reports this as billing_error. |
403 | permission_error · permission_denied | Valid key, but not entitled to this model. |
404 | not_found_error · not_found | Unknown model or job ID. Usually a missing vendor prefix. |
408 | timeout_error · timeout | The request exceeded the blocking limit. |
422 | invalid_request_error · invalid_request | Upstream rejected the request shape. |
429 | rate_limit_error · rate_limit_exceeded | Too many requests, or too much usage. |
500 | server_error · internal_error | Our side. Anthropic dialect reports api_error. |
502 | server_error · bad_gateway | Upstream unreachable. |
503 | server_error · service_unavailable | Upstream unavailable. Anthropic dialect: overloaded_error. |
529 | server_error · overloaded | Upstream 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#
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");
}