Формат тела ошибки и порядок чтения
Все ответы об ошибках имеют одинаковую структуру:
{"error":{"code":"...","message":"..."}}
Порядок чтения фиксирован и состоит из трех шагов:
- HTTP-статус определяет общий класс ошибки;
error.codeуказывает точную причину и используется в коде для ветвления;error.messageпредназначен для чтения человеком и ведения логов, не используйте его для строкового сравнения.
Разделите ошибки на две категории: устраняемые повторными попытками (429, 503, тайм-ауты сети) и требующие изменений (остальные). Смешивание причин — частая ошибка, например, бесконечные повторные попытки при 402 приводят к десяткам неэффективных запросов в секунду.
В логах рекомендуется фиксировать четыре поля: время, статус, error.code и оценку prompt_tokens текущего запроса. Это позволит сразу увидеть всплеск определенного типа ошибок или общий рост сбоев. Настройте алерт: 402 — при первом появлении, так как сервис недоступен; 429 — по пропорции, так как единичные случаи нормальны, а устойчивые указывают на проблемы с параллельными запросами.
Быстрая шпаргалка по кодам состояния
| Статус | error.code | Значение | Повтор? |
|---|---|---|---|
| 400 | — | Неверный запрос, например, сумма prompt и max_tokens превышает 100k | Нет |
| 401 | — | Неверный или отсутствующий ключ | Нет |
| 402 | no_credit | Средства исчерпаны или пробный период истек | Нет |
| 403 | content_blocked | Контент заблокирован | Нет |
| 404 | — | Эндпоинт не найден | Нет |
| 429 | — | Превышен лимит запросов | Да, с задержкой |
| 503 | upstream_busy | Сервис временно перегружен | Да, через несколько секунд |
Значение «—» в таблице означает отсутствие фиксированной строки code, требующей специальной обработки; достаточно ветвления по статусу.
4xx: измените запрос, не повторяйте
400 Неверный запрос
- Причина 1: ошибка формата JSON, часто возникает из-за неэкранированных кавычек при ручном формировании строки.
- Причина 2: сумма токенов промпта и max_tokens превышает 100,000. Чаще всего возникает в длинных диалогах.
- Причина 3: тело запроса превышает 8 MB.
- Решение: используйте библиотеку JSON для сериализации; перед отправкой оценивайте количество токенов, при превышении лимита обрезайте историю или уменьшайте max_tokens; см.Практическое руководство по длинному контексту.
401 Проблемы с ключом
- Отсутствует заголовок или префикс
Bearer. - Ключ был перегенерирован, старый ключ стал недействительным, но на одном из серверов все еще используется старое значение.
- Переменная окружения не передана в контейнер или в cron-задачу.
402 no_credit
Баланс исчерпан или истёк 7-дневный пробный период. Восстановите доступ, пополнив предоплаченный баланс на странице аккаунта. Мониторьте баланс самостоятельно, чтобы не ждать, пока пользователи сообщат об ошибке.
403 content_blocked
Контент заблокирован. Допустимый контент для взрослых, вымышленные и спорные темы не блокируются, но контент сексуального характера с участием несовершеннолетних блокируется всегда, включая художественную литературу и ролевые игры. При ошибке 403 проверяйте ввод и историю на наличие такого контента, а не меняйте формулировки.
404 эндпоинт не найден
Доступны только два эндпоинта: POST /v1/chat/completions и GET /v1/models. Ошибка 404 возникает, если в пути отсутствует /v1, добавлены лишние слэши, допущена опечатка или вы обращаетесь к неподдерживаемым эндпоинтам для эмбеддингов, изображений и т. д.
400: ошибка часто возникает при длинных диалогах, где количество токенов растёт линейно. Тестирование проходит успешно, но после десятков раундов появляется 400. Это не нестабильность API, а исчерпание контекстного окна. Ограничьте историю: удаляйте старые раунды или сжимайте их в резюме. Оценивайте лимиты до отправки запроса.
Совет по диагностике 401: выведите в лог первые и последние четыре символа ключа, не показывая его полностью, и сравните с тем, что отображается на странице аккаунта. Это поможет убедиться, что в процессе используется именно тот ключ, который вы ожидаете. В контейнерных средах и при выполнении периодических задач наиболее частой причиной является отсутствие переменной окружения или чтение устаревшего значения.
429 и 503: два случая, когда нужно повторить запрос
429 лимит запросов
Лимит: 300 запросов в минуту на ключ. Вызывается пакетными задачами, использованием одного ключа несколькими экземплярами или штормом повторных попыток. Решение: сначала ограничьте частоту на стороне клиента (код ниже), затем применяйте экспоненциальную задержку при повторных попытках для 429.
503 upstream_busy
Сервис временно перегружен, повторите запрос через несколько секунд. Не отправляйте запросы сразу друг за другом и не повторяйте их по десять раз в секунду — это только ухудшит ситуацию.
Тайм-аут сети
Генерация длинного текста занимает много времени, поэтому не устанавливайте слишком короткий тайм-аут; в примере указано 120 секунд. При повторном запросе после тайм-аута помните: предыдущий запрос мог уже выполниться на сервере, и повторный вызов может привести к двойному списанию средств. Для генеративных запросов количество повторных запросов после тайм-аута должно быть небольшим; лучше использовать потоковую передачу, чтобы сократить время ожидания.
import os
import random
import time
import requests
URL = "https://api.wushenchaapi.com/v1/chat/completions"
HEADERS = {
"Authorization": "Bearer " + os.environ["API_KEY"],
"Content-Type": "application/json",
}
RETRYABLE = {429, 503} # 只重试这两类
FATAL = {400, 401, 402, 403, 404}
class ApiError(Exception):
def __init__(self, status, code, message):
super().__init__(f"{status} {code}: {message}")
self.status, self.code = status, code
def call(payload, max_retries=5, base=1.0, cap=30.0):
for attempt in range(max_retries + 1):
try:
r = requests.post(URL, headers=HEADERS, json=payload, timeout=120)
except (requests.ConnectionError, requests.Timeout):
r = None # 网络层失败,按可重试处理
if r is not None and r.ok:
return r.json()
if r is not None:
try:
err = r.json().get("error", {})
except ValueError:
err = {}
if r.status_code in FATAL or r.status_code not in RETRYABLE:
raise ApiError(r.status_code, err.get("code"), err.get("message"))
if attempt == max_retries:
raise ApiError(r.status_code if r is not None else 0, "retry_exhausted", "重试次数用尽")
delay = min(cap, base * (2 ** attempt)) * random.uniform(0.5, 1.0)
time.sleep(delay)
if __name__ == "__main__":
out = call({"model": "uncensored", "max_tokens": 100,
"messages": [{"role": "user", "content": "回复一个字:好"}]})
print(out["choices"][0]["message"]["content"])Важно: формула экспоненциальной задержки — min(cap, base × 2^n) × случайный коэффициент. Случайная добавка (джиттер) предотвращает одновременный повтор запросов множеством клиентов. Статусы из набора FATAL не повторяются вообще.
Ограничение скорости на стороне клиента и диагностические запросы
Вместо того чтобы ждать 429 и затем применять задержку, лучше ограничить скорость заранее. Приведённый ниже инструмент гарантирует, что количество запросов в минуту не превысит заданное значение; он безопасен для многопоточного использования:
import threading
import time
class RateGate:
"""简单的客户端限速:保证一分钟内请求数不超过 limit。"""
def __init__(self, limit=240): # 留出余量,低于 300/分钟
self.limit, self.stamps, self.lock = limit, [], threading.Lock()
def wait(self):
while True:
with self.lock:
now = time.time()
self.stamps = [t for t in self.stamps if now - t < 60]
if len(self.stamps) < self.limit:
self.stamps.append(now)
return
sleep_for = 60 - (now - self.stamps[0])
time.sleep(max(sleep_for, 0.05))Самый чистый способ диагностики — выполнить минимальный запрос через curl, отключив бизнес-логику. Флаг -i позволяет одновременно увидеть строку состояния и заголовки ответа:
curl -i https://api.wushenchaapi.com/v1/chat/completions \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"uncensored","max_tokens":20,"messages":[{"role":"user","content":"ping"}]}'Если curl работает, а ваш код — нет, проблема в вашем коде или среде (прокси, переменные окружения, кодировка). Если curl тоже не работает, проверьте ключ, баланс и сеть.
Почему лимит установлен на 240, а не на 300: при развёртывании нескольких экземпляров каждый экземпляр ограничивает скорость самостоятельно, и их суммарная нагрузка всё равно может превысить общий лимит. Кроме того, повторные запросы занимают дополнительный квоту. Оставить 20% запаса — это надёжный подход. Если у вас несколько экземпляров, распределите общий лимит поровну между ними или используйте общий счётчик для централизованного управления.
Чек-лист по устранению неполадок
- Читайте значение
error.codeв теле ответа, а не полагайтесь только на код состояния. - 401: префикс ключа, переменные окружения, не был ли ключ перегенерирован.
- 400: корректен ли JSON, не превышает ли сумма токенов промпта и max_tokens значение 100 000, не превышает ли тело запроса 8 МБ.
- 402: баланс и срок действия пробного периода.
- 403: не содержит ли ввод и история запрещённый контент.
- 404: является ли путь /v1/chat/completions или /v1/models.
- 429: не использует ли несколько экземпляров один ключ, применяется ли ограничение скорости на стороне клиента.
- 503: применялась ли экспоненциальная задержка при повторных запросх, превышал ли интервал несколько секунд.
- Тайм-аут: достаточно ли значение тайм-аута, можно ли переключиться на потоковую передачу.
- Если всё остальное исключено: воспроизведите проблему с помощью минимального запроса curl.
Если только начали подключаться, сначала ознакомьтесь со руководством по подключению. Дополнительные параметры описаны в документации.
Как пользоваться чек-листом: при возникновении проблемы последовательно исключайте варианты сверху вниз, не пропускайте пункты. Большинство «странностей» в конечном итоге сводятся к первым четырём пунктам.
Дополнительный совет: записывайте выводы по устранению неполадок в общую документацию команды. В следующий раз при аналогичной ошибке вы сможете сразу определить причину.