Errors
Every HTTP status Lovely Router returns, the error body for each API format, and when and how to retry.
Lovely Router uses standard HTTP status codes. 2xx means success, 4xx means the request needs a change (or your account needs credit), and 5xx or 429 means a temporary problem worth retrying. The body says why, in the format of the API you called, so the official SDKs raise their usual exceptions.
Status codes#
| Status | Meaning | Common causes | Retry? |
|---|---|---|---|
400 | Bad request | Body isn't valid JSON. The model rejected a parameter, such as max_tokens above its limit. Tool input is missing a required field. Unknown x-lovely-group. | No. Fix the request. |
401 | Unauthenticated | Key missing, mistyped, revoked or expired | No. Check the key. |
402 | Payment required | Balance at $0. The key reached its spend limit. Your balance can't cover a run's maximum cost. | After you add credit or raise the limit |
403 | Forbidden | Model not on the key's allow-list | No |
404 | Not found | Unknown model id, model not offered in this API format, unknown endpoint, tool or run | No |
405 | Method not allowed | Wrong HTTP method on a browser vault or files endpoint | No |
409 | Conflict | Reply to a browser run that isn't waiting for that reply, or has finished. File storage limit reached. | No. Check the run's state. |
413 | Payload too large | Prompt too large for the model. Tool input over 50 KB. File over 10 MB. | No. Make it smaller. |
422 | Unprocessable | The model rejected the content of the request | No |
429 | Too many requests | Key rate or concurrency limit, model capacity busy, 10 runs already in progress, planner limit | Yes, after retry-after |
500 | Internal error | Unexpected error in Lovely Router | Yes, with backoff |
502 | Bad gateway | The model failed on every channel tried. A tool couldn't start (you're not charged). | Yes, with backoff |
503 | Service unavailable | A data tool or the browser agent is temporarily unavailable | Yes, with backoff |
Treat any other 5xx like 502. Errors from requests refused before reaching a model (400 for invalid JSON, 401, 402, 403, 404 for an unknown model, and 429 from a key limit) are never charged.
Error bodies for model endpoints#
Model endpoints return errors in the format of the API you called. Each tab below shows the same 401.
{"type": "error", "error": {"type": "authentication_error", "message": "Invalid or revoked API key"}}The error type (Anthropic and OpenAI) or status (Gemini) for each code:
| Status | Anthropic error.type | OpenAI error.type and error.code | Gemini error.status |
|---|---|---|---|
400 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
401 | authentication_error | authentication_error | UNAUTHENTICATED |
402 | billing_error | insufficient_quota | RESOURCE_EXHAUSTED |
403 | permission_error | permission_denied | PERMISSION_DENIED |
404 | not_found_error | not_found_error, or not_found for an unknown endpoint | NOT_FOUND |
413, 422 | invalid_request_error | invalid_request_error | INVALID_ARGUMENT |
429 | rate_limit_error | rate_limit_exceeded | RESOURCE_EXHAUSTED |
500 | api_error | api_error | INTERNAL |
502, 503 | api_error | api_error | UNAVAILABLE |
504 | api_error | api_error | DEADLINE_EXCEEDED |
In Gemini bodies, error.code always equals the HTTP status. Branch on the status code rather than the status string. Errors raised before the request is read, such as an unknown endpoint or an unexpected gateway error, also use the format of the endpoint you called.
Messages you may see#
| Status | Message |
|---|---|
400 | Body must be JSON |
400 | Unknown group 'xyz' |
400, 413, 422 | The model's own reason, for example prompt is too long |
401 | Missing API key, Invalid or revoked API key, This API key has expired |
402 | Insufficient balance. Top up at the dashboard. |
402 | This API key has reached its spend limit. |
403 | This API key is not allowed to use 'gpt-6.1-sol'. |
404 | Model 'gpt-9' is not available. See GET /v1/models. |
404 | Unknown endpoint /v1/embeddings |
429 | This API key has reached its rate or concurrency limit. |
429 | All upstream capacity for this model is busy. Retry shortly. |
502 | The model provider returned an error. Please retry. |
Every error response includes an x-lovely-request-id header, including requests refused before they reach a model (bad key, empty balance, unknown model). Quote it when you contact support.
Error bodies for tools and the browser agent#
/v1/tools and /v1/browser return one format for every error: a machine-readable type and a message written for people.
{"error": {"type": "insufficient_quota", "message": "Insufficient balance: this run can cost up to $0.3000 (100 results). Lower max_results or top up."}}| Status | error.type | Examples |
|---|---|---|
400 | invalid_request_error | Missing required input: query. See GET /v1/tools/{id}., "start_url" must be an http(s) URL |
401 | authentication_error | Missing, invalid or revoked API key, This API key has expired |
402 | insufficient_quota | Balance can't cover the run's maximum cost, or the key's spend limit would be exceeded |
404 | not_found | Unknown tool 'x'. Find one with POST /v1/tools/discover., No such run |
405 | invalid_request_error | Wrong method on a vault or files endpoint |
409 | invalid_request_error | This run is not waiting for a reply, This run has finished |
413 | invalid_request_error | Input too large, Files are limited to 10 MB |
429 | rate_limit_exceeded | At most 10 tool runs may be running at once. |
500 | api_error | Internal error |
502, 503 | tool_unavailable | The tool is temporarily unavailable; you were not charged. Try again shortly. |
A run that starts and then fails is not an HTTP error. The run object comes back with status set to failed, timed_out or stopped and an error message, and you pay only for results delivered. See Run a tool.
Errors during a stream#
Once a stream has started, the status is already 200, so errors arrive as an event in the stream instead. Failures before the first token are retried on another channel, so you normally see a stream either succeed or fail with a plain HTTP error.
If a model stops sending data for 3 minutes mid-stream, Lovely Router ends the stream with an error event in the stream's format:
event: error
data: {"type": "error", "error": {"type": "api_error", "message": "Upstream stopped responding mid-stream"}}An error the channel reports inside a running stream is replaced by a generic event in the same format (for the Responses API, an error event like {"type": "error", "code": "server_error", "message": "…"}), so its own wording never reaches you. A busy or overloaded channel becomes Anthropic overloaded_error, OpenAI rate_limit_exceeded or Gemini RESOURCE_EXHAUSTED with the message Upstream capacity for this model is busy. Retry shortly.; anything else becomes api_error (Gemini UNAVAILABLE) with The model provider returned an error. Please retry. Retry the request from the start; output already streamed is charged.
Retry failed requests#
Lovely Router already retries failed model requests on other channels before returning an error. Retry on your side too, for the cases it can't absorb:
- Retry
429,500,502,503,504and network errors. - Wait for the
retry-afterheader when it's present. Otherwise back off exponentially: 1, 2, 4, 8 seconds and so on, capped at 30 seconds, plus up to a second of random jitter so clients don't retry in lockstep. - Stop after about five attempts and surface the error.
- Don't retry
400,401,403,404,409,413or422without changing the request. Retry402only after adding credit. - Don't blindly retry run starts. If starting a tool or browser run times out, check
GET /v1/tools/runsbefore starting another. See Retries and idempotency.
The official OpenAI and Anthropic SDKs already retry 429 and 5xx twice with backoff. Pass max_retries (Python) or maxRetries (Node.js) to the client to change that. For plain HTTP calls, use a helper like this:
import os
import random
import time
import requests
RETRYABLE = {429, 500, 502, 503, 504}
def backoff(attempt, retry_after=None):
if retry_after:
return float(retry_after)
return min(30, 2 ** (attempt - 1)) + random.random()
def post_with_retry(url, payload, max_attempts=5):
headers = {"Authorization": f"Bearer {os.environ['LOVELY_API_KEY']}"}
for attempt in range(1, max_attempts + 1):
try:
r = requests.post(url, json=payload, headers=headers, timeout=300)
except (requests.ConnectionError, requests.Timeout):
if attempt == max_attempts:
raise
time.sleep(backoff(attempt))
continue
if r.status_code in RETRYABLE and attempt < max_attempts:
time.sleep(backoff(attempt, r.headers.get("retry-after")))
continue
if not r.ok: # other 4xx, or still failing after the last attempt
raise RuntimeError(f"{r.status_code}: {r.text[:500]}")
return r.json()
reply = post_with_retry(
"https://lovelyrouter.com/v1/chat/completions",
{"model": "gpt-6.1-sol", "messages": [{"role": "user", "content": "Hello"}]},
)
print(reply["choices"][0]["message"]["content"])Every error format, Anthropic, OpenAI, Gemini and Lovely Router's own, has an error.message written for people. Log it with the status code and the x-lovely-request-id header.