Lovely RouterDocs
Get a key
API reference

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#

StatusMeaningCommon causesRetry?
400Bad requestBody 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.
401UnauthenticatedKey missing, mistyped, revoked or expiredNo. Check the key.
402Payment requiredBalance 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
403ForbiddenModel not on the key's allow-listNo
404Not foundUnknown model id, model not offered in this API format, unknown endpoint, tool or runNo
405Method not allowedWrong HTTP method on a browser vault or files endpointNo
409ConflictReply to a browser run that isn't waiting for that reply, or has finished. File storage limit reached.No. Check the run's state.
413Payload too largePrompt too large for the model. Tool input over 50 KB. File over 10 MB.No. Make it smaller.
422UnprocessableThe model rejected the content of the requestNo
429Too many requestsKey rate or concurrency limit, model capacity busy, 10 runs already in progress, planner limitYes, after retry-after
500Internal errorUnexpected error in Lovely RouterYes, with backoff
502Bad gatewayThe model failed on every channel tried. A tool couldn't start (you're not charged).Yes, with backoff
503Service unavailableA data tool or the browser agent is temporarily unavailableYes, 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:

StatusAnthropic error.typeOpenAI error.type and error.codeGemini error.status
400invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
401authentication_errorauthentication_errorUNAUTHENTICATED
402billing_errorinsufficient_quotaRESOURCE_EXHAUSTED
403permission_errorpermission_deniedPERMISSION_DENIED
404not_found_errornot_found_error, or not_found for an unknown endpointNOT_FOUND
413, 422invalid_request_errorinvalid_request_errorINVALID_ARGUMENT
429rate_limit_errorrate_limit_exceededRESOURCE_EXHAUSTED
500api_errorapi_errorINTERNAL
502, 503api_errorapi_errorUNAVAILABLE
504api_errorapi_errorDEADLINE_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#

StatusMessage
400Body must be JSON
400Unknown group 'xyz'
400, 413, 422The model's own reason, for example prompt is too long
401Missing API key, Invalid or revoked API key, This API key has expired
402Insufficient balance. Top up at the dashboard.
402This API key has reached its spend limit.
403This API key is not allowed to use 'gpt-6.1-sol'.
404Model 'gpt-9' is not available. See GET /v1/models.
404Unknown endpoint /v1/embeddings
429This API key has reached its rate or concurrency limit.
429All upstream capacity for this model is busy. Retry shortly.
502The 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.

JSON
{"error": {"type": "insufficient_quota", "message": "Insufficient balance: this run can cost up to $0.3000 (100 results). Lower max_results or top up."}}
Statuserror.typeExamples
400invalid_request_errorMissing required input: query. See GET /v1/tools/{id}., "start_url" must be an http(s) URL
401authentication_errorMissing, invalid or revoked API key, This API key has expired
402insufficient_quotaBalance can't cover the run's maximum cost, or the key's spend limit would be exceeded
404not_foundUnknown tool 'x'. Find one with POST /v1/tools/discover., No such run
405invalid_request_errorWrong method on a vault or files endpoint
409invalid_request_errorThis run is not waiting for a reply, This run has finished
413invalid_request_errorInput too large, Files are limited to 10 MB
429rate_limit_exceededAt most 10 tool runs may be running at once.
500api_errorInternal error
502, 503tool_unavailableThe 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, 504 and network errors.
  • Wait for the retry-after header 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, 413 or 422 without changing the request. Retry 402 only after adding credit.
  • Don't blindly retry run starts. If starting a tool or browser run times out, check GET /v1/tools/runs before 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.