Errors and rate limits
The OpenAI error envelope, every status code /v1 returns, the rate limits, and what is safe to retry.
Every check, from the key and the request to the limits and the database, runs
before the turn starts. So a refused call is a real HTTP status, and no turn
has run. Only a failure after the turn started is different: a
frame on the stream, or a 5xx that says not to
retry.
The error envelope
Every error under /v1 uses OpenAI's envelope, so OpenAI-style SDKs raise
their usual exceptions. All four keys are always present; param and code
may be null.
{
"error": {
"message": "This field is not supported.",
"type": "invalid_request_error",
"param": "response_format",
"code": "invalid_request"
}
}Every response, error or not, carries an x-request-id header. Quote it when
reporting a problem.
An error body never echoes what was sent: a message the member typed does not come back in a validation error.
Routes outside /v1 (/auth/*, /documents/*, /chat/*) keep the
native envelope.
Status codes
| Status | type | code | When | OpenAI Python raises |
|---|---|---|---|---|
400 | invalid_request_error | invalid_request | The body is invalid or unreadable, a refused field is set, or a message breaks a rule. param names the field, for example messages.0.content | BadRequestError |
401 | authentication_error | invalid_api_key | The key is missing, unknown, expired, revoked or for the other environment | AuthenticationError |
401 | authentication_error | unauthorised | A member session was sent but is empty or malformed | AuthenticationError |
403 | permission_error | permission_denied | The key's role may not call /v1, or a tool message answers a call that is not waiting for an answer | PermissionDeniedError |
404 | invalid_request_error | unsupported_endpoint | Any other path or method under /v1, including POST /v1/responses and GET /v1/models | NotFoundError |
429 | rate_limit_error | rate_limited | Over a rate limit | RateLimitError |
503 | server_error | upstream_unavailable | A dependency, the database included, is unavailable. No turn ran | InternalServerError |
500 | server_error | internal_error | Anything else before the turn started. No detail is returned | InternalServerError |
A 401 unauthorised is about the member session, not your key: mint a
session with POST /auth/session and send its id, rather than rotating the key.
The 404 points at the endpoint to use:
{
"error": {
"message": "Use POST /v1/chat/completions (AI SDK: openai.chat() or @ai-sdk/openai-compatible)",
"type": "invalid_request_error",
"param": null,
"code": "unsupported_endpoint"
}
}After the turn started
Once the turn is running, a failure can no longer be a clean refusal. Text may already have been sent, and an action may already have been attempted.
Streaming: one error frame in the envelope above, then the stream closes,
with no [DONE]. Its type is always server_error. See
Errors mid-stream.
Non-streaming: a 503 if the failure is worth retrying later, otherwise a
500, with type: "server_error" and the header x-should-retry: false. The
OpenAI SDKs obey that header, so they do not replay the turn.
The code says what happened:
code | Means |
|---|---|
upstream_timeout | The turn ran past TURN_TIMEOUT_SECONDS (120 seconds by default), or a step inside it timed out. The message says whether anything may have changed |
upstream_unavailable | A dependency failed during the turn |
stream_interrupted | The turn ended without finishing |
loop_limit | The assistant hit its internal step limit |
internal_error | Anything else |
Treat it as an open set: other codes, such as forbidden, can appear.
Before sending the message again, read the error's message: when an action
may have gone through, it says to check first. A failed turn's streamed text
may be missing from the server's history, so the assistant may not know what
the member saw. Check the state of any action before asking again on the same
conversation.
Rate limits
Two limits apply to a chat turn. Both use a fixed one-minute window.
| Limit | Counts | Default |
|---|---|---|
| Per key | Every request the key makes, on every route, /auth/session included | 120 per minute, or the key's own limit if one was set (details) |
| Per member | Chat turns for one member session | 20 per minute (RATE_LIMIT_MESSAGES_PER_MINUTE) |
Anonymous calls (no session) count only against the key's limit.
Over either limit, the call is a 429 before anything runs, with two headers:
| Header | Meaning |
|---|---|
Retry-After | Whole seconds to wait, at least 1 |
retry-after-ms | The same wait in milliseconds |
What to retry
| Response | Retry? |
|---|---|
429 | Yes, after Retry-After. The OpenAI SDKs do this themselves unless retries are off |
503 before the turn | Yes, with backoff. No turn ran |
500 before the turn | At most once, then report it with the x-request-id |
400, 401, 403, 404 | No. Fix the request, the key or the session |
| Error after the turn started | Not automatically. Show what streamed, then let the member ask again |
Non-streaming calls need their own settings to stay safe; see Retries and timeouts.