FR ▾

API LLM sans censure compatible OpenAIuncensoredapis.com

AccueilErreurs

Erreurs et limites de débit : un guide pratique pour Uncensored API

Le code de production passe plus de temps sur les chemins d'échec que sur le chemin heureux. Ce guide liste chaque code de statut renvoyé par Uncensored API, indique lesquels méritent une nouvelle tentative, et vous donne un throttling côté client pour la limite de 300 requêtes par minute en Python asyncio et Node, ainsi qu'une façon calme d'afficher un 402 à vos propres utilisateurs.

Mis à jour le

Points clés

  1. Ne réessayez que les 429, 503 et les échecs de transport ; corrigez les 400, 401, 403, 404 et 402 à la cause.
  2. Les erreurs arrivent en JSON avec un objet error contenant code et message, donc branchez sur le statut d'abord, puis sur le code.
  3. Un seau de jetons qui se remplit de 5 tokens par seconde maintient une clé sous les 300 requêtes par minute sans retour du serveur.
  4. Un 402 signifie que le solde prépayé est épuisé ou que l'essai est terminé ; traitez-le comme un état produit, pas comme un crash.

L'enveloppe d'erreur

Chaque échec renvoie un corps JSON avec un seul objet error de premier niveau. Deux champs sont importants : code, une courte chaîne pour les machines, et message, du texte destiné aux logs et aux humains.

{
  "error": {
    "code": "no_credit",
    "message": "Balance used up or trial expired."
  }
}

Décidez dans cet ordre. Regardez d'abord le statut HTTP, car il indique la classe du problème. Utilisez code uniquement pour séparer les cas qui partagent un statut, et n'analysez jamais message pour la logique, car le libellé peut changer. Pour la structure des réponses réussies, consultez la référence de l'API.

Codes de statut, causes et actions

StatutCodeCause typiqueNouvelle tentative ?Action
400requête incorrecteJSON malformé, ou tokens de prompt plus max_tokens au-dessus de 100 000NonCorrigez le payload ; réduisez l'historique ou abaissez max_tokens
401clé invalideEn-tête manquant, faute de frappe, ou clé régénéréeNonRechargez la clé depuis votre magasin de secrets
402no_creditSolde épuisé ou essai gratuit expiréNonRechargez le solde prépayé, puis reprenez
403content_blockedLa requête touche à du contenu sexuel impliquant des mineursNonNe renvoyez pas ; affichez votre propre message de politique
404non trouvéMauvais chemin, comme un préfixe /v1 manquantNonVérifiez l'URL de base
429limite de débitPlus de 300 requêtes en une minute sur une cléOuiBackoff avec jitter ; ajoutez un throttling client
503upstream_busyPression temporaire sur la capacitéOuiRéessayez après quelques secondes

Le 403 mérite une attention particulière : il s'agit d'une décision de politique, et non d'une panne transitoire. Réessayer la même requête gaspille le quota et renverra la même réponse. Le cas 400 qui surprend le plus est la somme du contexte. Un prompt de 60 000 tokens avec max_tokens de 8 000 déborde de 4 000 et échoue même si chaque nombre est valide individuellement.

Une politique de retry qui se comporte

Utilisez une politique petite et bornée, et appliquez-la uniquement là où le retry peut aider.

  1. Listez les erreurs réessayables : 429, 503, les réinitialisations de connexion et les délais d'attente dépassés. Tout le reste lève immédiatement une exception.
  2. Backoff exponentiel avec aléa : attendez 1, 2, 4, 8 secondes plus une fraction aléatoire de seconde, plafonnée à 30. L'aléa empêche de nombreux workers de réessayer en synchronisation.
  3. Limitez les tentatives à cinq. Au-delà, une panne est plus probable qu'une simple malchance, et les travaux en file d'attente doivent échouer visiblement.
  4. Désactivez les réessais automatiques du SDK (max_retries=0) lorsque vous exécutez votre propre boucle, sinon les tentatives s'accumulent et votre limitation perd le compte.
  5. Réessayez les flux depuis le début. Un flux qui échoue en cours ne peut pas être repris ; jetez le texte partiel et réémettez la requête, ou conservez-le et demandez une continuation.

Pour un 503, la recommandation est d'attendre quelques secondes, donc commencez votre premier délai près d'une ou deux secondes plutôt que de frapper immédiatement. Pour un 429, le remède est de ralentir, ce que la section suivante automatise.

Rester en dessous de 300 requêtes par minute

La limite est de 300 requêtes par minute par clé, soit cinq par seconde en moyenne. Un seau de jetons applique exactement cela : il contient jusqu'à une petite rafale de tokens, se remplit à raison de cinq par seconde, et fait que chaque requête coûte un token. Si le seau est vide, l'appelant dort jusqu'à ce qu'un token existe.

La version Python utilise asyncio. Les vingt tâches partagent un seul seau et un verrou, donc les coroutines concurrentes ne peuvent pas le dépasser. Notez que le seau compte chaque tentative, y compris les renvois.

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())

Exécutez-le avec API_KEY exporté. Vingt requêtes tiennent dans le burst plus quelques secondes de recharge, donc vous verrez la fin rapidement ; augmentez le nombre à 600 et l'exécution s'étale sur environ deux minutes, ce qui est la limite qui fait son travail.

La version Node, utilisant le package officiel dans un module ES, enchaîne les acquisitions via une file d'attente de promesses afin que les appelants soient servis dans l'ordre.

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]);

