# Errors and rate limits

> Every error the OpenCheese API returns: the JSON shapes per endpoint, status codes and error codes, rate-limit headers, and which errors are worth retrying.
>
> Web page: https://opencheese.dev/en/docs/errors/ · Every docs page: https://opencheese.dev/llms.txt

Errors come back as an HTTP status with a JSON body in the format of the endpoint you called.
Every message starts with 🧀, so you can tell an OpenCheese message from the model's text, and
every response carries an `x-request-id` header: quote it when you contact support.

## Error shapes

OpenAI endpoints (`/v1/chat/completions`, `/v1/responses`, `/v1/images/*`, `/v1/models` and
the account endpoints):

```json
{
  "error": {
    "message": "🧀 Invalid or disabled OpenCheese API key.",
    "type": "authentication_error",
    "code": "invalid_api_key"
  }
}
```

The Anthropic endpoint (`/v1/messages`) wraps the same fields in Anthropic's envelope:

```json
{
  "type": "error",
  "error": {
    "message": "🧀 Invalid or disabled OpenCheese API key.",
    "type": "authentication_error",
    "code": "invalid_api_key"
  }
}
```

Errors worth waiting for add `retry_after` (seconds) to the body and a `Retry-After` header.

## Errors after a stream has started

Once a streaming answer has begun, the HTTP status is already `200`, so a failure arrives in
the stream:

- `/v1/chat/completions`: the message arrives as assistant text, then a final chunk and
  `data: [DONE]`.
- `/v1/responses`: a `response.failed` event, with `response.error.code` and
  `response.error.message`.
- `/v1/messages`: an `error` event with the Anthropic error object.

## Chat Completions answers some errors as a reply

On `/v1/chat/completions`, problems you need to act on arrive as the assistant's reply, in a
`200` response streamed as `text/event-stream`, even when `stream` is `false`:

```text
data: {"object":"chat.completion.chunk","model":"opencheese","choices":[{"index":0,"delta":{"role":"assistant","content":"🧀 Invalid or disabled OpenCheese API key."},"finish_reason":"stop"}]}

data: [DONE]
```

That covers an invalid or disabled key, a blocked account, an invalid session header, a body
that is not a valid request, a request the content policy blocks, and every balance or plan
limit. Chat apps show the message to you that way. In your own code, check the
`Content-Type`: `text/event-stream` on a non-streaming request means the reply is such a
message. Every other error on this endpoint is a normal status with a JSON body.

## Codes

| Code | Status | What happened | Retry? |
| --- | --- | --- | --- |
| `invalid_api_key` | 401 | The key is wrong, revoked or disabled. | No |
| `account_banned`, `account_frozen`, `account_disabled` | 403 | The account cannot make requests; the dashboard says why. | No |
| `invalid_request_error` | 400 | The body is not a valid request for this endpoint. | No |
| `invalid_session_header` | 400 | `opencheese-session-uuid` is not a valid id; see [Sessions](https://opencheese.dev/en/docs/sessions.md). | No |
| `model_not_found` | 404 | No model has this ID. Read [`/v1/models`](https://opencheese.dev/en/docs/models.md). | No |
| `model_access_denied` | 403 | The model is switched off on your **Prices** page, or the key's model list leaves it out. | No |
| `model_disabled`, `model_pricing_disabled` | 400 | The model is not offered right now. | No |
| `unsupported_model` | 400 | Wrong endpoint for the model, such as a text model on `/v1/images/generations`. | No |
| `streaming_unsupported` | 400 | The model does not stream; send the request without `stream`. | No |
| `unsupported_reasoning_effort` | 400 | The model has no such effort level; `supported_reasoning_levels` lists its levels. | No |
| `context_length_exceeded` | 400 | The prompt is longer than the model's context window. | No |
| `request_blocked_by_guard` | 403 | The content policy blocked the request. | No |
| `provider_refusal` | 400 | The model declined to answer. | Only with a changed request |
| `moderation_blocked`, `moderation_blocked_billed` | 400 | The model's content policy declined the prompt or the image. | Only with a changed request |
| `plan_limit_reached` | 429 | The balance, a plan window, a key's spending cap or a monthly limit is used up, or the model is outside your plan. | After it resets or you top up |
| `rate_limit_exceeded` | 429 | Too many requests; see below. | After `Retry-After` |
| `requested_effort_unavailable` | 429 | The effort level is temporarily unavailable; the error lists the other levels. | After `Retry-After`, or another level |
| `provider_unavailable` | 429 | The model is temporarily unavailable. | After `Retry-After` |
| `provider_overloaded` | 5xx | The model is overloaded. | Yes, with backoff, or another model |
| `request_timeout` | 504 | The answer took too long. | Yes |
| `service_unavailable` | 503 | The gateway is briefly busy. | Yes, shortly |

On `/v1/chat/completions`, the errors that arrive as a reply (see above) come with status `200`
instead of the one shown here.

On `/v1/messages`, `plan_limit_reached` also sends `x-should-retry: false`, so the Anthropic
SDKs do not resend it.

## An effort the model does not offer

```json
{
  "error": {
    "message": "🧀 Example Model is not available for Ultra effort. Choose one of these efforts: low, medium, high.",
    "type": "invalid_request_error",
    "code": "unsupported_reasoning_effort",
    "supported_reasoning_levels": ["low", "medium", "high"]
  }
}
```

The levels run from the weakest to the strongest and are the same ones `/v1/models` lists for
the model. A model without levels answers that it takes no effort setting: leave the field
out. If you don't send an effort at all, the model's default is used and this error never
occurs.

## Rate limits

Each account has request limits per minute, hour, day and month. A request over a limit is
answered `429 rate_limit_exceeded` with these headers:

| Header | Meaning |
| --- | --- |
| `Retry-After` | Seconds until you may try again. |
| `x-ratelimit-limit` | The limit you reached. |
| `x-ratelimit-remaining` | `0`. |
| `x-ratelimit-reset` | When the window resets, as an ISO 8601 time. |

A very short burst of requests can also answer `429` without these headers: wait a few seconds
before retrying. The account endpoints (`/v1/key`, `/v1/plan`, `/v1/balance`) and
`/v1/messages/count_tokens` do not count toward the limits.

## Retrying

- Retry `429`, `5xx` and `request_timeout`, honouring `Retry-After` when it is present, with
  exponential backoff otherwise.
- Do not retry `400`, `401`, `403` and `404` unchanged: the same request fails the same way.
- `plan_limit_reached` clears when the window resets, when you top up, or when you switch on
  **Use balance for Credit Models** on the dashboard's **Overview**.

## Notices next to an answer

A successful answer can carry `opencheese_notifications`, a list of notices about your
account, such as a plan window that is about to run out. They do not change the answer.
