Ошибки и лимиты
Формат ошибок
Большинство ошибок приходят в стандартном формате:
{
"statusCode": 401,
"message": "Invalid or inactive API key",
"error": "Unauthorized"
}
Ошибки валидации содержат массив в message:
{
"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. | Баланс исчерпан. Пополните счёт в профиле |
| массив сообщений валидации | Параметр вне допустимого диапазона или отсутствует обязательное поле |
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
{
"statusCode": 429,
"message": "ThrottlerException: Too Many Requests"
}
502 — Bad Gateway
Ошибка на стороне провайдера. Формат отличается от остальных — без поля statusCode:
{
"message": "Provider returned error: model not found"
}
Типичные причины: неверный слаг модели, модель временно недоступна, запрос отклонён
провайдером. Для устойчивости используйте
fallback-роутинг через models[].
Ошибки внутри стрима
Если генерация оборвалась после начала стриминга, HTTP-статус уже равен 200 — заголовки отправлены до первого токена. Ошибка приходит кадром:
data: {"error":{"message":"..."}}
Для формата Anthropic — именованным событием:
event: error
data: {"type":"error","error":{"type":"api_error","message":"..."}}
Поэтому проверяйте поле error в каждом кадре, а не только код ответа.
Сейчас в теле такого кадра сообщение может сериализоваться как пустой объект (
{"error":{"message":{}}}) — см. Ограничения.
Rate limit
| Лимит | 60 запросов за 60 секунд |
| Ключ подсчёта | IP-адрес, не API-ключ |
| Исключение | POST /v1/ai/embeddings — не лимитируется |
Важные следствия:
- Лимит считается по IP, поэтому несколько приложений с одного сервера (или из-за одного NAT) расходуют общий лимит, даже с разными ключами.
- Разные ключи одного аккаунта не дают независимых лимитов.
- Стриминговый запрос — это один запрос независимо от длительности.
Ограничения на количество токенов в минуту нет: фактический потолок — баланс аккаунта и лимиты самого провайдера.
Рекомендации по обработке
| Код | Стратегия |
|---|---|
| 400 (баланс) | Не повторять. Уведомить о пополнении |
| 400 (валидация) | Не повторять. Исправить запрос |
| 401 | Не повторять. Проверить ключ |
| 429 | Повтор с экспоненциальной задержкой |
| 502 | Повтор 1–2 раза; при повторе — сменить модель через models[] |
Пример повторов только для восстановимых ошибок:
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");
}