← Tous les articles

Une API compatible OpenAI en remplacement direct — migration en deux lignes

20 août 2026 · 6 min de lecture · api, migration

unbleep est une API compatible OpenAI, ce qui signifie en pratique que la migration tient en deux lignes et n'ajoute aucune dépendance. Vous gardez le SDK officiel, votre logique de retry, votre boucle de streaming, votre comptage de tokens et vos gestionnaires d'erreurs. Ce qui change, c'est la destination des requêtes et la clé qui les authentifie. Ce billet décrit la bascule, puis les quatre points qui diffèrent assez pour casser quelque chose si vous ne les connaissez pas.

Migrer vers l'API compatible OpenAI en deux lignes

python
import os

from openai import OpenAI

client = OpenAI(
    base_url="https://unbleep.ai/v1",
    api_key=os.environ["UNBLEEP_API_KEY"],
)

resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Summarise this incident report."}],
)
print(resp.choices[0].message.content)

Si vous préférez ne pas toucher au code du tout, le SDK lit les deux valeurs dans l'environnement, donc un changement de configuration suffit :

bash
export OPENAI_BASE_URL="https://unbleep.ai/v1"
export OPENAI_API_KEY="ub_live_9f2c..."

Cette même paire couvre l'essentiel de l'écosystème construit au-dessus du SDK — LangChain, LlamaIndex, Instructor, le Vercel AI SDK, tout ce qui expose un réglage d'URL de base. Les clés sont préfixées pour que les fuites sautent aux yeux des scanners de secrets : ub_live_ et ub_test_ sont toutes deux facturées sur le même crédit prépayé, au même tarif par token. Une clé de test est un identifiant distinct et révocable avec un plafond plus bas — 15 requêtes/minute au lieu des 60 du compte — pas une offre gratuite. Gardez les deux côté serveur.

GET /v1/models fonctionne, donc les outils qui énumèrent les modèles pour remplir une liste déroulante n'ont pas besoin de traitement particulier.

Choisir un modèle

Trois paliers. Les ids datés — unbleep-250811, unbleep-high-250811, unbleep-mini-250811 — sont acceptés comme alias, mais aujourd'hui ils résolvent vers le même build que l'id nu, et la réponse renvoie l'id nu. Voyez-y une graphie prête pour l'avenir, pas une garantie de reproductibilité ; si une évaluation doit être répétable, enregistrez les sorties, pas l'id du modèle.

| Modèle | Contexte | Prix entrée / sortie par 1M | Notes | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | Par défaut. Palier de raisonnement. | | unbleep-high | 1M* | $5.00 / $5.00 | Les plus gros travaux. Palier de raisonnement. | | unbleep-mini | 32K | $1.00 / $1.00 | Bon marché et rapide. Répond directement, sans trace de raisonnement. |

*Le corps de la requête est plafonné à 2 000 000 octets — environ 500k tokens — donc un seul appel ne peut pas réellement remplir la fenêtre de 1M ; au-delà, la réponse est un 413 payload_too_large.

Le plafond de 32K de unbleep-mini est celui qui piège ceux qui migrent depuis un modèle à 128K de contexte : un prompt qui passait avant sera désormais rejeté. Si vous routez selon le coût, routez aussi selon la longueur.

python
def pick_model(prompt_chars: int) -> str:
    """~4 chars/token is a deliberate under-estimate; leave room for the completion."""
    est_tokens = prompt_chars // 4
    if est_tokens < 24_000:
        return "unbleep-mini"
    return "unbleep" if est_tokens < 200_000 else "unbleep-high"

Streaming

Passez stream=True et vous obtenez des Server-Sent Events standard : chaque événement est un chat.completion.chunk portant un delta, et le flux se termine par un data: [DONE] littéral. Votre boucle existante fonctionne sans modification.

bash
curl -N https://unbleep.ai/v1/chat/completions \
  -H "Authorization: Bearer $UNBLEEP_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "unbleep",
    "messages": [{"role": "user", "content": "Explain the residual stream."}],
    "stream": true,
    "stream_options": {"include_usage": true}
  }'

Vous recevez les comptes de tokens que vous les demandiez ou non : unbleep demande toujours l'usage au backend et le relaie, et seul un stream_options: {"include_usage": false} explicite le retire en sortie. Dans les deux cas, le flux se termine par un dernier chunk dont le tableau choices est vide — et qui porte l'objet usage rempli, sauf si vous l'avez désactivé — c'est pourquoi la boucle ci-dessous vérifie choices avant d'y toucher.

