Streaming-Fehler liegen selten am Modell — sie entstehen in der Infrastruktur zwischen dir und dem Modell. Proxys mit Inaktivitäts-Timeouts beenden stille Verbindungen, nginx-Puffer verschlucken Events, und halb konsumierte Streams sehen aus wie „Die API antwortet nicht mehr“. Prüfe zuerst die Infrastruktur.
Der Fehler
- Stream stops mid-sentence, connection closed (no error event)
- Client hangs after the last token, never sees [DONE]
- usage is null on streamed responses
- Works in curl, dies behind nginx / a corporate proxyUrsachen und Lösungen im Überblick
| Ursache | Lösung |
|---|---|
| Inaktivitäts-Timeout des Proxys/Load-Balancers (in vielen Setups standardmäßig 60 s) | Erhöhe die Lese-Timeouts für den API-Pfad; lange Denkpausen wirken auf einen Proxy wie Inaktivität. |
| Pufferung vor SSE (nginx proxy_buffering, einige CDNs) | Deaktiviere die Pufferung für die Streaming-Route (X-Accel-Buffering: no / proxy_buffering off). |
| Der Client konsumiert nicht weiter (await fehlt, Iterator verworfen) | Konsumiere bis zum Ende oder schließe ausdrücklich — ein mitten im Stream durch GC freigegebener Iterator ist nicht von einem Abbruch zu unterscheiden. |
| Nutzung wird erwartet, ohne sie anzufordern | OpenAI-Wire: Übergib stream_options: {"include_usage": true} — die Nutzungsdaten kommen im letzten Chunk. |
Mit curl -N direkt an der API reproduzieren
Umgehe jeden Proxy. Wenn der rohe SSE-Stream über die gesamte Generierung problemlos läuft, liegt das Problem im Pfad deiner Anwendung — füge die Zwischenstationen nacheinander wieder hinzu:
curl -N https://api.kunavo.com/v1/chat/completions \
-H "Authorization: Bearer $KUNAVO_API_KEY" \
-H "content-type: application/json" \
-d '{"model":"claude-sonnet-5","stream":true,
"stream_options":{"include_usage":true},
"max_tokens":300,
"messages":[{"role":"user","content":"Count slowly to 20 in words."}]}'Behebe die Zwischenstation, an der es bricht
nginx: proxy_buffering off + proxy_read_timeout 300s für die Route. Serverless: Prüfe die Antwort-Streaming-Limits der Plattform. Unternehmens-Proxys: Manche können SSE überhaupt nicht verarbeiten — wechsle dort auf Nicht-Streaming.
Das Ende korrekt verarbeiten
Gestreamte Abrechnungsdaten kommen zuletzt: Der letzte Chunk enthält die Nutzung (wenn angefordert) vor [DONE]. Aggregiere Deltas, lies die Nutzungsdaten aus dem letzten Chunk und behandle ein vorzeitiges Schließen (kein finish_reason) als wiederholbaren Fehler.
„SSE-Stream ohne [DONE] beendet“ — war die Antwort vollständig?
Diese Meldung stammt aus der Prüfung deines Clients, nicht aus einer Fehlermeldung der API: Die Verbindung wurde geschlossen, bevor die data: [DONE]-Zeile erreicht wurde, mit der ein OpenAI-kompatibler Stream endet. Drei Dinge können das verursachen: Eine Zwischenstation hat die Verbindung geschlossen (die oben genannten Proxy-Timeouts und Pufferung); der Server ist mitten in der Antwort fehlgeschlagen und hat ohne abschließenden Frame geschlossen; oder die Antwort war vollständig und nur das Signal ging verloren. Der letzte empfangene Chunk entscheidet: Ein finish_reason darin bedeutet, dass der Text vollständig ist; ohne finish_reason wurde er abgeschnitten und sollte erneut angefordert werden. Überlasse diese Prüfung nicht dem SDK — das OpenAI Python SDK beendet seine Schleife still, wenn die Verbindung ohne [DONE] geschlossen wird, und löst nur dann einen Fehler aus, wenn ein Chunk ein Fehlerobjekt enthält. Führe die Prüfung in deinem eigenen Code durch:
from openai import OpenAI
client = OpenAI(base_url="https://api.kunavo.com/v1", api_key="sk-kn-...")
finish_reason, parts = None, []
stream = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Count slowly to 20 in words."}],
stream=True,
)
for chunk in stream: # raises openai.APIError on a chunk that carries "error"
for choice in chunk.choices:
parts.append(choice.delta.content or "")
finish_reason = choice.finish_reason or finish_reason
if finish_reason is None:
raise RuntimeError("stream closed without a finish_reason: cut off, retry it")
print("".join(parts))Ein leerer Stream: 200, danach nichts
Manchmal öffnet sich der Stream mit HTTP 200 und endet ohne einen einzigen Content-Chunk — kein Text, kein Tool-Aufruf, manchmal nicht einmal der Role-Chunk. Fast immer ist dann der Upstream fehlgeschlagen, nachdem die Header bereits gesendet wurden: ein überlasteter Anbieter, ein Gateway, dessen eigener Upstream die Anfrage abgelehnt hat, oder ein Proxy, der den Body verworfen hat. Behandle das wie einen abgebrochenen Stream und wiederhole die Anfrage mit Backoff. Protokolliere außerdem den rohen Body einer fehlerhaften Antwort, weil die Ursache oft ein Fehler-Event innerhalb des Streams ist, das dein SDK übersprungen hat. Claude Code reagiert auf dieselbe Bedingung, indem es die Anfrage ohne Streaming wiederholt.
Wenn Sie Kunavo verwenden
Kunavo streamt standardmäßiges OpenAI-Wire-SSE (mit Unterstützung für stream_options.include_usage) sowie Anthropic-Wire-Events auf /v1/messages; daher ist die obige curl-Reproduktion zugleich der Kompatibilitätstest. Für die Claude- und GPT-Modelle auf /v1/chat/completions endet ein Kunavo-Stream mit data: [DONE], unabhängig davon, ob die Antwort beendet wurde: nach dem finish_reason-Chunk, wenn sie beendet wurde, und nach einem Fehler-Chunk — type upstream_error, code upstream_disconnect oder upstream_timeout — wenn der Upstream mitten in der Antwort abgebrochen ist. Dadurch lösen die OpenAI-SDKs einen APIError aus, statt dir das Fragment als vollständige Antwort zu übergeben. Ein Upstream-Stream, der vor jeglichem Inhalt endet oder vor dem ersten Token fehlschlägt, erreicht dich niemals als leerer 200-Response: Der Versuch wird über einen anderen Kanal wiederholt, sofern das Modell dort verfügbar ist, andernfalls als HTTP-Fehler zurückgegeben. Eine gestreamte Anfrage, die vor jeder Ausgabe fehlschlägt, wird nicht berechnet; bei einer Unterbrechung mitten in der Antwort ist das nicht garantiert — sende sie erneut, statt anzunehmen, sie sei kostenlos.
Häufig gestellte Fragen
Warum ist usage in meinen gestreamten Antworten null?
Bei OpenAI-Wire-APIs wird usage nur dann in Streams aufgenommen, wenn du stream_options: {"include_usage": true} übergibst; die Daten kommen anschließend im letzten Chunk. Der native Stream von Anthropic meldet die Nutzung in message_start/message_delta-Events.
Wie erkenne ich, ob ein Stream abgebrochen wurde oder beendet war?
Ein beendeter Stream endet mit einem finish_reason (oder Anthropic message_stop) und anschließend [DONE]. Eine Verbindung, die ohne diese Marker geschlossen wird, wurde abgebrochen — behandle das als wiederholbaren Fehler, nicht als kurze Antwort.
Was bedeutet „SSE-Stream ohne [DONE] beendet“?
Dein Client hat den Stream bis zum Ende der Verbindung gelesen und nie die data: [DONE]-Zeile gesehen, die einen OpenAI-kompatiblen Stream schließt. Die API hat diese Meldung nicht gesendet; dein Client oder Agent hat sie formuliert. Wenn der letzte Chunk einen finish_reason enthielt, ist die Antwort vollständig und nur der Abschluss ging verloren. Wenn nicht, wurde die Antwort durch einen Proxy, ein Timeout oder einen serverseitigen Fehler abgeschnitten, und die Anfrage sollte wiederholt werden.
Was bedeutet „Stream ohne finish_reason beendet“?
Der Client hat den Stream bis zum Ende gelesen, und kein Chunk enthielt einen finish_reason — das Feld, mit dem ein OpenAI-kompatibler Stream angibt, dass die Antwort vollständig ist (stop, length, tool_calls). Ohne dieses Feld ist der vorhandene Text ein Fragment, wie natürlich der letzte Satz auch wirken mag. Wiederhole die Anfrage; tritt es erneut auf, suche nach einem Proxy-Timeout oder Pufferung zwischen dir und der API.
Warum würde eine LLM-API einen leeren Stream zurückgeben?
Weil der Fehler auftrat, nachdem die HTTP-Header gesendet worden waren: Der Anbieter war überlastet, der Upstream eines Gateways lehnte die Anfrage ab oder ein Proxy verwarf den Body. Die Statuszeile lautet weiterhin 200, daher erkennen reine Statusprüfungen das nicht. Behandle einen Stream ohne Content-Chunk als fehlgeschlagene Anfrage, wiederhole ihn mit Backoff und protokolliere eine rohe Antwort, um das darin enthaltene Fehler-Event zu finden.
Ist es sicher, ein fehlendes [DONE] zu ignorieren, wenn ich bereits Text habe?
Nur wenn der letzte Chunk einen finish_reason enthielt. Ohne ihn ist der vorhandene Text ein Fragment, das mitten im Satz oder mitten in einem Tool-Aufruf enden kann, wobei seine JSON-Argumente unvollständig sind. Wiederhole die Anfrage, statt den Text als Antwort zu speichern.
Verwandte Anleitungen
- OpenAI-kompatibler API-Leitfaden — von Ollamas lokalem Endpoint bis zu gehosteten Frontier-Modellen
- „Streaming unterbrochen. Warten auf die vollständige Nachricht“ — Bedeutung und Lösung
- Claude Code „Antwort während des Streams ins Stocken geraten“ und „Streaming-Antwort beendet, bevor vollständige Daten empfangen wurden“ — was den Stream beendet
Weitere Informationen zur Fehlersemantik finden Sie unter Fehlerreferenz; einen Schlüssel erhalten Sie in einer Minute über Registrierung und die Authentifizierungsanleitung.