Streaming ist der Unterschied zwischen einer Chatbox, die acht Sekunden lang tot dasteht, und einer, die in unter einer Sekunde zu tippen beginnt. Die Mechanik ist dieselbe, egal ob hinter deiner base_url Claude, Gemini oder GPT steckt: Setze stream=True, iteriere über die Chunks, lies bei jedem das delta und höre beim [DONE]-Marker auf. Weil Brievio das OpenAI-Chat-Completions-Protokoll spricht, funktioniert exakt dieselbe Schleife über jedes echte First-Party-Modell hinweg — du änderst den model-String und sonst nichts.
Dieser Beitrag zeigt, wie Server-Sent-Events-Streaming über einen OpenAI-kompatiblen Endpunkt tatsächlich funktioniert, wie du mit stream_options präzise Token-Usage im finalen Chunk bekommst, das identische Muster in Python und Node sowie die zwei stillen Fehlermodi, die einen Stream gut aussehen lassen, während er dich klammheimlich hintergeht: gefälschtes (gepuffertes) Streaming und fehlende Usage.
Was "Streaming" über HTTP bedeutet
Ein Call ohne Streaming ist ein Request, eine Response: Der Server überlegt ein paar Sekunden und reicht dir dann die gesamte Completion auf einmal. Streaming hält die HTTP-Verbindung offen und schiebt die Antwort Stück für Stück hinaus, während das Modell sie generiert — über Server-Sent Events (SSE). Auf der Leitung kommt jedes Stück als Zeile an, die mit data: beginnt, gefolgt von einem JSON-Objekt, und der Stream endet mit einer wörtlichen data: [DONE]-Zeile.
Diesen Text parst du fast nie selbst — das übernimmt das SDK. Im Code bekommst du ein iterierbares Objekt aus Chunks. Jeder Chunk sieht aus wie ein normales Completion-Objekt, nur dass der Content in choices[0].delta statt in choices[0].message liegt und nur das Fragment enthält, das seit dem letzten Chunk generiert wurde. Verkettest du jedes delta.content der Reihe nach, hast du die vollständige Nachricht rekonstruiert. Die eine Metrik, die hier zählt, ist die Zeit bis zum ersten Token (TTFB): wie lange es dauert, bis das erste nicht-leere Delta auftaucht. Genau diese Zahl ist der ganze Grund, überhaupt zu streamen.
Das Python-Muster
Hier ist das Ganze — eine echte Streaming-Schleife, die zugleich die Usage einfängt. Die einzige Brievio-spezifische Zeile ist die base_url:
from openai import OpenAI
client = OpenAI(
api_key="sk-brievio-...",
base_url="https://api.brievio.com/v1", # eine base_url, echte First-Party-Modelle
)
# stream=True schaltet die Antwort auf einen Server-Sent-Events-Stream um.
# Du iterierst über das Objekt; jedes Element ist ein Chunk mit einem partiellen "delta".
stream = client.chat.completions.create(
model="claude-sonnet-4-6", # oder gemini-2.5-flash, gpt-..., etc.
messages=[{"role": "user", "content": "Explain Raft consensus in 200 words."}],
stream=True,
stream_options={"include_usage": True}, # Usage im FINALEN Chunk anfordern
)
usage = None
for chunk in stream:
# Das letzte Daten-Event vor [DONE] trägt usage und eine leere choices-Liste.
if chunk.usage is not None:
usage = chunk.usage
continue
delta = chunk.choices[0].delta.content
if delta:
print(delta, end="", flush=True) # Tokens rendern, sobald sie ankommen
print()
# usage ist nur befüllt, weil include_usage gesetzt wurde. Das sind echte Zahlen.
print(usage.prompt_tokens, usage.completion_tokens, usage.total_tokens)Drei Details, über die Leute stolpern. Erstens kann das Content-Delta bei manchen Chunks None oder leer sein (der Eröffnungs-Chunk setzt oft nur die Rolle), also prüfe es ab, bevor du ausgibst. Zweitens kommt der Chunk mit der usage nach dem Content und hat eine leere choices-Liste — deshalb prüft das Beispiel zuerst chunk.usage und macht ein continue. Drittens suchst du nicht selbst nach [DONE]; das SDK verbraucht diesen Marker und beendet den Iterator für dich. Würdest du den Endpunkt mit rohem requests oder fetch ansprechen, müsstest du an Zeilenumbrüchen trennen und bei [DONE] manuell abbrechen.
Dieselbe Schleife in Node
Das Node-SDK stellt den Stream als async iterable bereit, daher ist die Struktur identisch — for await ... of statt for und process.stdout.write statt eines flushenden print:
import OpenAI from "openai";
const client = new OpenAI({
apiKey: "sk-brievio-...",
baseURL: "https://api.brievio.com/v1",
});
// Gleicher Vertrag in Node: stream=true liefert ein async iterable von Chunks.
const stream = await client.chat.completions.create({
model: "claude-sonnet-4-6",
messages: [{ role: "user", content: "Explain Raft consensus in 200 words." }],
stream: true,
stream_options: { include_usage: true },
});
let usage = null;
for await (const chunk of stream) {
// Finales Event: choices ist leer, usage ist vorhanden.
if (chunk.usage) {
usage = chunk.usage;
continue;
}
const delta = chunk.choices[0]?.delta?.content;
if (delta) process.stdout.write(delta); // jedes Token ins Terminal schreiben
}
console.log();
console.log(usage?.prompt_tokens, usage?.completion_tokens, usage?.total_tokens);Beachte das Optional Chaining (chunk.choices[0]?.delta?.content). Im finalen, usage-tragenden Chunk ist choices leer, sodass der Zugriff auf [0] ohne die Absicherung genau auf der Ziellinie eine Exception werfen würde. Das ist der mit Abstand häufigste Grund, warum ein Node-Streaming-Handler beim letzten Event abstürzt, nachdem er die ganze Antwort über tadellos funktioniert hat.
Echte Usage im letzten Chunk bekommen
Standardmäßig enthält eine Streaming-Response keine Token-Zahlen — auf keinem Chunk gibt es ein usage-Objekt. Das ist ein bewusster Teil des OpenAI-Protokolls und beißt Teams, die in Produktion streamen und dann ihre Rechnung nicht abgleichen können. Die Lösung ist ein einziger Parameter:
- Setze
stream_options={"include_usage": True}(Python) oderstream_options: { include_usage: true }(Node). - Der Server gibt dann genau vor
[DONE]einen zusätzlichen Chunk aus, dessenchoicesleer ist und dessenusagedie Werteprompt_tokens,completion_tokensundtotal_tokensenthält. - Bei Brievio sind das die echten Zahlen, die das Modell meldet — dieselben Werte, die du bei einem Call ohne Streaming bekämst, abgerechnet zu rund 15% unter dem offiziellen Tarif. Es gibt kein aufgepolstertes Usage-Objekt und keinen injizierten System-Prompt, der die Prompt-Seite aufbläht.
Wenn du include_usage auslässt und trotzdem eine Token-Schätzung brauchst, bleibt dir nur, lokal mit dem Tokenizer des Modells zu zählen — was ungenau ist und Pflegeaufwand bedeutet. Setz einfach das Flag.
Die stillen Brüche: gefälschtes Streaming und fehlende Usage
Zwei Fehlermodi bestehen einen flüchtigen Blick und zeigen sich erst unter genauer Prüfung. Beide sind einen 20-Sekunden-Check wert, bevor du einem Gateway echten Traffic anvertraust.
- Gepuffertes "Fake"-Streaming. Manche Gateways akzeptieren
stream=True, warten auf die komplette Upstream-Completion und spielen sie dir dann am Ende als Schwall von Chunks zurück. Deine Schleife läuft, Deltas kommen an, alles sieht gestreamt aus — aber die TTFB ist identisch mit einem Call ohne Streaming, weil bis zum Ende der Generierung nichts gesendet wurde. Das verräterische Signal ist einfach: Miss die Lücke zwischen dem Absenden des Requests und dem ersten nicht-leeren Delta. Bei echtem Streaming landet sie deutlich unter einer Sekunde; bei einer gepufferten Wiedergabe entspricht sie der vollen Generierungszeit. Folgt die First-Token-Latenz der Gesamtlatenz, streamst du nicht, du schaust dir eine Aufzeichnung an. - Fehlende oder erfundene Usage. Ein Gateway, das
include_usagenicht respektiert, lässt dich bei gestreamten Calls ohne Token-Zahlen zurück — du gleichst deine Rechnung also gegen Luft ab. Schlimmer noch: Ein unehrliches kann einusage-Objekt mit aufgeblähten Zahlen anhängen, weil der Client bei einem Stream selten nachzählt. Prüfe es auf die langweilige Art: Lass denselben Prompt einmal gestreamt und einmal nicht laufen und bestätige, dass die Usage des gestreamten Final-Chunks zurusageohne Streaming passt. Sie sollten identisch sein. - Mid-Stream-Fehler, die wie ein sauberes Ende aussehen. Wenn das Upstream-Modell auf halbem Weg einen Fehler wirft, legt ein korrektes Gateway ihn als Exception in deiner Schleife offen, nicht als stille Abschneidung. Prüfe immer, dass du einen Finish-Reason (oder den Usage-Chunk) erhalten hast, bevor du den Text als vollständig behandelst — ein Stream, der einfach aufhört, ist nicht dasselbe wie ein Stream, der fertig geworden ist.
Das Fazit
Streaming über einen OpenAI-kompatiblen Endpunkt sind vier bewegliche Teile: stream=True, über Chunks iterieren, jedes delta lesen und das SDK [DONE] erledigen lassen. Füge stream_options={"include_usage": True} hinzu, und du bekommst zusätzlich ehrliche Token-Zahlen im finalen Chunk. Dieselben fünfzehn Zeilen funktionieren unverändert über Claude Sonnet 4.6, Gemini 2.5 Flash und die GPT-Familie hinter einer einzigen base_url — tausch den Model-String, behalte die Schleife.
Bevor du ausrollst, miss die Zeit bis zum ersten Token und vergleiche die Usage gestreamt vs. nicht gestreamt. Echtes Streaming liefert dir Sub-Sekunden-TTFB und übereinstimmende Zahlen; eine gepufferte Wiedergabe verrät sich über die Latenz. Bei Brievio werden fehlgeschlagene 4xx/5xx-Calls nicht berechnet, du kannst diese Checks also kostenlos laufen lassen. Sieh dir die Chat-Completions-Referenz für die vollständige Parameterliste an, die übrigen API-Docs für Tools und Vision über denselben Stream, den Leitfaden zum Aufrufen von Claude mit dem OpenAI-SDK für die Grundlagen ohne Streaming und den Modellkatalog für jeden Slug, auf den du diese Schleife richten kannst.