Le champ reasoning_content

C'est le seul véritable ajout au schéma. unbleep et unbleep-high réfléchissent avant de répondre, et cette chaîne de raisonnement est renvoyée dans reasoning_content, un champ frère de content sur le message (hors streaming) ou sur le delta (en streaming). Les backends en amont hésitent entre reasoning et reasoning_content ; l'API normalise en reasoning_content pour que vous n'ayez jamais qu'une seule forme à gérer.

Comme il ne fait pas partie du schéma OpenAI, il n'est pas dans les stubs de typage du SDK. Les modèles de réponse acceptent des champs supplémentaires, donc l'accès par attribut fonctionne à l'exécution — mais lisez-le avec getattr pour qu'une réponse de mini, qui n'a pas de trace, ne lève pas d'exception :

python
import sys

stream = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "Why did this detection rule misfire?"}],
    stream=True,
    stream_options={"include_usage": True},
)

usage = None
for chunk in stream:
    if not chunk.choices:          # final usage-only chunk
        usage = chunk.usage
        continue
    delta = chunk.choices[0].delta
    thought = getattr(delta, "reasoning_content", None)
    if thought:                    # trace to stderr, answer to stdout
        sys.stderr.write(thought)
    if delta.content:
        sys.stdout.write(delta.content)

if usage:
    details = usage.completion_tokens_details
    print(f"\nreasoning tokens: {getattr(details, 'reasoning_tokens', 0)}")

Trois conséquences pratiques :

python
resp = client.chat.completions.create(
    model="unbleep",
    messages=[{"role": "user", "content": "phishing or benign?"}],
    max_tokens=4,
    extra_body={"thinking": False},   # spend the budget on the answer, not the trace
)

Ne renvoyez jamais reasoning_content dans un tour ultérieur comme contenu de l'assistant. C'est une sortie de diagnostic, pas un historique de conversation, et la rejouer dégrade la réponse suivante.

Les erreurs et les deux vrais pièges

Les erreurs utilisent l'enveloppe OpenAI — {"error": {"type", "code", "message"}} — donc vos blocs except existants continuent de fonctionner. Les codes de statut correspondent à ce que vous attendez : 401 clé invalide, 422 bloqué par policy: strict, 429 limite de débit, 5xx erreur en amont, à réessayer.

Le statut que vous n'avez probablement jamais géré est 402, crédit épuisé. Les comptes sont prépayés, donc pas de dépassement ni de facture — les requêtes s'arrêtent simplement jusqu'à ce que vous rechargiez. OpenAI signale l'épuisement du quota par un 429, ce qui veut dire que, dans une migration naïve, votre logique de backoff réessaiera un 402 indéfiniment. Traitez-le comme définitif et déclenchez une alerte.

Le second piège : system_fingerprint n'est pas renvoyé. Il identifie le backend qui sert la requête, il est donc retiré avec les autres champs propres au fournisseur. Si vous vous en servez comme clé de cache ou de vérification de reproductibilité, il vous faudra votre propre marqueur de version : les ids de modèle datés sont des alias du build courant, pas des instantanés figés, ils ne vous diront donc pas quand le backend change.

Les limites de débit reviennent en en-têtes sur chaque réponse — x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests — pour qu'un job batch puisse cadencer ses requêtes au lieu de découvrir le plafond en le heurtant. Il existe un second plafond que les en-têtes ne décrivent pas : au plus 8 requêtes en cours par compte, et la neuvième revient en 429 avec le code too_many_concurrent_requests. Un appel en streaming garde sa place jusqu'à la fin du flux, donc limitez votre propre pool de workers à 8.

Ce vers quoi vous pointez

Autant être explicite : les modèles derrière cet endpoint sont abliterated, c'est-à-dire que leur comportement de refus a été retiré au niveau des poids. C'est tout l'intérêt — c'est une API pour développeurs destinée à la recherche en sécurité, au red-teaming et à l'évaluation, où un refus est une erreur de mesure. Cela signifie aussi que les garde-fous habituels ne sont pas là pour rattraper un mauvais prompt, alors gardez un humain responsable des sorties et lisez la politique d'usage acceptable. La légalité de l'usage vous incombe.

Obtenez une clé API — la migration tient vraiment en deux lignes.