> ## Documentation Index
> Fetch the complete documentation index at: https://speshu.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Ошибки

> Форматы ошибок, коды и что с ними делать

У API не один формат ошибки, а три. Перед тем как писать разбор ответа, определите, к какой группе эндпоинтов обращается запрос: клиент, ожидающий `error.code`, на части маршрутов получит `null` или вообще другое поле.

## Три формата

| Группа эндпоинтов | Формат |
| - | - |
| Текстовые: `/api/v1/chat/completions`, `/api/v1/responses`, `/api/v1/balance`, `GET /api/v1/models`, `GET /api/v1/media/models`, а также отказы на этапе аутентификации | [OpenAI](#формат-openai) |
| `/api/v1/messages` | [Anthropic](#формат-anthropic) |
| Медиа-эндпоинты и хранилище | свои форматы, по одному на группу |

<Warning>
  Текстовые эндпоинты и медиа-эндпоинты отвечают об ошибке по-разному. Клиент, который разбирает единый формат на всей базе `/api/v1`, будет неправ на медиа-маршрутах и на хранилище.
</Warning>

## Формат OpenAI

Основной формат. Объект `error` с четырьмя полями:

```json theme={null} theme={null}
{
  "error": {
    "message": "model is required",
    "type": "invalid_request_error",
    "param": null,
    "code": "invalid_request"
  }
}
```

| Поле | Тип | Описание |
| - | - | - |
| `message` | string | Человекочитаемый текст. **Не машиностабильный**: опираться на него в логике нельзя |
| `type` | string | Класс ошибки: `invalid_request_error`, `authentication_error`, `rate_limit_error` и подобные |
| `param` | string \| null | Поле запроса, виновное в ошибке. На этих эндпоинтах всегда `null` |
| `code` | string \| null | Машиночитаемый код. Единственное поле, по которому стоит ветвиться |

<Note>
  Исключение из правила про `param`: `GET /api/v1/models` и `GET /api/v1/media/models` при своей ошибке `500` не возвращают поле `param` вообще — объект там меньше, чем в примере выше.
</Note>

Разбор ошибки в коде:

```python Python theme={null} theme={null}
import os

import requests

response = requests.post(
    "https://speshu.ai/api/v1/chat/completions",
    headers={"Authorization": f"Bearer {os.environ['SPESHU_API_KEY']}"},
    json={"model": os.environ["SPESHU_MODEL"], "messages": [{"role": "user", "content": "Привет"}]},
    timeout=300,
)

if response.status_code >= 400:
    error = response.json()["error"]
    raise RuntimeError(f"{response.status_code} {error['code']}: {error['message']}")
```

## Формат Anthropic

Эндпоинт `POST /api/v1/messages` отвечает в формате Anthropic — без полей `param` и `code`:

```json theme={null} theme={null}
{
  "type": "error",
  "error": {
    "type": "invalid_request_error",
    "message": "model is required"
  }
}
```

Единственное поле для ветвления — `error.type`. Этих значений меньше, чем кодов в формате OpenAI, поэтому в этом формате различать, например, `model_not_found` и `content_too_large`, не получится.

<Warning>
  Недостаток средств на этом эндпоинте возвращает **400**, а не 402. Клиент, который ловит нехватку денег исключительно по `402`, на `/api/v1/messages` её пропустит.
</Warning>

## Формат медиа

Медиа-эндпоинты отвечают конвертом. `code` дублирует HTTP-статус, `msg` — человекочитаемый текст, `data` на ошибке всегда `null`:

```json theme={null} theme={null}
{
  "code": 422,
  "msg": "duration must be one of 5, 10",
  "data": null
}
```

Тот же конверт обслуживает и успех: `code` равен `200`, `msg` — `"success"`. Это верно даже при HTTP-статусе `201` — внутри конверта всё равно будет `200`.

<Warning>
  `msg` не машиностабилен. Он читается человеком и меняется; разбирайте ошибку по HTTP-статусу и полю `code`.
</Warning>

Отказ по аутентификации — единственное исключение: `401` на медиа-эндпоинтах возвращается в формате OpenAI, а не конвертом.

## Формат хранилища

У хранилища третий формат — свой, без `param` и без `code`:

```json theme={null} theme={null}
{
  "error": {
    "message": "file is required",
    "type": "invalid_request"
  }
}
```

Здесь `type` стоит на месте кода: это `invalid_request`. Разбор по `error.code` на хранилище вернёт `null`.

## Ошибки в потоке

При потоковой передаче ошибка приходит **внутри потока**, а не HTTP-статусом: HTTP-ответ к этому моменту уже `200`. В середине потока появляется кадр `data:` с объектом `error`, после чего поток обрывается — маркера `[DONE]` не будет:

```
data: {"error":{"message":"...","type":"server_error"}}
```

Проверяйте каждый кадр на наличие `error`: обрыв соединения без `[DONE]` означает ровно это.

```python Python theme={null} theme={null}
for line in response.iter_lines():
    if not line.startswith("data: "):
        continue
    payload = line[len("data: "):]
    if payload == "[DONE]":
        break
    chunk = json.loads(payload)
    if "error" in chunk:
        raise RuntimeError(chunk["error"]["message"])
```

## Коды ошибок OpenAI-формата

| HTTP | `code` | `type` | Когда |
| - | - | - | - |
| `400` | `invalid_request` | `invalid_request_error` | Некорректное тело, не передан `model`, `messages` или `input`, `Idempotency-Key` длиннее 255 символов |
| `400` | `provider_bad_request` | `invalid_request_error` | Провайдер отклонил запрос. В `message` — его собственный текст |
| `400` | `context_length_exceeded` | `invalid_request_error` | Вход не помещается в максимальное контекстное окно модели |
| `401` | `unauthorized` | `authentication_error` | Ключ не передан либо сессия недействительна или истекла |
| `401` | `invalid_api_key` | `authentication_error` | Ключ неверный |
| `402` | `insufficient_quota` | `invalid_request_error` | Недостаточно средств |
| `403` | `feature_not_available` | `invalid_request_error` | Возможность недоступна на вашем тарифе |
| `403` | `sub_account_blocked` | `invalid_request_error` | Дочерний аккаунт заблокирован |
| `403` | `ip_not_allowed` | `invalid_request_error` | IP не входит в список разрешённых |
| `403` | `forbidden` | `invalid_request_error` | Доступ запрещён |
| `404` | `model_not_found` | `invalid_request_error` | Модель неизвестна **или** объект не найден / не ваш |
| `422` | `content_too_large` | `invalid_request_error` | Входные токены плюс резерв выхода не влезают в контекстное окно. В `message` приходят сами числа |
| `422` | `limit_exceeded` | `invalid_request_error` | Исчерпан лимит тарифа |
| `429` | `rate_limit_exceeded` | `rate_limit_error` | Превышено число запросов в минуту |
| `429` | `token_limit_exceeded` | `rate_limit_error` | Превышено число токенов в минуту |
| `499` | `client_closed_request` | `client_closed_request` | Запрос отменён клиентом |
| `500` | `internal_error` | `internal_error` | Внутренняя ошибка |
| `502` | `provider_error` | `bad_gateway` | Провайдер настроен неверно |
| `503` | `no_providers` | `service_unavailable` | Для этой модели сейчас нет доступного провайдера |
| `503` | `service_unavailable` | `service_unavailable` | Провайдер отключён |
| `504` | `gateway_timeout` | `gateway_timeout` | Исчерпан потолок запроса в 300 секунд |

### Типы в Anthropic-формате

На `POST /api/v1/messages` значения скромнее:

| `error.type` | HTTP |
| - | - |
| `invalid_request_error` | 400, 402, 409, 410, 422 |
| `authentication_error` | 401 |
| `permission_error` | 403 |
| `not_found_error` | 404 |
| `rate_limit_error` | 429 |
| `api_error` | 500, 502, 503, 504 |

## Коды медиа-эндпоинтов

| HTTP | Когда |
| - | - |
| `400` | Некорректное тело запроса |
| `401` | Ключ не передан или недействителен. Формат OpenAI, а не конверт |
| `402` | Недостаточно средств |
| `403` | Модель недоступна на вашем тарифе |
| `404` | Не найдено **или не ваше** |
| `409` | Конфликт: сессия занята, промокод уже использован |
| `410` | Срок действия промокода истёк |
| `422` | Некорректный ввод или проверка по схеме не пройдена. В `msg` названо конкретное поле |
| `429` | Превышен лимит частоты запросов |
| `499` | Клиент прервал запрос |
| `500` | Внутренняя ошибка |
| `502` | Генератор настроен неверно |
| `503` | Генератор недоступен |
| `504` | Таймаут |

## Что делать с ошибкой

### Повторять или нет

| HTTP | Повторять | Как |
| - | - | - |
| `429` | Да | Экспоненциальная пауза с джиттером |
| `500`, `502`, `503`, `504` | Да | Экспоненциальная пауза с джиттером |
| `499` | Нет | Клиент уже отказался от запроса |
| `400`, `401`, `402`, `403`, `404`, `422` | Нет | Правьте запрос: повтор принесёт тот же результат |

<Note>
  Если в ответе есть заголовок `Retry-After`, следуйте ему — он точнее любого собственного расчёта. Экспоненциальная пауза нужна для случаев, когда такого заголовка нет.
</Note>

### Цикл повторов

```python Python theme={null} theme={null}
import os
import random
import time

import requests

MAX_ATTEMPTS = 5
RETRY_STATUSES = {429, 500, 502, 503, 504}

request = {
    "model": os.environ["SPESHU_MODEL"],
    "messages": [{"role": "user", "content": "Привет!"}],
}

for attempt in range(MAX_ATTEMPTS):
    response = requests.post(
        "https://speshu.ai/api/v1/chat/completions",
        headers={"Authorization": f"Bearer {os.environ['SPESHU_API_KEY']}"},
        json=request,
        timeout=300,
    )

    if response.status_code not in RETRY_STATUSES:
        response.raise_for_status()
        break

    if attempt == MAX_ATTEMPTS - 1:
        response.raise_for_status()

    delay = float(response.headers.get("Retry-After", 2 ** attempt))
    time.sleep(delay + random.uniform(0, delay / 2))

print(response.json()["choices"][0]["message"]["content"])
```

### Отдельные случаи

**`499 client_closed_request`** означает, что отменил клиент, а не платформа. В логах сервера это нормальная запись об оборванном соединении, а не сбой.

**`404` и «не ваше» неразличимы.** Задача, файл или сессия чужого аккаунта отвечают `404`, а не `403`. Это сделано намеренно, чтобы по коду ответа нельзя было проверить существование чужого объекта. Не стройте логику, которая отличает одно от другого.

**`422 content_too_large` — это ошибка планирования, а не сети.** Размер окна известен заранее: возьмите его из `GET /api/v1/models` и сверьте с суммой входа и резервируемого выхода.

```bash cURL theme={null} theme={null}
MODEL=$(curl -s https://speshu.ai/api/v1/models \
  -H "Authorization: Bearer $SPESHU_API_KEY" \
  | jq -r '.data[0].id')

curl -s https://speshu.ai/api/v1/models \
  -H "Authorization: Bearer $SPESHU_API_KEY" \
  | jq -r --arg m "$MODEL" '.data[] | select(.id == $m) | .context_length'
```

Выбрать решение можно только из трёх: уменьшить `max_completion_tokens`, сократить историю диалога или перейти на модель с окном побольше. Повтор с теми же аргументами ничего не изменит.

**`422 limit_exceeded` внутри периода не лечится повтором.** Это лимит тарифа, а не частота запросов: до сброса счётчика — только ожидание или смена тарифа. Подробности — в разделе [Лимиты](/docs/limits).

**`503 no_providers` лечится повтором, но не сразу.** Очередь на модель может разойтись через минуты, поэтому увеличьте базовую паузу для этого кода до нескольких секунд.

### Безопасный повтор запроса, создающего задачу

Для медиа повторять создание задачи нельзя вслепую: новый запрос — новая задача и новое списание средств. Заголовок `Idempotency-Key` делает повтор безопасным — тот же ключ возвращает исходную задачу.

```bash cURL theme={null} theme={null}
curl https://speshu.ai/api/v1/async/media/tasks \
  -H "Authorization: Bearer $SPESHU_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"model":"nano-banana-2","input":{"prompt":"Кот в смокинге"}}'
```

<Warning>
  Ключ нужно **сгенерировать один раз на логическую операцию** и переиспользовать его во всех повторах. Новый ключ на каждый повтор — это новая задача и повторное списание.
</Warning>

Формат заголовка — в разделе [Создать задачу](/docs/api-reference/media/create).

## Что дальше

<CardGroup cols={2}>
  <Card title="Запрос чату" icon="message" href="/docs/api-reference/chat/completions">
    Поля запроса и формат ответа
  </Card>

  <Card title="Anthropic Messages" icon="a" href="/docs/api-reference/messages/create">
    Эндпоинт с форматом ошибок Anthropic
  </Card>

  <Card title="Создать задачу" icon="plus" href="/docs/api-reference/media/create">
    Медиа-конверт и `Idempotency-Key`
  </Card>

  <Card title="Загрузить файл" icon="upload" href="/docs/api-reference/storage/upload">
    Третий формат ошибок
  </Card>

  <Card title="Лимиты" icon="gauge-high" href="/docs/limits">
    Таймауты и частота запросов
  </Card>

  <Card title="Модели и цены" icon="microchip" href="/docs/models">
    Контекстные окна и стоимость
  </Card>

  <Card title="Биллинг" icon="credit-card" href="/docs/billing">
    Списания, тарифы и потребление
  </Card>

  <Card title="Быстрый старт" icon="rocket" href="/docs/quickstart">
    Первый запрос за три шага
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.