Обработка ошибок
Как обрабатывать ошибки LLM API в ZvenoAI — envelope-формат, HTTP коды, стратегия повторов с exponential backoff, fallback по моделям и логирование.
ZvenoAI возвращает ошибки в формате, совместимом с OpenRouter. Постройте логику повторов, используйте fallback по моделям и собирайте метрики стабильности.
Структура ошибок
Все ошибки приходят в едином envelope:
{
"error": {
"code": 400,
"message": "invalid request parameters: model is required"
}
}Поля ответа:
error.code— HTTP status code числом, совпадает со статусом ответа.error.message— человекочитаемое описание (русский/английский). Не разбирайте программно — для логики используйтеerror.codeилиmetadata.error.metadata— опциональный объект с дополнительным контекстом. Структура зависит от типа ошибки — см. ниже известные формы.
Ломающее изменение в v2.0.0. Старый формат {code: "ERR_*", message, param, sequence_number} на верхнем уровне больше не возвращается. Обновите парсинг на envelope {error: {code, message, metadata?}}.
Коды ответов и действия
| HTTP | Когда возникает | Реакция |
|---|---|---|
| 400 | Невалидные/отсутствующие параметры, malformed input, CORS. | Не повторяйте — исправьте запрос. |
| 401 | Невалидный API-ключ, истёкшая сессия, отключённый ключ. | Не повторяйте — обновите ключ и уведомьте команду. |
| 402 | Недостаточно средств на счёте или у API-ключа. | Пополните баланс или дождитесь обновления квоты. |
| 403 | Moderation: входной текст flagged провайдером (см. metadata.reasons). | Не повторяйте — измените промпт. |
| 408 | Request timeout — запрос не завершился вовремя. | Можно повторить с большим таймаутом. |
| 429 | Rate limit на стороне ZvenoAI или провайдера. См. Лимиты запросов. | Exponential backoff + повтор. Уважайте Retry-After. |
| 502 | Выбранная модель/провайдер недоступны или вернули невалидный ответ. | Повторите — маршрутизатор уже понизил приоритет провайдера. |
| 503 | Нет провайдера, удовлетворяющего routing-требованиям (only, max_price). См. Выбор модели. | Ослабьте provider.preferences или дождитесь восстановления. |
Прочие 4xx/5xx (например, 404 для несуществующих ресурсов или 500 для непредвиденных внутренних ошибок) обрабатывайте по тому же envelope: error.code равен HTTP-статусу, error.message несёт описание.
Структура metadata для типовых ошибок
Поле metadata свободной формы (Record<string, unknown>), но для ряда ошибок структура зафиксирована.
Moderation (HTTP 403)
{
"error": {
"code": 403,
"message": "Input flagged by provider moderation",
"metadata": {
"reasons": ["sexual", "violence"],
"flagged_input": "...обрезанный фрагмент входа...",
"provider_name": "openai",
"model_slug": "openai/gpt-4o"
}
}
}Конкретные значения reasons зависят от провайдера. flagged_input обрезается до 100 символов посередине с ..., если оригинал длиннее.
Provider error (HTTP 502 / 503)
{
"error": {
"code": 502,
"message": "upstream provider returned an error",
"metadata": {
"provider_name": "openrouter",
"raw": {
"error": {
"code": "model_not_found",
"message": "The model is currently overloaded"
}
}
}
}
}metadata.raw — payload провайдера as-is: JSON-объект если upstream структурирован, либо строка для plain text. Размер ограничен 8 KB. Конкретная структура зависит от провайдера — не закладывайтесь на конкретные поля внутри raw, используйте его для логов и эскалаций.
Если провайдер не отдал поле структурно (например, request_id шёл только в текстовом теле), оно
не попадает в metadata — для эскалации читайте error.message. ZvenoAI не извлекает поля из
текста — только структурный JSON провайдера.
Ошибки в streaming (mid-stream)
Если ошибка происходит уже после того, как поток начался, HTTP статус остаётся 200. ZvenoAI отправляет SSE-кадр с top-level error и choices[0].finish_reason = "error", после чего шлёт data: [DONE].
: OPENROUTER PROCESSING
data: {"id":"cmpl-...","object":"chat.completion.chunk","created":1735689600,"model":"openai/gpt-4o","choices":[{"index":0,"delta":{"content":"Привет"},"finish_reason":null}]}
data: {"id":"cmpl-...","object":"chat.completion.chunk","created":1735689601,"model":"openai/gpt-4o","provider":"openai","error":{"code":"server_error","message":"Provider disconnected unexpectedly"},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}
data: [DONE]В streaming-кадре error.code приходит от провайдера — может быть строкой (как в примере) или числом. В request-error envelope code всегда число (HTTP-статус). Для streaming в Responses API ошибки приходят отдельными событиями: response.failed, response.error или error.
Fallback по моделям
Передавайте массив моделей, если важно гарантировать ответ. ZvenoAI выполнит запрос первой доступной моделью:
const response = await client.chat.completions.create({
models: ['openai/gpt-4o', 'anthropic/claude-3.5-sonnet', 'google/gemini-2.5-pro'],
messages: [{ role: 'user', content: 'Привет!' }],
})ZvenoAI автоматически выберет доступную модель и вернёт ответ. Если ни одна не доступна — 503 с metadata.provider_name последнего попытанного.
Ретраи с exponential backoff
Повторяйте только 408, 429, 502, 503. Для 400/401/403 повторы бессмысленны — исправляйте запрос.
import OpenAI from 'openai'
import pRetry, { AbortError } from 'p-retry'
const NON_RETRYABLE = new Set([400, 401, 402, 403])
async function callLLM(messages: OpenAI.ChatCompletionMessageParam[]) {
return pRetry(
async () => {
try {
const response = await client.chat.completions.create({
model: 'openai/gpt-4o',
messages,
})
return response.choices[0].message.content
} catch (err) {
if (err instanceof OpenAI.APIError && NON_RETRYABLE.has(err.status ?? 0)) {
throw new AbortError(err)
}
throw err
}
},
{ retries: 3 },
)
}from openai import APIStatusError
from tenacity import (
retry, stop_after_attempt, wait_exponential, retry_if_exception,
)
NON_RETRYABLE = {400, 401, 402, 403}
def is_retryable(exc: BaseException) -> bool:
if isinstance(exc, APIStatusError):
return exc.status_code not in NON_RETRYABLE
return True
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=8),
retry=retry_if_exception(is_retryable),
)
def call_llm(messages):
response = client.chat.completions.create(
model="openai/gpt-4o",
messages=messages,
)
return response.choices[0].message.contentoperation := func() error {
resp, err := client.CreateChatCompletion(ctx, request)
if err != nil {
var apiErr *openai.APIError
if errors.As(err, &apiErr) {
switch apiErr.HTTPStatusCode {
case http.StatusBadRequest, http.StatusUnauthorized,
http.StatusPaymentRequired, http.StatusForbidden:
return backoff.Permanent(err)
}
}
return err
}
process(resp)
return nil
}
err := backoff.Retry(operation, backoff.NewExponentialBackOff())Рекомендации
- Парсите
error.code(HTTP status) иerror.metadata.provider_name— этого хватает для роутинга реакции. - Для
401/403немедленно прекращайте повторы и уведомляйте команду. - Для
502/503логируйтеmetadata.rawцеликом — это сырой ответ провайдера, нужен для эскалаций. - Используйте массив моделей (
models: [...]), чтобы ZvenoAI переключился на доступную модель автоматически. - Настройте алерты по числу
5xx/429за интервал — это индикатор проблем.