Home/Errors
Fouten en rate limits: praktisch handboek voor Uncensored API
Productiecode besteedt meer tijd aan foutpaden dan aan het happy path. Deze handleiding somt elke statuscode op die Uncensored API retourneert, geeft aan welke een retry waard zijn, en biedt client-side throttling voor de limiet van 300 verzoeken per minuut in Python asyncio en Node, plus een rustige manier om een 402 aan je eigen gebruikers te tonen.
Bijgewerkt
Belangrijkste punten
- Probeer alleen 429, 503 en transportfouten opnieuw; los 400, 401, 403, 404 en 402 op bij de oorzaak.
- Fouten komen als JSON met een error-object met code en message, dus controleer eerst op status, daarna op code.
- Een token bucket die 5 tokens per seconde bijvult, houdt één sleutel onder de 300 verzoeken per minuut zonder terugkoppeling van de server.
- Een 402 betekent dat het prepaid tegoed op is of de proefperiode is verlopen; behandel het als een product state, geen crash.
The error envelope
Elke fout retourneert een JSON-antwoord met een enkel object op het hoogste niveau error. Twee velden zijn belangrijk: code, een korte machine string, en message, tekst bedoeld voor logs en mensen.
{
"error": {
"code": "no_credit",
"message": "Balance used up or trial expired."
}
}Beslis in deze volgorde. Kijk eerst naar de HTTP-status, omdat dit de klasse van het probleem aangeeft. Gebruik code alleen om gevallen te onderscheiden die dezelfde status delen, en parse message nooit voor logica, omdat de wording kan veranderen. Voor de vorm van succesvolle antwoorden, zie de API-referentie.
Statuscodes, oorzaken en acties
| Status | Code | Typische oorzaak | Retry? | Actie |
|---|---|---|---|---|
| 400 | bad request | Gemankeerde JSON, of prompt tokens plus max_tokens boven de 100.000 | Nee | Los de payload op; knip history of verlaag max_tokens |
| 401 | invalid key | Ontbrekende header, typo, of een sleutel die geregend is | Nee | Laad de sleutel opnieuw in van je secret store |
| 402 | no_credit | Saldo opgebruikt of gratis trial verlopen | Nee | Laad het prepaid saldo op, en ga dan verder |
| 403 | content_blocked | Verzoek raakt seksuele content met minderjarigen | Nee | Niet opnieuw verzenden; toon je eigen policy bericht |
| 404 | not found | Verkeerd pad, zoals een ontbrekende /v1 prefix | Nee | Controleer de base URL |
| 429 | rate limit | Meer dan 300 verzoeken in een minuut op één sleutel | Ja | Back off met jitter; voeg client throttling toe |
| 503 | upstream_busy | Tijdelijke capaciteitsdruk | Ja | Retry na een paar seconden |
De 403 verdient nadruk: het is een beleidsbeslissing, geen tijdelijke storing. Het identieke verzoek opnieuw proberen verspillt quota en levert hetzelfde antwoord op. De 400-geval dat mensen het meest verbaast, is de contextsom. Een prompt van 60.000 tokens met max_tokens van 8.000 loopt over met 4.000 en faalt, hoewel elk getal op zich geldig is.
Een retry-beleid dat zich gedraagt
Gebruik een klein, gebonden beleid en pas het alleen toe waar herhalen helpt.
- Whitelist voor herhaalbare fouten: 429, 503, verbindingsherstellingen en time-outs. Alles anders wordt onmiddellijk gegooid.
- Exponentiële backoff met jitter: wacht 1, 2, 4, 8 seconden plus een willekeurig deel van een seconde, met een maximum van 30. Jitter voorkomt dat veel workers in lockstep opnieuw proberen.
- Beperk pogingen tot vijf. Daarboven is een storing waarschijnlijker dan pech, en ingewachte taken moeten zichtbaar falen.
- Schakel SDK auto-retries uit (
max_retries=0) als je je eigen lus uitvoert, anders verdubbelen de pogingen en verliest je throttle het overzicht. - Herstart streams vanaf het begin. Een stream die halverwege faalt, kan niet worden hervat; verwerp de gedeeltelijke tekst en stuur het verzoek opnieuw, of bewaar het en vraag om een voortzetting.
Voor een 503 is het advies om een paar seconden te wachten, dus begin je eerste delay bij één of twee seconden in plaats van direct te hammeren. Voor een 429 is de remedie langzamer gaan, wat de volgende sectie automatiseert.
Blijf onder de 300 verzoeken per minuut
De limiet is 300 verzoeken per minuut per sleutel, wat gemiddeld vijf per seconde is. Een token bucket handhaaft precies dat: het houdt tot een kleine burst van tokens vast, vult aan met vijf per seconde, en zorgt ervoor dat elk verzoek één token kost. Als de bucket leeg is, slaapt de caller tot er een token beschikbaar is.
De Python-versie gebruikt asyncio. Alle twintig taken delen één bucket en een lock, dus gelijktijdige coroutines kunnen het niet overschrijden. Let op dat de bucket elke poging telt, inclusief retries.
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())Voer het uit met API_KEY geëxporteerd. Twintig verzoeken passen binnen de burst plus een paar seconden van vullen, dus je ziet het snel afsluiten; verhoog het aantal naar 600 en de run strekt zich uit tot ongeveer twee minuten, wat de limiet zijn taak laat doen.
De Node-versie, met het officiële pakket in een ES-module, ketent acquisities via een promise queue zodat aanroepers in volgorde worden bediend.
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]);Stel de burst conservatief in. Een burst van 10 staat een korte piek toe terwijl het duurzame tempo vijf per seconde blijft. Als meerdere processen één sleutel delen, verdeel dan het tempo over hen, bijvoorbeeld 100 per minuut elk over drie workers, of routeer al het verkeer via één queue.
402 afhandelen in je product
Een 402 met code no_credit betekent dat het prepaid tegoed nul is geworden of dat de gratis proefperiode is verlopen. Het is de enige fout die je eindgebruikers nooit als stack trace moeten zien, en het signaleert meestal iets dat jij alleen kunt oplossen: opwaarderen.
Bouw drie lagen. Eerst, detect het centraal in één wrapper, zodat geen enkele call site het hoeft te onthouden. Tweede, translate het: de browser of mobiele app ontvangt een neutrale status zoals capacity_paused en toont een vriendelijk scherm, nooit het upstream bericht. Derde, alert jezelf, omdat elke gebruiker tegelijkertijd wordt beïnvloed; stuur een melding naar de accounteigenaar en zet een feature flag zodat de UI stopt met het indienen van verzoeken.
# 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)
raiseVoeg ook proactieve bescherming toe. Tel usage.total_tokens per verzoek op in je eigen grootboek en waarschuw bij een drempel die je kiest, zodat een top-up gebeurt voordat een gebruiker wordt geblokkeerd. Onthoud dat gratis proeftegoed $0,50 is en 7 dagen duurt, dus een prototype kan 402 bereiken door alleen verloop, met geld dat nog niet is uitgegeven. Tarieven voor planning staan op de prijzenpagina.
Zodra het saldo weer positief is, speel je de taken af die je hebt ingewacht tijdens het pauzeren. Je wrapper moet de pauze-vlag wissen bij het eerste succes.
Wat loggen en waarop alerten
Goede logs maken incidenten kort. Log, per verzoek: je eigen request id, de completion id wanneer aanwezig, HTTP-status, fout code, pogingsnummer en tokengebruik. Log nooit de Authorization-header of volledige prompts die persoonlijke gegevens bevatten.
Nuttige alerts, in prioriteit: elke 402 (direct), een aanhoudende reeks 401s (een opnieuw gegenereerde sleutel is niet overal geïmplementeerd), 429 rate boven een paar procent van calls (je throttle is verkeerd ingedeeld), en 503 clusters (back off wider, overweeg een queue). Een dashboard van status counts per minuut maakt alle vier zichtbaar in één oogopslag. Goed afgehandeld, worden deze codes routine signalen in plaats van uitval.
Je foutpaden testen vóór de lancering
Error handling die nog nooit is uitgevoerd is een gok. Je kunt de meeste branches oefenen zonder te wachten op een echte outage, met goedkope verzoeken en je eigen test doubles.
- 401: call
GET /v1/modelsmet een expres verkeerde sleutel en bevestig dat je app een configuratiefout aan operators toont, niet aan klanten. - 404: request
/v1/chat/completion(enkelvoud) één keer en bevestig dat de logregel het pad noemt, zodat een typefout in een base URL binnen minuten duidelijk is. - 400: stuur
max_tokensvan 32.000 met een prompt opgevuld tot ongeveer 50.000 tokens. De som passeert 100.000, en je kunt zien hoe het foutpad afvuurt. - 429 en 503: je kunt deze niet op verzoek oproepen, dus simuleer ze. Wikkel je HTTP-client in een fake die de statuscode voor de eerste twee calls verhoogt, daarna slaagt, en controleer dat je lus slaapt, retryt en het eindantwoord retourneert.
- 402: simuleer het ook, en bevestig dat de UI het pauzescherm toont, de alert afgaat en er niets retryt in een strakke lus.
Schrijf deze als geautomatiseerde tests die in continue integratie draaien. Een tabelgestuurde test met één rij per status uit de tabel hierboven is in minuten geschreven en beschermt het beleid tegen welbedoelde refactors. Voer tot slot een korte load test uit tegen je eigen throttle met de API gestubbed: fire 1.000 acquisitions en controleer dat geen 60-seconden window meer dan 300 grants plus de burst bevat. Dat bewijst de rekenkunde voordat echt verkeer erop leunt. Wanneer je later de agent loop guide leest, hergebruik dan dezelfde wrapper daar, omdat agents verzoeksommen vermenigvuldigen en deze limieten als eerste raken.
Vragen en antwoorden
Telt een 429 mee tegen mijn saldo?
Controleer de usage figures in je eigen ledger in plaats van te aannemen. Hoe dan ook is de oplossing om langzamer te gaan en te retryen met backoff.
Moet ik een 403 content_blocked response retryen?
Nee. Het is een policy block en een identiek verzoek levert hetzelfde resultaat op. Toon je eigen bericht en ga verder.
Mijn sleutel geeft plotseling 401. Wat is er veranderd?
Meestal is de sleutel vernieuwd, wat de oude direct ongeldig maakt. Kopieer de nieuwe sleutel van je account en deploy hem opnieuw.
Hoeveel workers kunnen één sleutel delen?
Ze delen allemaal de limiet van 300 verzoeken per minuut. Verdeel het tempo over workers of stuur calls via één queue.
Je sleutel is nog maar één formulier verwijderd
Maak een account aan, kopieer de sleutel, verander de base URL. Dat is de hele setup.