# Ошибки и лимиты

## Формат ошибок

Большинство ошибок приходят в стандартном формате:

```json
{
  "statusCode": 401,
  "message": "Invalid or inactive API key",
  "error": "Unauthorized"
}
```

Ошибки валидации содержат массив в `message`:

```json
{
  "statusCode": 400,
  "message": [
    "temperature must not be greater than 2",
    "messages should not be empty"
  ],
  "error": "Bad Request"
}
```

> Формат отличается от OpenAI: ответ **не** обёрнут в объект `error` с полями
> `type` и `code`. Библиотеки, ожидающие OpenAI-формат ошибок, покажут сообщение,
> но не смогут разобрать его тип.

## Коды ответов

### 400 — Bad Request

| Сообщение | Причина |
|---|---|
| `Negative or zero balance. Available: 0.00 RUB. Please top up your account.` | Баланс исчерпан. Пополните счёт в [профиле](https://agentapi.ru/profile) |
| массив сообщений валидации | Параметр вне допустимого диапазона или отсутствует обязательное поле |
| `content must be a string` | Передан массив блоков в `content` — мультимодальный ввод не поддерживается |
| `property X should not exist` | Неизвестное поле в запросе к `/v1/ai/embeddings` (там строгая валидация) |

Баланс проверяется **до** отправки запроса провайдеру. Он может стать отрицательным
после списания за уже выполненный запрос — блокируется только следующий.

### 401 — Unauthorized

| Сообщение | Причина |
|---|---|
| `API key is required in x-api-key or Authorization header` | Заголовок не передан |
| `Invalid API key format` | Ключ не начинается с `agentapi-` |
| `Invalid or inactive API key` | Ключ не найден или отключён (`active = false`) |
| `API key not found` | Ключ не связан с пользователем |

### 403 — Forbidden

`Insufficient balance to create an API key` — при попытке создать ключ с нулевым балансом.

### 404 — Not Found

Чаще всего означает **неверный URL**, а не отсутствующий ресурс:

| Что запрошено | Проблема |
|---|---|
| `https://agentapi.ru/v1/...` | Основной домен отдаёт сайт. Нужен `api.agentapi.ru` |
| `https://api.agentapi.ru/v1/chat/completions` | Пропущен сегмент `/ai` |
| `https://api.agentapi.ru/v1/messages` | Правильный путь — `/v1/ai/messages` |

### 429 — Too Many Requests

```json
{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}
```

### 502 — Bad Gateway

Ошибка на стороне провайдера. Формат **отличается** от остальных — без поля `statusCode`:

```json
{
  "message": "Provider returned error: model not found"
}
```

Типичные причины: неверный слаг модели, модель временно недоступна, запрос отклонён
провайдером. Для устойчивости используйте
[fallback-роутинг](https://agentapi.ru/docs/models) через `models[]`.

### Ошибки внутри стрима

Если генерация оборвалась после начала стриминга, HTTP-статус уже равен 200 — заголовки
отправлены до первого токена. Ошибка приходит кадром:

```
data: {"error":{"message":"..."}}
```

Для формата Anthropic — именованным событием:

```
event: error
data: {"type":"error","error":{"type":"api_error","message":"..."}}
```

Поэтому проверяйте поле `error` в каждом кадре, а не только код ответа.

> Сейчас в теле такого кадра сообщение может сериализоваться как пустой объект
> (`{"error":{"message":{}}}`) — см. [Ограничения](https://agentapi.ru/docs/limitations).

## Rate limit

| | |
|---|---|
| Лимит | 60 запросов за 60 секунд |
| Ключ подсчёта | **IP-адрес**, не API-ключ |
| Исключение | `POST /v1/ai/embeddings` — не лимитируется |

Важные следствия:

- Лимит считается по IP, поэтому несколько приложений с одного сервера (или из-за одного
  NAT) расходуют **общий** лимит, даже с разными ключами.
- Разные ключи одного аккаунта не дают независимых лимитов.
- Стриминговый запрос — это один запрос независимо от длительности.

Ограничения на количество токенов в минуту нет: фактический потолок — баланс аккаунта
и лимиты самого провайдера.

## Рекомендации по обработке

| Код | Стратегия |
|---|---|
| 400 (баланс) | Не повторять. Уведомить о пополнении |
| 400 (валидация) | Не повторять. Исправить запрос |
| 401 | Не повторять. Проверить ключ |
| 429 | Повтор с экспоненциальной задержкой |
| 502 | Повтор 1–2 раза; при повторе — сменить модель через `models[]` |

Пример повторов только для восстановимых ошибок:

```typescript
async function withRetry<T>(fn: () => Promise<T>, attempts = 3): Promise<T> {
  for (let i = 0; i < attempts; i++) {
    try {
      return await fn();
    } catch (error) {
      const status = (error as { status?: number }).status;

      // Повторяем только то, что может пройти со второй попытки.
      if (status !== 429 && status !== 502) throw error;
      if (i === attempts - 1) throw error;

      await new Promise((r) => setTimeout(r, 2 ** i * 1000));
    }
  }

  throw new Error("unreachable");
}
```
