Главная/Ошибки
Гайд по ошибкам и лимитам запросов: практическое руководство для Uncensored API
Код в продакшене тратит больше времени на обработку ошибок, чем на счастливый сценарий. В этом руководстве описаны все коды состояния, которые возвращает Uncensored API, указано, на какие стоит делать повторные попытки, и приведены примеры клиентского ограничения скорости для лимита 300 запросов в минуту в Python asyncio и Node, а также спокойный способ отобразить ошибку 402 вашим пользователям.
Обновлено
Ключевые моменты
- Повторяйте только 429, 503 и транспортные сбои; исправляйте 400, 401, 403, 404 и 402 в источнике.
- Ошибки приходят в формате JSON с объектом ошибки, содержащим код и сообщение, поэтому сначала ветвите логику по статусу, а затем по коду.
- Токенный бак, пополняющийся 5 токенов в секунду, удерживает один ключ в пределах 300 запросов в минуту без обратного давления со стороны сервера.
- 402 означает, что предоплаченный баланс исчерпан или закончился пробный период; воспринимайте это как состояние продукта, а не как сбой.
Обертка ошибки
Каждый сбой возвращает тело JSON с единственным объектом верхнего уровня error. Важны два поля: code, короткая машинная строка, и message, текст для логов и людей.
{
"error": {
"code": "no_credit",
"message": "Balance used up or trial expired."
}
}Принимайте решения в таком порядке. Сначала смотрите на HTTP-статус, потому что он указывает на класс проблемы. Используйте code только для разделения случаев с одинаковым статусом и никогда не парсите message для логики, так как формулировки могут измениться. О структуре успешных ответов см. в API-справочнике.
Коды статуса, причины и действия
| Статус | Код | Типичная причина | Повторять? | Действие |
|---|---|---|---|---|
| 400 | bad request | Некорректный JSON или токены промпта плюс max_tokens выше 100 000 | Нет | Исправьте полезную нагрузку; сократите историю или уменьшите max_tokens |
| 401 | invalid key | Отсутствует заголовок, опечатка или ключ был перегенерирован | Нет | Обновите ключ в хранилище секретов |
| 402 | no_credit | Баланс исчерпан или закончился бесплатный пробный период | Нет | Пополните предоплаченный баланс, затем продолжайте |
| 403 | content_blocked | Запрос касается сексуального контента с участием несовершеннолетних | Нет | Не отправляйте повторно; покажите свое сообщение о политике |
| 404 | not found | Неверный путь, например, отсутствует префикс /v1 | Нет | Проверьте базовый URL |
| 429 | rate limit | Более 300 запросов в минуту по одному ключу | Да | Увеличьте задержку с джиттером; добавьте клиентское ограничение |
| 503 | upstream_busy | Временная нагрузка на мощность | Да | Повторите запрос через несколько секунд |
403 заслуживает особого внимания: это политическое решение, а не временная ошибка. Повторный запрос того же запроса тратит квоту и вернёт тот же ответ. Случай 400, который больше всего удивляет людей, — это сумма контекста. Промпт на 60 000 токенов с max_tokens равным 8 000 переполняется на 4 000 и завершается ошибкой, хотя каждое число в отдельности корректно.
Политика повторных попыток, которая ведёт себя корректно
Используйте небольшую ограниченную политику и применяйте её только там, где повторные попытки могут помочь.
- Внесите в белый список повторяемые сбои: 429, 503, сбросы соединений и таймауты. Всё остальное вызывает исключение немедленно.
- Экспоненциальная задержка с джиттером: ждите 1, 2, 4, 8 секунд плюс случайную долю секунды, с ограничением до 30. Джиттер предотвращает одновременные повторные попытки множества рабочих процессов.
- Ограничьте попытки до пяти. За этим пределом сбой более вероятен, чем случайность, и ожидающие задачи должны завершаться видимой ошибкой.
- Отключите автоповторные запросы в SDK (
max_retries=0), когда вы запускаете собственный цикл, иначе попытки умножаются, и ограничение скорости теряет счётчик. - Повторяйте потоки с самого начала. Поток, прерванный на полпути, нельзя продолжить; отбросьте частичный текст и повторите запрос или сохраните его и запросите продолжение.
Для 503 рекомендация — подождать несколько секунд, поэтому начните первую задержку с одной или двух секунд, а не давите сразу. Для 429 лечение — замедление, что автоматизируется в следующем разделе.
Укладывание в 300 запросов в минуту
Лимит составляет 300 запросов в минуту на ключ, что в среднем равно пяти в секунду. Токенный бак обеспечивает именно это: он удерживает небольшой всплеск токенов, пополняется пятью токенами в секунду и требует один токен за запрос. Если бак пуст, вызывающий засыпает до появления токена.
Версия на Python использует asyncio. Все двадцать задач используют одно вёдро и блокировку, поэтому одновременные корутины не могут превысить лимит. Обратите внимание, что вёдро учитывает каждую попытку, включая повторные.
import asyncio, os, random, time
from openai import AsyncOpenAI, APIStatusError, APIConnectionError
class TokenBucket:
"""Allows `rate` acquisitions per `per` seconds, with a small burst."""
def __init__(self, rate=300, per=60.0, burst=10):
self.fill = rate / per # tokens added per second (5.0)
self.cap = burst
self.tokens = burst
self.stamp = time.monotonic()
self.lock = asyncio.Lock()
async def acquire(self):
async with self.lock:
while True:
now = time.monotonic()
self.tokens = min(self.cap, self.tokens + (now - self.stamp) * self.fill)
self.stamp = now
if self.tokens >= 1:
self.tokens -= 1
return
await asyncio.sleep((1 - self.tokens) / self.fill)
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1",
api_key=os.environ["API_KEY"], max_retries=0)
bucket = TokenBucket()
RETRYABLE = {429, 503}
async def ask(prompt, attempts=5):
for n in range(attempts):
await bucket.acquire()
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[{"role": "user", "content": prompt}],
max_tokens=200,
)
return r.choices[0].message.content
except APIStatusError as e:
if e.status_code not in RETRYABLE or n == attempts - 1:
raise
except APIConnectionError:
if n == attempts - 1:
raise
await asyncio.sleep(min(30, 2 ** n) + random.uniform(0, 1))
async def main():
prompts = [f"Give one-line fact number {i} about tides." for i in range(1, 21)]
results = await asyncio.gather(*(ask(p) for p in prompts))
print(len(results), "answers; first:", results[0])
asyncio.run(main())Запустите его с экспортированным API_KEY. Двадцать запросов укладываются во всплеск плюс несколько секунд пополнения, поэтому вы увидите быстрое завершение; увеличьте количество до 600, и выполнение займёт около двух минут, что является работой лимита.
Версия для Node, использующая официальный пакет в ES-модуле, связывает получение токенов через очередь обещаний, чтобы вызывающие стороны обслуживались по порядку.
import OpenAI from "openai";
class TokenBucket {
constructor(rate = 300, perMs = 60000, burst = 10) {
this.fillPerMs = rate / perMs;
this.cap = burst;
this.tokens = burst;
this.stamp = Date.now();
this.queue = Promise.resolve();
}
acquire() {
// Serialize callers so the bucket math never races.
this.queue = this.queue.then(async () => {
for (;;) {
const now = Date.now();
this.tokens = Math.min(this.cap, this.tokens + (now - this.stamp) * this.fillPerMs);
this.stamp = now;
if (this.tokens >= 1) { this.tokens -= 1; return; }
await new Promise((r) => setTimeout(r, Math.ceil((1 - this.tokens) / this.fillPerMs)));
}
});
return this.queue;
}
}
const client = new OpenAI({
baseURL: "https://api.uncensoredapis.com/v1",
apiKey: process.env.API_KEY,
maxRetries: 0,
});
const bucket = new TokenBucket();
async function ask(prompt, attempts = 5) {
for (let n = 0; n < attempts; n++) {
await bucket.acquire();
try {
const r = await client.chat.completions.create({
model: "uncensored",
messages: [{ role: "user", content: prompt }],
max_tokens: 200,
});
return r.choices[0].message.content;
} catch (err) {
const retryable = err.status === 429 || err.status === 503 || err.status === undefined;
if (!retryable || n === attempts - 1) throw err;
await new Promise((r) => setTimeout(r, Math.min(30000, 2 ** n * 1000) + Math.random() * 1000));
}
}
}
const prompts = Array.from({ length: 20 }, (_, i) => `Give one-line fact number ${i + 1} about tides.`);
const answers = await Promise.all(prompts.map((p) => ask(p)));
console.log(answers.length, "answers; first:", answers[0]);Устанавливайте всплеск консервативно. Всплеск в 10 позволяет короткий пик, пока устойчивая скорость остаётся на уровне пяти в секунду. Если несколько процессов используют один ключ, разделите скорость между ними, например по 100 в минуту каждый для трёх рабочих процессов, или направьте весь трафик через одну очередь.
Корректная обработка 402 в вашем продукте
402 с кодом no_credit означает, что предоплаченный баланс достиг нуля или закончился бесплатный пробный период. Это единственная ошибка, которую ваши конечные пользователи никогда не должны видеть как стек вызовов, и она обычно сигнализирует о чём-то, что можете исправить только вы: пополнении.
Постройте три уровня. Во-первых, detect его централизованно в одной обёртке, чтобы ни одна точка вызова не должна была запоминать. Во-вторых, translate его: браузер или мобильное приложение получает нейтральное состояние, такое как capacity_paused, и показывает дружелюбный экран, никогда не показывая сообщение из вышестоящего сервиса. В-третьих, alert себя, потому что каждый пользователь затронут одновременно; отправьте уведомление владельцу аккаунта и переключите флаг функции, чтобы интерфейс перестал отправлять запросы.
# server side: translate the upstream 402 into a product state
from fastapi import FastAPI
from fastapi.responses import JSONResponse
from openai import AsyncOpenAI, APIStatusError
import os
app = FastAPI()
client = AsyncOpenAI(base_url="https://api.uncensoredapis.com/v1", api_key=os.environ["API_KEY"])
@app.post("/api/reply")
async def reply(body: dict):
try:
r = await client.chat.completions.create(
model="uncensored", messages=body["messages"], max_tokens=400)
return {"text": r.choices[0].message.content}
except APIStatusError as e:
if e.status_code == 402:
# Tell the client app to show a "temporarily unavailable" screen,
# and page yourself to top up the prepaid balance.
return JSONResponse({"state": "capacity_paused"}, status_code=503)
if e.status_code == 429 or e.status_code == 503:
return JSONResponse({"state": "busy_retry"}, status_code=503)
raiseДобавьте также проактивную защиту. Суммируйте usage.total_tokens за запрос в свою собственную книгу учёта и предупреждайте на выбранном вами пороге, чтобы пополнение происходило до блокировки пользователя. Помните, что пробный баланс составляет $0,50 и действует 7 дней, поэтому прототип может получить 402 только из-за истечения срока, даже если деньги ещё не потрачены. Тарифы для планирования указаны на странице тарифов.
Как только баланс снова станет положительным, воспроизведите работу, которую вы поставили на паузу. Ваша обёртка должна сбросить флаг паузы при первом успешном результате.
Что логировать и на что настраивать оповещения
Хорошие записи делают инциденты короткими. Логируйте для каждого запроса: ваш собственный идентификатор запроса, идентификатор завершения id, если он присутствует, HTTP-статус, код ошибки code, номер попытки и использование токенов. Никогда не логируйте заголовок Authorization или полные промпты, содержащие персональные данные.
Полезные оповещения в порядке приоритета: любой 402 (немедленно), устойчивая серия 401 (пересозданный ключ не был развёрнут везде), уровень 429 выше нескольких процентов вызовов (ваше ограничение настроено неверно) и кластеры 503 (снизьте нагрузку, рассмотрите очередь). Панель с подсчётом статусов в минуту делает все четыре видимыми одним взглядом. При правильной обработке эти коды становятся рутинными сигналами, а не авариями.
Тестирование путей сбоев перед запуском
Обработка ошибок, которая никогда не запускалась, — это догадка. Вы можете протестировать большинство ветвей, не дожидаясь реальной аварии, используя дешёвые запросы и собственные тестовые заглушки.
- 401: Вызовите
GET /v1/modelsс намеренно неверным ключом и подтвердите, что ваше приложение показывает операторам ошибку конфигурации, а не клиентам. - 404: Запросите
/v1/chat/completion(в единственном числе) один раз и подтвердите, что в строке лога указан путь, чтобы опечатка в базовом URL была очевидна за минуты. - 400: Отправьте
max_tokensравным 32 000 с промптом, дополненным примерно до 50 000 токенов. Сумма превышает 100 000, и вы можете наблюдать срабатывание пути валидации. - 429 и 503: вы не можете вызвать их по требованию, поэтому заглушите их. Оберните HTTP-клиент в фейк, который вызывает статус для первых двух вызовов, затем succeeds, и убедитесь, что ваш цикл спит, повторяет запросы и возвращает окончательный ответ.
- 402: Заглушите и это, и подтвердите, что UI показывает экран паузы, срабатывает оповещение и ничего не повторяется в тесном цикле.
Напишите это как автоматизированные тесты, запускаемые в непрерывной интеграции. Тест с таблицей данных с одной строкой на статус из таблицы выше занимает минуты на написание и защищает политику от доброжелательных рефакторингов. Наконец, запустите короткий нагрузочный тест против вашего собственного ограничения с заглушённым API: отправьте 1 000 запросов на получение токенов и утверждайте, что ни одно 60-секундное окно не содержит более 300 разрешений плюс всплеск. Это доказывает арифметику до того, как реальный трафик будет зависеть от неё. Когда вы позже прочтете руководство по циклу агентов, используйте ту же обёртку там, поскольку агенты умножают количество запросов и первыми достигают этих лимитов.
Вопросы и ответы
Считается ли 429 против моего баланса?
Проверьте показатели использования в своём собственном реестре, а не предполагайте. В любом случае, исправление — замедлить скорость и повторить с задержкой.
Следует ли повторять запрос с ответом 403 content_blocked?
Нет. Это блокировка политики, и идентичный запрос вернёт тот же результат. Покажите своё собственное сообщение и продолжайте работу.
Мой ключ внезапно возвращает 401. Что изменилось?
Чаще всего ключ был сгенерирован заново, что немедленно делает старый недействительным. Скопируйте новый ключ из вашего аккаунта и развёрните его снова.
Сколько воркеров могут использовать один ключ?
Все они делят лимит в 300 запросов в минуту. Разделите скорость между воркерами или направьте вызовы через одну очередь.
Ваш ключ — в одной форме от вас
Создайте аккаунт, скопируйте ключ, измените базовый URL. Это вся настройка.