Strona główna/Błędy
Błędy i limity: Praktyczna instrukcja dla Uncensored API
W kodzie produkcyjnym ścieżki błędów zajmują więcej czasu niż ścieżka sukcesu. Ten poradnik wymienia każdy kod statusu zwracany przez Uncensored API, wskazuje, które wymagają ponownej próby, i dostarcza mechanizmów ograniczania szybkości zapytań po stronie klienta dla limitu 300 zapytań na minutę w Python asyncio i Node, a także sposób na eleganckie wyświetlenie błędu 402 Twoim użytkownikom.
Zaktualizowano
Kluczowe punkty
- Ponawiaj tylko błędy 429, 503 oraz awarie transportu; napraw błędy 400, 401, 403, 404 i 402 w ich źródle.
- Błędy przychodzą jako JSON z obiektem błędu zawierającym code i message, więc rozgałęziaj się najpierw po statusie, potem po code.
- Wiadro tokenów napełniające się 5 tokenami na sekundę utrzymuje jeden klucz poniżej 300 zapytań na minutę bez odrzucania po stronie serwera.
- 402 oznacza, że przedpłacony balans się skończył lub próba się skończyła; traktuj to jako stan produktu, a nie crash.
Envelop błędu
Każdy błąd zwraca ciało JSON z jednym obiektem error na najwyższym poziomie. Istotne są dwa pola: code, krótki ciąg znaków dla maszyn, oraz message, tekst przeznaczony dla logów i ludzi.
{
"error": {
"code": "no_credit",
"message": "Balance used up or trial expired."
}
}Decyduj w tej kolejności. Najpierw sprawdź status HTTP, ponieważ określa on klasę problemu. Użyj code tylko do rozróżnienia przypadków, które dzielą ten sam status, i nigdy nie analizuj message pod kątem logiki, ponieważ treść może się zmienić. Aby poznać strukturę poprawnych odpowiedzi, zobacz odniesienie do API.
Kody statusu, przyczyny i działania
| Status | Kod | Typowa przyczyna | Ponowić? | Działanie |
|---|---|---|---|---|
| 400 | niepoprawne żądanie | Niepoprawny format JSON lub suma tokenów promptu plus max_tokens przekraczająca 100 000 | Nie | Napraw ładunek; skróć historię lub zmniejsz max_tokens |
| 401 | nieprawidłowy klucz | Brakujący nagłówek, literówka lub klucz, który został wygenerowany ponownie | Nie | Pobierz ponownie klucz ze swojego magazynu sekretów |
| 402 | no_credit | Saldo wyczerpane lub darmowy kredyt próbny wygasł | Nie | Doładuj przedpłacony balans, a następnie wznów |
| 403 | content_blocked | Zapytanie dotyczy treści seksualnych z udziałem małoletnich | Nie | Nie wysyłaj ponownie; pokaż własną wiadomość polityki |
| 404 | nie znaleziono | Nieprawidłowa ścieżka, np. brakujący prefiks /v1 | Nie | Sprawdź Base URL |
| 429 | limit zapytań | Powyżej 300 zapytań w ciągu minuty dla jednego klucza | Tak | Zastosuj wycofanie się z losowym opóźnieniem; dodaj ograniczanie szybkości zapytań po stronie klienta |
| 503 | upstream_busy | Tymczasowe obciążenie zasobów | Tak | Ponów próbę po kilku sekundach |
Błąd 403 zasługuje na podkreślenie: jest to decyzja polityki, a nie błąd przejściowy. Ponawianie identycznego zapytania marnuje limit i zwróci tę samą odpowiedź. Przypadek 400, który najbardziej zaskakuje, to przekroczenie sumy kontekstu. Prompt o długości 60 000 tokenów z max_tokens równym 8000 przekracza limit o 4000 tokenów i kończy się niepowodzeniem, mimo że każda z liczb jest poprawna sama w sobie.
Polityka ponownych prób, która działa poprawnie
Użyj małej, ograniczonej polityki i stosuj ją tylko tam, gdzie ponowienie może przynieść skutek.
- Błędy dopuszczające ponowną próbę: 429, 503, resety połączenia i przekroczenia limitu czasu. Wszystkie inne błędy zgłaszane są natychmiast.
- Wycofanie się z wykładniczym wzrostem czasu oczekiwania z losowym opóźnieniem: poczekaj 1, 2, 4, 8 sekund plus losowy ułamek sekundy, z maksymalnym limitem 30 sekund. Losowe opóźnienie zapobiega równoczesnemu ponawianiu prób przez wiele wątków.
- Ogranicz liczbę prób do pięciu. Po przekroczeniu tego limitu awaria jest bardziej prawdopodobna niż pech, a oczekujące zadania powinny zostać jawnie odrzucone.
- Wyłącz automatyczne ponawianie prób w SDK (
max_retries=0), gdy uruchamiasz własną pętlę, w przeciwnym razie liczba prób się mnoży, a ogranicznik szybkości traci licznik. - Ponawiaj strumienie od początku. Strumień, który ulegnie awarii w trakcie, nie może być wznowiony; odrzuć częściowy tekst i ponów zapytanie, lub zachowaj go i poproś o kontynuację.
W przypadku błędu 503 zaleca się odczekanie kilku sekund, więc rozpocznij pierwsze opóźnienie od jednej lub dwóch sekund, zamiast natychmiast bombardować serwer. W przypadku błędu 429 rozwiązaniem jest zwolnienie tempa, co automatyzuje następna sekcja.
Pozostawanie poniżej 300 zapytań na minutę
Limit wynosi 300 zapytań na minutę na klucz, co daje średnio pięć na sekundę. Wiadro tokenów egzekwuje dokładnie ten limit: przechowuje do pewnej liczby tokenów dla krótkiego szczytu, napełnia się pięcioma tokenami na sekundę i pobiera jeden token za każde zapytanie. Jeśli wiadro jest puste, wywołujący usypia się, aż pojawi się token.
Wersja Pythona wykorzystuje asyncio. Wszystkie dwadzieścia zadań dzieli jedno wiadro i blokadę, więc współbieżne korutyny nie mogą go nadużyć. Pamiętaj, że wiadro liczy każdą próbę, w tym ponowienia.
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())Uruchom go z wyeksportowanym API_KEY. Dwadzieścia zapytań mieści się w puli plus kilka sekund odświeżania, więc zobaczysz szybkie zakończenie; zwiększ liczbę do 600, a uruchomienie rozciągnie się na około dwie minuty, co jest limitem wykonującym swoją pracę.
Wersja Node.js, korzystająca z oficjalnego pakietu w module ES, łączy pozyskiwanie tokenów poprzez kolejkę promise, dzięki czemu wywołujący są obsługiwani w kolejności.
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]);Dostosuj limit szczytowy ostrożnie. Szczyt o wartości 10 pozwala na krótką spike, podczas gdy utrzymywane tempo pozostaje na poziomie pięciu na sekundę. Jeśli kilka procesów dzieli jeden klucz, podziel tempo między nie, na przykład 100 na minutę każdy dla trzech wątków, lub przekaż cały ruch przez jedną kolejkę.
Eleganckie obsłużenie błędu 402 w Twojej aplikacji
Błąd 402 z kodem no_credit oznacza, że przedpłacony balans osiągnął zero lub darmowy kredyt próbny wygasł. To jedyny błąd, którego użytkownicy końcowi nie powinni widzieć jako stack trace, i zwykle sygnalizuje coś, co możesz naprawić tylko Ty: doładowanie.
Zbuduj trzy warstwy. Po pierwsze, detect to centralnie w jednym wrapperze, aby żadna strona wywołania nie musiała pamiętać. Po drugie, translate to: przeglądarka lub aplikacja mobilna otrzymuje neutralny stan taki jak capacity_paused i wyświetla przyjazny ekran, nigdy komunikat od serwera. Po trzecie, alert siebie, ponieważ każdy użytkownik jest dotknięty jednocześnie; wyślij powiadomienie do właściciela konta i przełącz flagę funkcji, aby interfejs UI przestał wysyłać zapytania.
# 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)
raiseDodaj również ochronę proaktywną. Sumuj usage.total_tokens dla każdego zapytania w swoim własnym rejestrze i ostrzegaj przy wybranym przez Ciebie progu, aby doładowanie nastąpiło zanim użytkownik zostanie zablokowany. Pamiętaj, że kredyt próbny wynosi $0,50 i trwa 7 dni, więc prototyp może osiągnąć błąd 402 tylko z powodu wygaśnięcia, mimo że pieniądze nie zostały jeszcze wydane. Stawki do planowania znajdziesz na stronie z cennikiem.
Gdy saldo ponownie stanie się dodatnie, odtwórz zadania, które zablokowałeś podczas pauzy. Twój wrapper powinien usunąć flagę pauzy przy pierwszym sukcesie.
Co logować i na co reagować alertami
Dobre zapisy skracają czas trwania incydentów. Loguj dla każdego zapytania: własne ID zapytania, completion id (jeśli występuje), status HTTP, błąd code, numer próby oraz zużycie tokenów. Nigdy nie loguj nagłówka Authorization ani pełnych promptów zawierających dane osobowe.
Użyteczne alerty, w kolejności priorytetu: dowolny błąd 402 (natychmiastowy), ciągłe występowanie błędów 401 (odnowiony klucz nie został wdrożony wszędzie), limit 429 powyżej kilku procent wywołań (Twój ogranicznik jest źle skonfigurowany) oraz klastry 503 (zastosuj szersze wycofanie się, rozważ kolejkę). Dashboard z liczbą statusów na minutę sprawia, że wszystkie cztery są widoczne na pierwszy rzut oka. Dobrze obsłużone, stają się one rutynowymi sygnałami zamiast awarii.
Testowanie ścieżek błędów przed uruchomieniem
Obsługa błędów, która nigdy nie została uruchomiona, to zgadywanie. Możesz przetestować większość gałęzi bez czekania na prawdziwą awarię, używając tanich zapytań i własnych obiektów testowych.
- 401: wywołaj
GET /v1/modelsz celowo błędnym kluczem i potwierdź, że Twoja aplikacja pokazuje błąd konfiguracji operatorom, a nie klientom. - 404: wyślij zapytanie
/v1/chat/completion(pojedyncze) raz i potwierdź, że linia logu wskazuje ścieżkę, aby literówka w Base URL była widoczna w ciągu minut. - 400: wyślij
max_tokensrówny 32 000 z promptem wypełnionym do około 50 000 tokenów. Suma przekracza 100 000, a Ty możesz obserwować uruchomienie ścieżki walidacji. - 429 i 503: nie możesz ich wywołać na żądanie, więc je zmockuj. Owiń swój klient HTTP mockiem, który zwraca ten status dla dwóch pierwszych wywołań, a następnie zwraca sukces, i sprawdź, czy Twoja pętla wykonuje sleep, retry i zwraca ostateczną odpowiedź.
- 402: zmockuj to również i potwierdź, że UI pokazuje ekran pauzy, alert się pojawia, a nic nie retryuje w ścisłej pętli.
Napisz to jako automatyczne testy uruchamiane w ciągłej integracji. Test sterowany tabelą z jednym wierszem dla każdego statusu z powyższej tabeli zajmuje kilka minut na napisanie i chroni politykę przed dobrze myślącymi refaktoryzacjami. Na koniec uruchom krótki test obciążenia przeciwko swojemu ogranicznikowi ze stubowanym API: wyślij 1000 żądań i sprawdź, czy żadne okno 60-sekundowe nie zawiera więcej niż 300 przyznań plus limit szczytowy. To dowodzi arytmetyki zanim prawdziwy ruch na niej zależy. Kiedy później przeczytasz przewodnik po pętli agenta, użyj tego samego wrappera tam, ponieważ agenci mnożą liczbę zapytań i osiągają te limity jako pierwsi.
Pytania i odpowiedzi
Czy 429 liczy się do mojego salda?
Sprawdź figury użycia w swojej własnej księdze, zamiast zakładać. W każdym razie rozwiązaniem jest zwolnienie tempa i ponowienie z opóźnieniem.
Czy powinienem ponowić próbę odpowiedzi 403 content_blocked?
Nie. To blokada polityki, a identyczne zapytanie zwróci ten sam wynik. Wyświetl własną wiadomość i przejdź dalej.
Mój klucz nagle zwraca 401. Co się zmieniło?
Najczęściej klucz został odnowiony, co unieważnia stary natychmiast. Skopiuj nowy klucz ze swojego konta i wdróż go ponownie.
Ile wątków może dzielić jeden klucz?
Wszystkie one dzielą limit 300 zapytań na minutę. Podziel tempo między wątki lub przekaż wywołania przez jedną kolejkę.
Twój klucz jest o jeden formularz stąd
Utwórz konto, skopiuj klucz, zmień bazowy URL. To cała konfiguracja.