Errors
Errors come back in the dialect of the endpoint you called, so your SDK's own error classes work unchanged. Every error response — every response at all — carries an x-request-id header. Quote it when you write to support@princep.org.
The two envelopes#
OpenAI wire (/v1/chat/completions, and everything that is not a completion endpoint, including a 404 for an unknown path — /v1/messages/count_tokens is the one exception and speaks the Anthropic envelope):
{"error": {"message": "…", "type": "invalid_request_error", "param": null, "code": "invalid_api_key"}}Anthropic wire (/v1/messages):
{"type": "error", "error": {"type": "authentication_error", "message": "…"}, "request_id": "…"}The envelope is chosen from the path you requested, so a request that fails before it is even parsed still fails in the shape your client expects.
Anthropic error types#
On the Anthropic wire, error.type follows Anthropic's documented mapping from the HTTP status, which is what its SDKs' typed exceptions are built on:
| Status | error.type |
|---|---|
| 400 | invalid_request_error |
| 401 | authentication_error |
| 402 | billing_error |
| 403 | permission_error |
| 404 | not_found_error |
| 413 | request_too_large |
| 429 | rate_limit_error |
| anything else | api_error |
Every status and code#
type and code are the OpenAI-wire values; the message is the same on both wires.
| Status | type | code | param | When | Retry? |
|---|---|---|---|---|---|
| 400 | invalid_request_error | invalid_body | — | Body is not a JSON object | No — fix the request |
| 400 | invalid_request_error | invalid_body | model | model missing or not a string | No |
| 400 | invalid_request_error | invalid_body | max_tokens | Anthropic wire: max_tokens missing or not positive | No |
| 400 | invalid_request_error | invalid_cache_key | prompt_cache_key or x-session-id | Conversation key over 256 characters | No |
| 400 / 404 / 422 | invalid_request_error | invalid_request | — | The model rejected the request (too long, unsupported field, content filter); its message is relayed | No |
| 401 | invalid_request_error | invalid_api_key | — | Key missing, unknown or revoked | No — check the key |
| 401 | invalid_request_error | account_suspended | — | The account is not active | No — contact support |
| 402 | insufficient_quota | insufficient_credits | — | Balance is $0 or below | After adding credit |
| 404 | invalid_request_error | model_not_found | model | model is not in the catalog | No — see GET /v1/models |
| 400 | invalid_request_error | unsupported_parameter | the field | /v1/responses given previous_response_id, conversation, background or an item_reference — the wire is stateless | No — send the full input |
| 404 | invalid_request_error | unknown_url | — | Unrouted path or method (also /v1/embeddings) | No |
| 413 | invalid_request_error | request_too_large | — | Body over 5 MB | No — shrink the request |
| 429 | rate_limit_error | rate_limit_exceeded | — | Over 300 requests/minute for the account, or capacity temporarily unavailable | Yes — after retry-after if present, else with backoff |
| 429 | rate_limit_error | too_many_failed_auth | — | 10 failed authentications in a minute from one IP | After retry-after |
| 500 | api_error | internal_error | — | Unhandled failure | Yes, with backoff |
| 502 | api_error | upstream_error | — | The model's endpoint failed | Yes, with backoff |
| 503 | api_error | no_upstream_account | — | No capacity available for this model right now | Yes, with backoff |
Retry guidance#
- Honour
retry-after. It is sent on429 rate_limit_exceededwhen a reset time is known and always ontoo_many_failed_auth. The OpenAI, Anthropic and Vercel AI SDKs already do this. - Retry 5xx with exponential backoff, a few times, then surface the error. A
502or503is transient by definition. - Do not retry 4xx except
402after adding credit and429after waiting. A400will fail the same way every time. - Streamed errors. A failure before the first byte is a normal error response with one of the statuses above. A stream that has already started cannot change its status; if it ends early you receive a truncated stream without its terminator (
[DONE]ormessage_stop) — treat a missing terminator as a failure and retry. See Streaming.
Relayed messages#
When the model itself rejects a request, its message is relayed so you can act on it — "prompt too long", an unsupported parameter, a content-policy rejection. Anything that would describe how Sator is served rather than what your request did wrong is replaced with a generic message; nothing you see is about our infrastructure. Request bodies are never echoed back.