ZvenoAI
Руководства

Обработка ошибок

Как обрабатывать ошибки 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. Сюда же — отказ по provider-ограничениям (only, ignore, max_price): ни один маршрут им не удовлетворяет. См. Выбор модели.Не повторяйте — исправьте запрос или ослабьте ограничения.
401Невалидный API-ключ, истёкшая сессия, отключённый ключ.Не повторяйте — обновите ключ и уведомьте команду.
402Нехватка средств на счёте или исчерпанный лимит API-ключа. Причину смотрите в metadata.reason.Пополните баланс или поднимите лимит ключа.
403Moderation: входной текст flagged провайдером (см. metadata.reasons).Не повторяйте — измените промпт.
408Request timeout — запрос не завершился вовремя.Можно повторить с большим таймаутом.
429Rate limit на стороне ZvenoAI или провайдера. См. Лимиты запросов.Exponential backoff + повтор. Уважайте Retry-After.
502Выбранная модель/провайдер недоступны или вернули невалидный ответ.Повторите — маршрутизатор уже понизил приоритет провайдера.
503Нет провайдера под запрошенные параметры видео (например, max_price.video). См. Видеогенерация.Ослабьте условия или дождитесь восстановления.

Прочие 4xx/5xx (например, 404 для несуществующих ресурсов или 500 для непредвиденных внутренних ошибок) обрабатывайте по тому же envelope: error.code равен HTTP-статусу, error.message несёт описание.

Структура metadata для типовых ошибок

Поле metadata свободной формы (Record<string, unknown>), но для ряда ошибок структура зафиксирована.

Нехватка средств (HTTP 402)

Запрос, который нечем оплатить, получает 402. Причина — в metadata.reason.

402 — исчерпан лимит API-ключа
{
  "error": {
    "code": 402,
    "message": "api key budget exceeded: limit=5000 RUB, spent=5000 RUB, resets at 2026-10-01T00:00:00+03:00",
    "metadata": {
      "reason": "api_key_budget_exceeded",
      "budget_limit": "5000",
      "resets_at": "2026-10-01T00:00:00+03:00"
    }
  }
}
reasonЧто произошлоЧто делать
account_insufficient_fundsНа счёте не хватает средств, чтобы оплатить запрос.Пополните баланс в разделе Оплата.
api_key_budget_exceededКлюч упёрся в свой лимит расхода. Деньги на счёте при этом могут оставаться.Поднимите лимит ключа или дождитесь сброса (resets_at).
поля нетПровайдер вернул свой 402 на /v1/images, и ZvenoAI передал его как есть. Ни к вашему счёту, ни к лимитам ключа это не относится.Покажите пользователю общее сообщение об отказе оплаты.

В Chat Completions и видеогенерации провайдерский 402 наружу не выходит — там он приходит как 503.

api_key_budget_exceeded приходит только при аутентификации API-ключом, и вместе с ним — ещё два поля:

  • budget_limit — лимит ключа в рублях. Приходит строкой, а не числом; хвостовых нулей в дробной части нет.
  • resets_at — момент сброса лимита в формате RFC 3339 со смещением часового пояса. Период считается по московскому времени. Если у лимита нет автосброса, поля не будет: такой лимит действует всё время жизни ключа.

reason необязателен — проверяйте, что поле есть, прежде чем его читать: код вида error.metadata.reason.toLowerCase() упадёт на 402, который пришёл от провайдера.

Вместе с resets_at приходит и заголовок Retry-After с числом секунд до сброса, но браузерному коду он недоступен: CORS не открывает его для чтения. Поэтому момент сброса продублирован в теле — берите resets_at.

Moderation (HTTP 403)

403 — moderation reject
{
  "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 символов посередине с ..., если оригинал длиннее.

Видеогенерация (/v1/videos) обычно возвращает moderation-rejects провайдера как HTTP 400, а не 403 — провайдерский код приходит в error.metadata.raw.error.code (например, InputImageSensitiveContentDetected.PrivacyInformation при фото реальных людей в input_references). Перечень известных кодов и рекомендации — в разделе Provider moderation гайда по видеогенерации.

Provider error (HTTP 502 / 503)

502 — провайдер вернул ошибку
{
  "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].

Пример mid-stream ошибки в Chat Completions
: 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":500,"message":"Provider disconnected unexpectedly","metadata":{"error_type":"server"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}

data: [DONE]

В streaming-кадре error.code — число, HTTP-статус ошибки, как и в request-error envelope. Тип ошибки лежит в error.metadata.error_type, провайдер — в поле provider кадра. Для streaming в Responses API ошибки приходят отдельными событиями: response.failed, response.error или error.

Fallback по моделям

Передавайте массив моделей, если важно гарантировать ответ. ZvenoAI выполнит запрос первой доступной моделью:

Использование fallback моделей
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.content
operation := 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 за интервал — это индикатор проблем.

On this page