← Alle Beiträge

Eine OpenAI-kompatible Drop-in-API – Migration in zwei Zeilen

20 Aug 2026 · 6 Min. Lesezeit · api, migration

unbleep ist eine OpenAI-kompatible API – in der Praxis heißt das: Die Migration besteht aus zwei Zeilen und braucht keine neue Abhängigkeit. Du behältst das offizielle SDK, deine Retry-Logik, deine Streaming-Schleife, deine Token-Buchhaltung und deine Error-Handler. Was sich ändert, ist, wohin die Requests gehen und welcher Key sie authentifiziert. Dieser Beitrag behandelt den Wechsel und danach die vier Dinge, die sich so weit unterscheiden, dass sie etwas kaputtmachen, wenn man sie nicht kennt.

Migration auf die OpenAI-kompatible API in zwei Zeilen

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)

Wenn du den Code lieber gar nicht anfassen willst: Das SDK liest beide Werte aus der Umgebung, eine Konfigurationsänderung reicht also:

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

Dasselbe Paar deckt den Großteil des Ökosystems ab, das auf dem SDK aufsetzt – LangChain, LlamaIndex, Instructor, das Vercel AI SDK, alles, was eine Base-URL-Einstellung anbietet. Keys tragen ein Präfix, damit Leaks für Secret-Scanner offensichtlich sind: ub_live_ und ub_test_ werden beide gegen dasselbe Prepaid-Guthaben zum selben Preis pro Token abgerechnet. Ein Test-Key ist ein separates, widerrufbares Credential mit niedrigerer Obergrenze – 15 Requests/Minute statt der 60 des Accounts – und kein Gratis-Tarif. Beide gehören auf die Serverseite.

GET /v1/models funktioniert, Tooling, das Modelle aufzählt, um ein Dropdown zu füllen, braucht also keine Sonderbehandlung.

Ein Modell wählen

Drei Stufen. Die datierten IDs – unbleep-250811, unbleep-high-250811, unbleep-mini-250811 – werden als Aliase akzeptiert, lösen heute aber auf denselben Build auf wie die nackte ID, und die Antwort meldet die nackte ID zurück. Betrachte sie als vorwärtskompatible Schreibweise, nicht als Reproduzierbarkeitsgarantie; wenn eine Evaluation wiederholbar sein muss, protokolliere die Ausgaben, nicht die Modell-ID.

| Modell | Kontext | Preis in / out pro 1M | Hinweise | | --- | --- | --- | --- | | unbleep | 256K | $3.00 / $3.00 | Standard. Reasoning-Stufe. | | unbleep-high | 1M* | $5.00 / $5.00 | Größte Jobs. Reasoning-Stufe. | | unbleep-mini | 32K | $1.00 / $1.00 | Günstig und schnell. Antwortet direkt, ohne Reasoning-Trace. |

*Der Request-Body ist auf 2.000.000 Bytes gedeckelt – grob 500k Tokens –, ein einzelner Aufruf kann das 1M-Fenster also gar nicht füllen; alles darüber kommt als 413 payload_too_large zurück.

Die 32K-Obergrenze von unbleep-mini ist die, die Leute beim Umstieg von einem 128K-Kontext-Modell erwischt: Ein Prompt, der vorher gepasst hat, wird jetzt abgelehnt. Wenn du nach Kosten routest, route auch nach Länge.

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

Setze stream=True, und du bekommst Standard-Server-Sent-Events: Jedes Event ist ein chat.completion.chunk mit einem delta, und der Stream endet mit einem wörtlichen data: [DONE]. Deine bestehende Schleife funktioniert unverändert.

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}
  }'

Die Token-Zahlen bekommst du, ob du sie anforderst oder nicht: unbleep fragt beim Backend immer usage an und reicht sie durch; nur ein explizites stream_options: {"include_usage": false} entfernt sie auf dem Weg nach draußen. So oder so endet der Stream mit einem letzten Chunk, dessen choices-Array leer ist – und der das befüllte usage-Objekt trägt, sofern du es nicht abgewählt hast –, weshalb die Schleife unten choices prüft, bevor sie darauf zugreift.

