Error Codes
Error shapes, status codes, and handling behavior.
Most errors follow a consistent shape:
{ "error": "ERROR_CODE", "message": "Human readable description" }POST /v1/topup is an exception: some of its error responses (invalid payment encoding, mismatched payment, undecodable wallet address, and a 503 when the payment facilitator itself is unreachable) return { "error": "<human-readable message>" } with no separate message field and no machine-readable code. For this endpoint, match on HTTP status rather than parsing error as a code enum.
Error reference
| HTTP | Code | When |
|---|---|---|
400 | VALIDATION_ERROR | Missing/invalid params, amount too low |
400 | UNKNOWN_MODEL | Model ID is not one this gateway can resolve |
401 | UNAUTHORIZED | Missing/invalid API key or SIWE signature |
402 | INSUFFICIENT_BALANCE | Not enough credit for the estimated cost |
402 | KEY_BUDGET_EXCEEDED | The API key's own budget is spent |
404 | NOT_FOUND | Resource not found |
429 | RATE_LIMITED | Per-wallet rate limit exceeded |
4xx | UPSTREAM_CLIENT_ERROR | The model rejected the request — status is the upstream's own |
500 | INTERNAL_ERROR | Server error |
502 | UPSTREAM_ERROR | The model failed |
504 | UPSTREAM_DEADLINE | The model did not respond within the deadline |
Detailed examples
Insufficient Balance (402)
Returned when balance can't cover the reserved cost estimate.
{
"error": "INSUFFICIENT_BALANCE",
"message": "Insufficient balance. Top up at POST /v1/topup"
}Rate Limited (429)
Returned when per-wallet sliding window limits are exceeded.
{
"error": "RATE_LIMITED",
"message": "Rate limit exceeded"
}Validation Error (400)
{
"error": "VALIDATION_ERROR",
"message": "model and messages are required"
}Upstream Error (502)
Returned when the model fails on a non-streaming request. The balance lock is released (no charge).
{
"error": "UPSTREAM_ERROR",
"message": "Upstream LLM provider error"
}The message is fixed. Diagnostic detail is recorded server-side rather than returned, so the response body never varies with which model served the request.
Upstream Client Error (4xx)
A request the model itself rejects — an unsupported parameter, a prompt beyond its context window — is not flattened to 502. It comes back at the status the model returned, with a fixed message:
{
"error": "UPSTREAM_CLIENT_ERROR",
"message": "The upstream model rejected this request"
}Handling only 502 will miss these. A rejection commonly arrives as 400, and a model's own rate limit arrives as 429 — the same status as this gateway's RATE_LIMITED, but a different code. Match on error, not on status alone.
For stream: true requests, the SSE headers and HTTP 200 status are sent before the upstream call starts, so a mid-stream provider failure cannot become an HTTP 502. Instead it arrives as an SSE event over the already-open 200 connection:
data: {"error":{"message":"...","type":"upstream_error"}}
data: [DONE]Streaming requests never see the 502 shape above
Check for an error key in each parsed SSE data: payload — don't rely on the HTTP status code or the {error: "UPSTREAM_ERROR", ...} shape for streaming failures. The balance lock is still released in this case.
Error handling behavior
| Scenario | Behavior |
|---|---|
| Insufficient balance | HTTP 402 with top-up instructions and current balance |
| Provider down | Release lock, return 502, no balance deducted |
| Invalid model | Return 400, release lock, no balance deducted |
| Rate limited | Return 429, no balance deducted |
| Invalid API key | Return 401 |
| Expired/invalid SIWE | Return 401 |
| Top-up below minimum | Return 400 before issuing 402 |