Errors
All errors — from the gateway's /v1 API and from the dashboard API — use one JSON envelope and one set of codes. Use error.code for programmatic handling; error.type groups codes into broad classes; error.message is for humans and may change.
The envelope
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded for api_key (60 requests/minute)",
"request_id": "req_01J6ZK3M9PQR7S8T9V",
"param": null,
"provider": null,
"retry_after": 2,
"details": null
}
}| Field | Type | Always | Description |
|---|---|---|---|
type | string | yes | One of invalid_request_error, authentication_error, permission_error, rate_limit_error, quota_error, not_found_error, conflict_error, provider_error, timeout_error, internal_error. |
code | string | yes | Machine-readable code from the table below. Stable. |
message | string | yes | Human-readable explanation. Not stable; do not parse. |
request_id | string | yes | Same value as the X-Request-Id header. Quote it in support requests. |
param | string | no | JSON path of the offending request field for validation errors (e.g. messages.0.content). |
provider | string | no | Slug of the upstream provider involved in a provider error. Never contains credentials. |
retry_after | integer | no | Seconds to wait before retrying; mirrors the Retry-After header. |
details | any | no | Optional structured context (e.g. a list of validation issues). |
Errors on /v1 are compatible with OpenAI SDK error handling: the SDKs read error.message, error.type, error.code and error.param and raise the class matching the HTTP status (AuthenticationError, RateLimitError, BadRequestError, …).
Streaming errors
Once an SSE stream has started (HTTP 200 already sent), an error is delivered as a data event containing the same envelope, followed by [DONE]:
data: {"error":{"type":"provider_error","code":"stream_interrupted","message":"provider connection lost","request_id":"req_…","provider":"groq"}}
data: [DONE]See Streaming.
Retry-After
Every 429 response and every 409 idempotency_conflict caused by an in-flight duplicate carry a Retry-After header (seconds, integer) and the matching error.retry_after. 503/504 provider failures may also include it when the upstream supplied one. Honour it before retrying.
Status codes
| HTTP | type | Typical codes |
|---|---|---|
| 400 | invalid_request_error | invalid_json, validation_failed, idempotency_key_required, ssrf_blocked |
| 401 | authentication_error | invalid_api_key, key_expired, key_disabled, key_revoked |
| 402 | quota_error | insufficient_credits, budget_exceeded, spending_limit_exceeded |
| 403 | permission_error | ip_not_allowed, model_not_allowed, modality_not_allowed |
| 404 | not_found_error | model_not_found, not_found |
| 409 | conflict_error | idempotency_conflict, job_not_cancellable |
| 413 | invalid_request_error | request_too_large |
| 415 | invalid_request_error | unsupported_media_type |
| 429 | rate_limit_error | rate_limited, tokens_per_minute_exceeded, concurrency_exceeded, provider_rate_limited |
| 500 | internal_error | internal_error |
| 501 | invalid_request_error | not_implemented, not_configured |
| 502 | provider_error | provider_error, provider_disconnect, stream_interrupted |
| 503 | provider_error / internal_error | no_provider_available, provider_unavailable, service_unavailable |
| 504 | timeout_error | provider_timeout |
Every error code
This table is generated at build time from the platform's shared schema package, so it is always complete. Codes marked (dashboard API only) are emitted by the control-plane API used by the dashboard and never by /v1.
| HTTP | code | type | Meaning | Retry? |
|---|---|---|---|---|
| 400 | idempotency_key_required | invalid_request_error | Video and music generation require an Idempotency-Key header. | No — add the header |
| 400 | invalid_json | invalid_request_error | The body is not valid JSON. | No — fix the request |
| 400 | ssrf_blocked | invalid_request_error | A URL you supplied (input image, webhook) points at a private, loopback or otherwise blocked address. | No — use a public https URL |
| 400 | validation_failed | invalid_request_error | The body is valid JSON but violates the schema; param points at the offending field. | No — fix the request |
| 401 | invalid_api_key | authentication_error | The Authorization header is missing, malformed, or the key does not exist. | No — fix the key |
| 401 | invalid_credentials | authentication_error | Wrong email or password.(dashboard API only) | No |
| 401 | key_disabled | authentication_error | The key was disabled in the dashboard. | No — re-enable in the dashboard |
| 401 | key_expired | authentication_error | The key’s expires_at has passed. | No — rotate or create a new key |
| 401 | key_revoked | authentication_error | The key was revoked (or replaced by rotation). | No — use the new key |
| 401 | unauthenticated | authentication_error | No valid session cookie.(dashboard API only) | No — sign in |
| 402 | budget_exceeded | quota_error | The key’s daily or monthly budget would be exceeded. | No — raise budget or wait for the window |
| 402 | insufficient_credits | quota_error | The organization balance cannot cover the estimated cost. | No — top up credits |
| 402 | spending_limit_exceeded | quota_error | The organization’s monthly spending limit would be exceeded. | No — raise the limit |
| 403 | account_disabled | permission_error | The user account is disabled.(dashboard API only) | No |
| 403 | csrf_failed | permission_error | Missing X-Requested-With header on a mutating dashboard request.(dashboard API only) | No |
| 403 | email_not_verified | permission_error | The account email has not been verified.(dashboard API only) | No — verify email |
| 403 | forbidden | permission_error | The caller lacks the role required for this action.(dashboard API only) | No |
| 403 | ip_not_allowed | permission_error | The caller IP is not in the key’s allowed_ips list (or the organization is not active). | No — update allowed_ips |
| 403 | modality_not_allowed | permission_error | The key’s allowed_modalities scope excludes this modality. | No — adjust key scope |
| 403 | model_not_allowed | permission_error | The key’s allowed_models scope excludes this model. | No — adjust key scope |
| 404 | model_not_found | not_found_error | No enabled model with this id. Check GET /v1/models. | No |
| 404 | not_found | not_found_error | The resource (job, model detail, …) does not exist or belongs to another organization. | No |
| 409 | conflict | conflict_error | The operation conflicts with current state.(dashboard API only) | No |
| 409 | idempotency_conflict | conflict_error | Idempotency-Key was reused with a different request body, or the original request is still in flight (retry_after set). | Only when retry_after is present |
| 409 | job_not_cancellable | conflict_error | DELETE on a job that is not in a cancellable state. | No |
| 409 | max_api_keys_reached | conflict_error | The organization already has the maximum number of API keys.(dashboard API only) | No |
| 413 | request_too_large | invalid_request_error | Body exceeds MAX_REQUEST_BYTES (default 10 MiB JSON, 25 MiB multipart). | No — reduce payload |
| 415 | unsupported_media_type | invalid_request_error | Content-Type is not accepted by this endpoint (e.g. JSON where multipart is required). | No |
| 429 | concurrency_exceeded | rate_limit_error | Too many in-flight requests for this key. | Yes — when a request completes |
| 429 | provider_rate_limited | rate_limit_error | Upstream providers returned 429 and fallbacks are exhausted. | Yes — after Retry-After |
| 429 | rate_limited | rate_limit_error | Requests-per-second or requests-per-minute limit exceeded (key, org, model or global scope). | Yes — after Retry-After |
| 429 | tokens_per_minute_exceeded | rate_limit_error | Estimated tokens for this minute exceed the key’s tokens_per_minute. | Yes — after Retry-After |
| 500 | internal_error | internal_error | Unexpected failure. Include request_id when contacting support. | Yes — once, with backoff |
| 501 | not_configured | invalid_request_error | The feature requires configuration the operator has not provided (e.g. object storage). | No |
| 501 | not_implemented | invalid_request_error | The feature (e.g. the smart strategy, real video providers) is not implemented in this build. | No |
| 502 | provider_disconnect | provider_error | The provider closed the stream before sending finish_reason. | Yes — re-issue the request |
| 502 | provider_error | provider_error | The provider returned an error the gateway could not recover from. | Maybe — inspect message |
| 502 | stream_interrupted | provider_error | A provider failure occurred after the first token was sent; delivered as an in-stream error chunk. | Yes — re-issue the request |
| 503 | no_provider_available | provider_error | Routing found no eligible, healthy provider for the model (all disabled, open-circuit, or excluded). | Yes — with backoff |
| 503 | provider_unavailable | provider_error | Every attempted provider returned 5xx/connection errors and fallbacks are exhausted. | Yes — with backoff |
| 503 | service_unavailable | internal_error | A dependency (Redis, database) is unavailable; the gateway fails closed. | Yes — with backoff |
| 504 | provider_timeout | timeout_error | The provider did not respond within the timeout and fallbacks are exhausted. | Yes — with backoff |
Handling errors
import OpenAI, { APIError } from 'openai';
try {
await client.chat.completions.create({ model: 'openai/gpt-4o-mini', messages });
} catch (err) {
if (err instanceof APIError) {
const code = (err.error as { code?: string } | undefined)?.code;
switch (code) {
case 'rate_limited':
case 'provider_rate_limited':
// wait err.headers['retry-after'] seconds, then retry
break;
case 'insufficient_credits':
case 'budget_exceeded':
// alert billing owner; do not retry
break;
case 'model_not_allowed':
case 'model_not_found':
// configuration problem
break;
default:
console.error(err.status, code, err.requestID ?? err.headers?.['x-request-id']);
}
} else throw err;
}import openai
try:
client.chat.completions.create(model="openai/gpt-4o-mini", messages=messages)
except openai.APIStatusError as e:
body = e.response.json().get("error", {})
code = body.get("code")
request_id = body.get("request_id") or e.response.headers.get("x-request-id")
if code in ("rate_limited", "provider_rate_limited"):
retry_after = int(e.response.headers.get("retry-after", "1"))
elif code in ("insufficient_credits", "budget_exceeded", "spending_limit_exceeded"):
... # notify billing owner
else:
print(e.status_code, code, request_id)const res = await fetch(url, init);
if (!res.ok) {
const { error } = await res.json();
const retryAfter = Number(res.headers.get('retry-after') ?? error.retry_after ?? 0);
throw Object.assign(new Error(`${error.code}: ${error.message}`), { status: res.status, code: error.code, requestId: error.request_id, retryAfter });
}