Errors & status codes
Every upstream provider error is normalised into a single predictable JSON envelope. Client code never has to handle Vertex's google.rpc.Status alongside Anthropic's error.type alongside OpenAI's error.code.
Error envelope
Every non-2xx response carries this shape. The OpenAI-compatible fields (message, type, code, param) are present on every error; the Meridian extensions (retryable, upstream_provider, upstream_status, provider_attempts) are present when applicable.
{
"error": {
"message": "Upstream provider rate limit hit",
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
"param": null,
"retryable": true,
"upstream_provider": "anthropic",
"upstream_status": 429
}
}
Always read error.retryable instead of inferring retryability from the status code. The router takes provider-specific knowledge into account (for example OpenAI's insufficient_quota is a 429 but is not retryable — that distinction is encoded in retryable: false).
HTTP status codes
-
200
OK — Request succeeded. Inspect the
billingandX-Meridian-*headers for routing details. -
202
Accepted — Streaming response started; chunks follow as Server-Sent Events.
-
400
Bad Request — Malformed body, unknown model, missing
messages, or a high-risk request missingpurpose/user_consent_id. -
401
Unauthorized — Missing or malformed API key header.
-
402
Payment Required — Per-key credit limit reached, OR the user balance has hit the maximum debt threshold.
-
403
Forbidden — Key revoked, expired, or the request was blocked by deployer policy / Article 5 detector. Inspect
error.code. -
404
Not Found — The user account associated with this key was not found.
-
409
Conflict — The requested model is in
maintenanceordeprecatedlifecycle status. -
422
Unprocessable Entity — Capability mismatch (e.g. you sent an image to a text-only model).
-
429
Too Many Requests — Per-key rate limit, per-tenant rate limit, or daily multimodal cap exceeded.
-
502
Bad Gateway — All providers in the fallback chain failed. Response includes the full
provider_attemptsarray. -
500
Internal Server Error — Unexpected. Always retryable.
Meridian error codes
The error.code field uses these stable string identifiers. The set is closed at the gateway — every upstream code is mapped to one of these so client switch statements stay finite.
| Code | Meaning |
|---|---|
invalid_api_key | Missing or revoked key. |
key_expired | Key passed its expiresAt timestamp. |
key_credit_limit_exceeded | Per-key credit cap hit. The response body includes consumed, limit, and resetInterval. |
insufficient_quota | User balance has reached the maximum debt threshold; top up to continue. |
user_not_found | The key resolved but its owning user is missing — usually a deleted account. |
rate_limit_exceeded | Rate limit hit. retryable: true. |
authentication_error | Auth header rejected at upstream provider. |
permission_denied | Authorisation rejected at upstream provider. |
model_not_found | The requested model is not in the catalogue, or the upstream provider returned 404. |
request_timeout | Upstream took longer than the configured timeout. retryable: true. |
invalid_request | 422 from upstream. Usually a bad parameter combination. |
context_length_exceeded | Prompt exceeded the model's context window. Set auto_truncate: true to drop old messages automatically. |
upstream_unavailable | Upstream returned 5xx. retryable: true. |
all_providers_failed | Every entry in the fallback chain failed. Inspect provider_attempts. |
provider_error | Upstream error that didn't match any known shape. Inspect upstream_status. |
Provider attempts
When a fallback chain fails, the response includes a provider_attempts array with one entry per attempted provider. Useful for debugging which step in the chain blew up and why.
{
"error": {
"message": "All providers failed",
"type": "upstream_error",
"code": "all_providers_failed",
"retryable": false,
"provider_attempts": [
{
"provider": "openai",
"model": "gpt-4o",
"status": "failed",
"latency_ms": 312,
"error": "[503] upstream_unavailable"
},
{
"provider": "anthropic",
"model": "claude-3-5-sonnet",
"status": "failed",
"latency_ms": 204,
"error": "[529] overloaded_error"
}
]
}
}
Retry strategy
Most retries happen inside the gateway and never reach your client (see Smart retries and Fallback chains). When a retry-eligible error does reach your client, follow this rule:
- If
error.retryableistrue, retry with jittered exponential backoff (start at 250 ms). - If
error.retryableisfalse, do not retry — it's a deliberate refusal. - Never retry
402 key_credit_limit_exceededor402 insufficient_quota— top up first.