1. Формат ошибки
Каждая ошибка NormaHub — это HTTP-статус плюс тело с машиночитаемым кодом. Статус говорит классу проблемы (авторизация, оплата, лимиты, доступность), а поле error.code — точной причине внутри класса. Стройте ветвление именно по паре статус+код, а не по тексту message: тексты могут меняться, коды стабильны.
{
"error": {
"message": "Human-readable message",
"type": "invalid_request_error",
"code": "invalid_request"
}
}Для поддержки сохраняйте заголовок X-Request-Id каждого запроса — он связывает ваш клиентский вызов с записью в журнале и находится без ключа и промпта.
2. Таблица кодов
Постоянные ошибки повтором не лечатся: исправляйте запрос, ключ, баланс, разрешения или размер. Быстрая проверка ключа — один curl до /v1/models: он отделяет мёртвый ключ от живых денег за секунды.
400invalid_request- Проверьте JSON и обязательные поля маршрута.
401invalid_api_key- Ключ отсутствует, неверен или отозван.
402insufficient_balance- Пополните баланс владельца API-ключа.
403model_not_allowed- Модель или endpoint недоступны этому ключу.
413request_too_large- Уменьшите тело запроса или передаваемый контекст.
429budget_exceeded- Достигнут лимит API-ключа.
503provider_unavailable- Временно недоступна модель или upstream-провайдер.
# 401 или 402? Проверьте curl за 10 секунд:
curl -s -o /dev/null -w "%{http_code}
" \
https://api.normahub.cc/v1/models \
-H "Authorization: Bearer $NORMAHUB_API_KEY"
# 401 — ключ неверен или отозван, выпускайте новый в кабинете
# 200 — ключ жив, смотрите баланс и лимиты ключа3. Retry policy
Код 429 бывает двух видов — читайте error.code. При budget_exceeded исчерпан spend limit ключа: поднимайте его в кабинете, повтор с тем же ключом не поможет. При rate_limit_exceeded запросы идут слишком часто: повторяйте с экспоненциальной задержкой, учитывайте Retry-After и ограничивайте общее число попыток и deadline операции. Интерактивному UI чаще лучше быстро показать понятный отказ с кнопкой «повторить», чем держать пользователя в невидимой очереди.
import os
import random
import time
from openai import OpenAI, APIError
client = OpenAI(
api_key=os.environ["NORMAHUB_API_KEY"],
base_url="https://api.normahub.cc/v1",
)
RETRIABLE = {429, 500, 502, 503}
def chat(messages, tries=4, deadline=60):
started = time.monotonic()
for attempt in range(tries):
try:
return client.chat.completions.create(
model="MODEL_ID",
messages=messages,
timeout=20,
)
except APIError as e:
retryable = (e.status_code in RETRIABLE
and time.monotonic() - started < deadline)
if not retryable or attempt == tries - 1:
raise
delay = min(2 ** attempt + random.uniform(0, 1), 15)
time.sleep(delay)
raise RuntimeError("unreachable")Не повторяйте автоматически 400, 401, 402, 403 и 413: очередь таких повторов создаёт нагрузку, не приближая успех. Отдельный риск — восстановление после инцидента: вернувшиеся без верхнего предела повторы устраивают вторую волну нагрузки. Примеры обработки в SDK — в гайде Python и гайде Node.js.
4. Диагностика и лимиты
Минимальный набор для расследования: время, endpoint, model ID, HTTP-статус, error.code, X-Request-Id. Полный ключ, промпты и ответы в обычные application logs не пишите — проверяйте также CI-артефакты, docker history и тексты исключений HTTP-клиента, куда секреты протекают чаще всего. Системно про наблюдаемость — в статье «что логировать».
Лимиты бывают двух уровней: баланс владельца и spend limit конкретного ключа. После восстановления проверяйте оба, иначе «деньги есть, а запросы отклоняются». Баланс, лимиты и историю операций смотрите в личном кабинете, а цену каждого повтора считайте по гайду по стоимости.