Dimensionnez la rafale de manière conservatrice. Une rafale de 10 permet un pic court tandis que le débit soutenu reste à cinq par seconde. Si plusieurs processus partagent une clé, divisez le débit entre eux, par exemple 100 par minute chacun sur trois workers, ou acheminez tout le trafic via une seule file d'attente.

Gestion gracieuse du 402 dans votre produit

Un 402 avec le code no_credit signifie que le solde prépayé a atteint zéro ou que le crédit d'essai gratuit a expiré. C'est la seule erreur que vos utilisateurs finaux ne devraient jamais voir sous forme de trace de pile, et elle signale généralement quelque chose que vous seul pouvez corriger : recharger le solde.

Construisez trois couches. Premièrement, detect-le centralisé dans un seul wrapper pour qu'aucun site d'appel n'ait à s'en souvenir. Deuxièmement, translate-le : le navigateur ou l'application mobile reçoit un état neutre tel que capacity_paused et affiche un écran convivial, jamais le message amont. Troisièmement, alert-vous-même, car chaque utilisateur est affecté en même temps ; envoyez une notification au propriétaire du compte et activez un drapeau de fonctionnalité pour que l'UI arrête de soumettre des requêtes.

# 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

Ajoutez également une protection proactive. Additionnez usage.total_tokens par requête dans votre propre registre et avertissez à un seuil de votre choix, afin qu'un rechargement ait lieu avant qu'un utilisateur ne soit bloqué. Souvenez-vous que le crédit d'essai gratuit est de 0,50 $ et dure 7 jours, donc un prototype peut atteindre un 402 dû à l'expiration seule avec de l'argent encore non dépensé. Les tarifs pour la planification sont sur la page tarifs.

Une fois le solide à nouveau positif, rejouez le travail que vous avez mis en file d'attente pendant la pause. Votre wrapper doit effacer le drapeau de pause dès le premier succès.

Que journaliser et sur quoi alerter

De bons journaux rendent les incidents courts. Journalisez, par requête : votre propre ID de requête, l'ID de complétion id lorsqu'il est présent, le statut HTTP, l'erreur code, le numéro de tentative et l'utilisation de tokens. Ne journalisez jamais l'en-tête Authorization ni les prompts complets contenant des données personnelles.

Alertes utiles, par ordre de priorité : tout 402 (immédiat), une série soutenue de 401 (une clé régénérée n'a pas été déployée partout), un 429 au-dessus de quelques pourcents d'appels (votre limiteur est mal dimensionné), et des grappes de 503 (mécanisme de limitation plus large, envisagez une file d'attente). Un tableau de bord des comptes de statut par minute rend les quatre visibles d'un coup d'œil. Bien gérés, ces codes deviennent des signaux de routine au lieu de pannes.

Tester vos chemins d'échec avant le lancement

La gestion des erreurs qui n'a jamais été exécutée est une supposition. Vous pouvez exercer la plupart des branches sans attendre une vraie panne, en utilisant des requêtes peu coûteuses et vos propres doubles de test.

  • 401: appelez GET /v1/models avec une clé délibérément incorrecte et confirmez que votre application affiche une erreur de configuration aux opérateurs, et non aux clients.
  • 404: envoyez une requête /v1/chat/completion (au singulier) une fois et confirmez que la ligne de journal nomme le chemin, afin qu'une faute de frappe dans une URL de base soit évidente en quelques minutes.
  • 400: envoyez max_tokens de 32 000 avec un prompt rempli à environ 50 000 tokens. La somme dépasse 100 000, et vous pouvez voir le chemin de validation s'activer.
  • 429 et 503 : vous ne pouvez pas les invoquer à la demande, donc simulez-les. Enveloppez votre client HTTP dans un faux qui lève le statut pour les deux premiers appels, puis réussit, et assertez que votre boucle dort, renvoie et retourne la réponse finale.
  • 402: simulez-le également, et confirmez que l'interface utilisateur affiche l'écran de pause, que l'alerte se déclenche et qu'aucun renvoi ne se produit dans une boucle serrée.

Rédigez-les en tant que tests automatisés qui s'exécutent en intégration continue. Un test piloté par table avec une ligne par statut du tableau ci-dessus prend quelques minutes à écrire et protège la politique contre les refactorisations bien intentionnées. Enfin, lancez un test de charge court contre votre propre limiteur avec l'API simulée : déclenchez 1 000 requêtes et assertez qu'aucune fenêtre de 60 secondes ne contient plus de 300 octrois plus la rafale. Cela prouve l'arithmétique avant que le trafic réel n'en dépende. Lorsque vous lirez plus tard le guide de la boucle agent, réutilisez le même wrapper là-bas, car les agents multiplient les comptes de requêtes et atteignent ces limites en premier.

Questions et réponses

Un 429 compte-t-il sur mon solde ?

Vérifiez les chiffres d'utilisation dans votre propre ledger plutôt que de supposer. Dans les deux cas, la solution est de ralentir et de réessayer avec un backoff.

Dois-je réessayer une réponse 403 content_blocked ?

Non. C'est un blocage de politique et une requête identique renverra le même résultat. Affichez votre propre message et passez à autre chose.

Ma clé renvoie soudainement un 401. Qu'est-ce qui a changé ?

Le plus souvent, la clé a été régénérée, ce qui invalide l'ancienne immédiatement. Copiez la nouvelle clé depuis votre compte et redéployez-la.

Combien de workers peuvent partager une seule clé ?

Tous partagent la limite de 300 requêtes par minute. Répartissez le taux entre les workers ou acheminez les appels via une seule file d'attente.

Votre clé est à un formulaire de vous

Créez un compte, copiez la clé, changez l'URL de base. C'est toute la configuration.