Das Feld reasoning_content

Das ist die eine echte Ergänzung des Schemas. unbleep und unbleep-high denken nach, bevor sie antworten, und diese Gedankenkette kommt in reasoning_content zurück – einem Geschwisterfeld von content auf der Message (nicht-streamend) bzw. dem Delta (streamend). Upstream-Backends sind sich uneins, ob es reasoning oder reasoning_content heißen soll; die API normalisiert auf reasoning_content, damit du immer nur eine Form behandeln musst.

Weil es nicht Teil des OpenAI-Schemas ist, steht es nicht in den Type-Stubs des SDK. Die Response-Modelle erlauben zusätzliche Felder, Attributzugriff funktioniert zur Laufzeit also – lies es aber per getattr, damit eine mini-Antwort, die keinen Trace hat, keine Exception wirft:

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)}")

Drei praktische Konsequenzen:

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
)

Füttere reasoning_content nie als Assistant-Content in einen späteren Turn zurück. Es ist Diagnoseausgabe, keine Gesprächshistorie, und sie erneut einzuspielen verschlechtert die nächste Antwort.

Fehler und die zwei echten Fallstricke

Fehler verwenden den OpenAI-Envelope – {"error": {"type", "code", "message"}} –, deine bestehenden except-Blöcke funktionieren also weiter. Die Statuscodes sind wie erwartet zugeordnet: 401 ungültiger Key, 422 von policy: strict blockiert, 429 Rate-Limit, 5xx wiederholbarer Upstream-Fehler.

Der Status, den du vermutlich noch nie behandelt hast, ist 402, kein Guthaben mehr. Accounts sind prepaid, es gibt also keine Überschreitung und keine Rechnung – die Requests hören einfach auf, bis du aufgeladen hast. OpenAI signalisiert ein erschöpftes Kontingent als 429, und deshalb führt der naive Migrationspfad dazu, dass deine Backoff-Logik ein 402 endlos wiederholt. Behandle es als endgültig und richte einen Alert darauf ein.

Der zweite Fallstrick: system_fingerprint wird nicht zurückgegeben. Es identifiziert das ausliefernde Backend und wird deshalb zusammen mit den anderen Vendor-Feldern entfernt. Wenn du einen Cache oder eine Reproduzierbarkeitsprüfung daran aufhängst, brauchst du einen eigenen Versionsmarker: Die datierten Modell-IDs sind Aliase für den aktuellen Build, keine eingefrorenen Snapshots, sie verraten dir also nicht, wann sich das Backend ändert.

Rate-Limits kommen als Header auf jeder Antwort zurück – x-ratelimit-limit-requests, x-ratelimit-remaining-requests, x-ratelimit-reset-requests –, ein Batch-Job kann seine Request-Rate also dosieren, statt die Obergrenze erst beim Aufprall zu entdecken. Es gibt eine zweite Obergrenze, die die Header nicht beschreiben: höchstens 8 Requests gleichzeitig pro Account, und ein neunter kommt als 429 mit dem Code too_many_concurrent_requests zurück. Ein Streaming-Aufruf hält seinen Slot, bis der Stream endet – deckle deinen eigenen Worker-Pool also bei 8.

Worauf du da zeigst

Um es explizit zu sagen: Die Modelle hinter diesem Endpunkt sind abliterated, ihr Refusal-Verhalten wurde also auf Gewichtsebene entfernt. Das ist der Sinn der Sache – es ist eine Entwickler-API für Security-Research, Red-Teaming und Evaluation, wo eine Ablehnung ein Messfehler ist. Es bedeutet aber auch, dass die üblichen Guardrails nicht da sind, um einen schlechten Prompt abzufangen – halte also einen Menschen für die Ausgaben verantwortlich und lies die Richtlinie zur zulässigen Nutzung. Rechtmäßige Nutzung liegt bei dir.

Hol dir einen API-Key – die Migration besteht wirklich nur aus zwei Zeilen.