Streaming-Responses
Bei längeren Antworten kann die Wartezeit bis zur kompletten Generierung spürbar sein. Streaming löst dieses Problem: Statt auf die fertige Antwort zu warten, liefert der Server die einzelnen Tokens unmittelbar nach ihrer Erzeugung über Server-Sent Events (SSE) an den Client. So entsteht der bekannte “Tippt-in-Echtzeit”-Effekt, und die ersten Wörter erscheinen schon nach Bruchteilen einer Sekunde.
Aktiviert wird das Streaming durch Setzen von "stream": true im Request-Body. Alle übrigen Parameter bleiben unverändert gegenüber einer normalen Chat-Completion-Anfrage.
curl -X POST https://ai.noris.de/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -N \ -d '{ "model": "vllm/release/gpt-oss-120b", "messages": [{"role": "user", "content": "Erzähl mir eine Geschichte"}], "stream": true }'Das Flag -N deaktiviert Pufferung, damit jedes Chunk sofort im Terminal erscheint.
SSE-Format
Abschnitt betitelt „SSE-Format“Die Antwort besteht aus einer Folge von Zeilen, jeweils eingeleitet durch data: . Dahinter steht ein JSON-Objekt mit einem Teilfragment (delta) der Nachricht. Am Ende der Übertragung sendet der Server eine abschließende Zeile data: [DONE]. Typische Ausgabe:
data: {"id":"chatcmpl-xyz","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl-xyz","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Es"},"finish_reason":null}]}
data: {"id":"chatcmpl-xyz","object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":" war"},"finish_reason":null}]}
...
data: [DONE]Wichtig: Im Gegensatz zur Nicht-Streaming-Variante liegen die Inhalte nicht in message.content, sondern verteilt über mehrere Chunks in choices[0].delta.content. Konkatenieren Sie alle delta.content-Fragmente, um den vollständigen Text zu erhalten. Die Felder usage und finish_reason werden ausschließlich im letzten Daten-Chunk vor [DONE] geliefert, frühere Chunks enthalten dafür null.
Das openai-SDK kapselt das SSE-Parsing vollständig. Übergeben Sie stream=True und iterieren Sie über die zurückgegebene Stream-Instanz:
from openai import OpenAI
client = OpenAI(base_url="https://ai.noris.de/v1", api_key="YOUR_API_KEY")
stream = client.chat.completions.create( model="vllm/release/gpt-oss-120b", messages=[{"role": "user", "content": "Erzähl mir eine Geschichte"}], stream=True)for chunk in stream: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="")Aufgrund von end="" wird jeder Teil sofort ausgegeben, ohne dass die Konsole auf einen Zeilenumbruch wartet. Falls Sie die Token-Metriken benötigen, greifen Sie im letzten Chunk auf chunk.usage zu (sofern der Provider stream_options={"include_usage": True} unterstützt).
JavaScript
Abschnitt betitelt „JavaScript“Im Browser oder in Node.js bietet sich die ReadableStream-Schnittstelle von fetch an. Parsen Sie die data:-Zeilen manuell und brechen Sie bei [DONE] ab:
const response = await fetch("https://ai.noris.de/v1/chat/completions", { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer YOUR_API_KEY" }, body: JSON.stringify({ model: "vllm/release/gpt-oss-120b", messages: [{role: "user", content: "Erzähl mir eine Geschichte"}], stream: true })});
const reader = response.body.getReader();const decoder = new TextDecoder();let buffer = "";
while (true) { const {done, value} = await reader.read(); if (done) break; buffer += decoder.decode(value, {stream: true});
const lines = buffer.split("\n"); buffer = lines.pop();
for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith("data:")) continue; const payload = trimmed.slice(5).trim(); if (payload === "[DONE]") return; const json = JSON.parse(payload); const delta = json.choices[0]?.delta?.content; if (delta) process.stdout.write(delta); }}Hinweise
Abschnitt betitelt „Hinweise“usageundfinish_reasonerscheinen ausschließlich im letzten Chunk vor[DONE]. Werten Sie diese Felder erst nach Beendigung des Streams aus.- Bei Netzwerkabbrüchen während des Streams muss der Client entscheiden, ob ein erneuter Aufruf gestartet oder der Teilverlauf weiterverwendet wird, ein automatischer Resume-Mechanismus ist nicht Bestandteil des Protokolls.
- Leere
delta.content-Felder können ignoriert werden; sie treten gelegentlich am Anfang (beim Rollen-Token) oder zwischen Segmenten auf.
