- Wichtige Erkenntnisse
- Diagnose-Checkliste: Finde zuerst die Ursache
- Ursache 1: Provider-Fehler mitten im Stream (Überlastung, Content Filter, Provider-Absturz)
- Ursache 2: Read-Timeout oder Leerlauf-Stillstand des Streams
- Ursache 3: Rate-Limit (429) mitten im Stream
- Ursache 4: Fehlerhafter oder unerwarteter Chunk, der deinen Parser zerstört
- Ursache 5: Abgebrochene Netzwerkverbindung
- Ursache 6: Authentifizierungs- oder Kontingentfehler, der mitten im Stream auftaucht
- Retry- und Backoff-Muster, das alle sechs Ursachen abdeckt
- Fazit
- FAQ
- Empfohlene Artikel
Eine Meldung wie error: llm api error: an error occurred during streaming hat fast immer eine von sechs Ursachen: einen Provider-Fehler mitten im Stream, der als SSE-Ereignis gesendet wird, nachdem die Verbindung bereits 200 zurückgegeben hat; einen Read-Timeout auf Client- oder Gateway-Seite; ein 429-Rate-Limit, das mitten in der Generierung erreicht wird; einen fehlerhaften oder unerwarteten Chunk, der deinen SSE-Parser zum Absturz bringt; eine abgebrochene Netzwerkverbindung; oder ein Authentifizierungs-/Kontingentproblem, das erst sichtbar wird, sobald der Stream geöffnet ist. Prüfe sie in dieser Reihenfolge – die meisten Streaming-Fehler fallen in die ersten drei.
Ordne das, was du siehst, einer Ursache zu, bevor du weiterliest:
| Was du beobachtest | Weiter zu |
|---|---|
| Stream startet, einige Tokens kommen an, dann bricht er ohne HTTP-Statusänderung ab | Ursache 1 |
| Stream stockt 10s+ ohne neue Tokens, dann meldet dein Client einen Timeout | Ursache 2 |
| Fehler tritt am häufigsten bei Traffic-Spitzen oder direkt nach einem Request-Burst auf | Ursache 3 |
Exception nennt KeyError, JSONDecodeError oder einen Parsing-Fehler, keinen Netzwerkbegriff |
Ursache 4 |
Fehler ist allgemein (APIConnectionError, ECONNRESET) und tritt unregelmäßig auf, auch bei stabilen Netzwerken |
Ursache 5 |
| Fehler tritt beim allerersten Request nach einem Schlüsselwechsel, dem Erreichen eines Ausgabenlimits oder einem Umgebungswechsel auf | Ursache 6 |
Die vollständige Diagnose-Checkliste unten wiederholt dies mit der wahrscheinlichen Ursache pro Zeile.
Wichtige Erkenntnisse
- Ein Streaming-Fehler kann als SSE-
error-Ereignis nach dem bereits zurückgegebenen HTTP-Status 200 auftreten – eine Retry-Logik, die nur auf den Statuscode schaut, übersieht ihn. - Die Messages API von Anthropic meldet Überlastungen mitten im Stream als
event: errormit"type": "overloaded_error"im Stream-Body, nicht als neuen HTTP-529. - OpenRouter dokumentiert dasselbe Muster über alle Provider hinweg: Sobald das erste Token ausgeliefert wurde, kommt ein Fehler als
chat.completion.chunkmit einem Top-Level-Felderrorundfinish_reason: "error". - Client-Bibliotheken, die annehmen, dass jeder gestreamte Chunk wie ein Erfolg aussieht, stürzen mit einem irreführenden Fehler ab (einer
APIConnectionErrorstatt der eigentlichen Ursache), wenn ein Provider mitten im Stream eine strukturierte Fehler-Payload sendet. - Die meisten Lösungen sind unabhängig vom Provider dieselben: Den Stream-Body nach Fehlerereignissen durchsuchen, einen separaten Token-Timeout für Stockungen abseits des Verbindungs-Timeouts setzen und exponentielles Backoff verwenden, das auf den Fehlertyp abgestimmt ist, nicht nur auf den HTTP-Status.
Diagnose-Checkliste: Finde zuerst die Ursache
Gehe diese Liste durch, bevor du Code änderst. Jede Zeile ordnet einem beobachtbaren Symptom den Abschnitt zu, der es behebt.
| Was du beobachtest | Wahrscheinliche Ursache | Weiter zu |
|---|---|---|
| Stream startet, einige Tokens kommen an, dann bricht er ohne HTTP-Statusänderung ab | Provider-Fehler mitten im Stream (Überlastung, Content Filter, Provider-Absturz) | Ursache 1 |
| Stream stockt 10s+ ohne neue Tokens, dann meldet dein Client einen Timeout | Read-Timeout oder Leerlauf-Stillstand | Ursache 2 |
| Fehler tritt am häufigsten bei Traffic-Spitzen oder direkt nach einem Request-Burst auf | Rate-Limit mitten im Stream erreicht | Ursache 3 |
Exception nennt KeyError, JSONDecodeError oder einen Parsing-Fehler, keinen Netzwerkbegriff |
Fehlerhafter oder unerwarteter Chunk | Ursache 4 |
Fehler ist allgemein (APIConnectionError, ECONNRESET) und tritt unregelmäßig auf, auch bei stabilen Netzwerken |
Abgebrochene Verbindung oder maskierter Provider-Fehler | Ursache 5 und Ursache 4 |
| Fehler tritt beim allerersten Request nach einem Schlüsselwechsel, dem Erreichen eines Ausgabenlimits oder einem Umgebungswechsel auf | Authentifizierungs- oder Kontingentfehler | Ursache 6 |
Wenn deine Logs nur einen allgemeinen Exception-Namen und keine Upstream-Meldung zeigen, ist das selbst ein Symptom – siehe Ursache 4 und Ursache 5, warum generische Wrapper die eigentliche Ursache verbergen.
Ursache 1: Provider-Fehler mitten im Stream (Überlastung, Content Filter, Provider-Absturz)
Das ist die Ursache, die die meisten Menschen verwirrt, weil der Request erfolgreich aussah. Sobald eine Streaming-Antwort beginnt, sind der HTTP-Statuscode und die Header bereits an den Client übertragen. Wenn der Provider dann einen Fehler erleidet – Kapazitätserschöpfung, ein interner Fehler, ein nach partieller Ausgabe ausgelöster Content Filter oder ein Absturz des Modellprozesses – kann er den HTTP-Status nicht mehr in einen Fehlercode ändern. Der Fehler muss als spezielles Ereignis innerhalb des Streams selbst transportiert werden.
Die Messages API von Anthropic dokumentiert das direkt: Die API „kann gelegentlich [Fehler] im Ereignisstrom senden" und liefert genau dieses Beispiel für eine Überlastung mitten im Stream:
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}
Das ist eine echte Lücke in vieler Retry-Logik. Wenn dein Code nur response.status_code prüft, hat er bereits 200 gesehen, bevor der Fehler auftrat, und ein Retry wird nie ausgelöst. Ein Incident-Bericht eines Drittanbieters, der genau dieses Muster beschreibt, formuliert es deutlich: „Retry-Logik, die auf den HTTP-Statuscode setzt, greift nie, weil der Status bereits 200 war. Das Ergebnis ist eine stillschweigend abgeschnittene Antwort“ – und empfiehlt, den Stream-Body nach Fehlerereignissen zu durchsuchen, statt sich allein auf den Statuscode zu verlassen.
Die OpenRouter-Dokumentation beschreibt dasselbe strukturelle Problem über die Provider hinweg, an die es weiterleitet, nicht nur bei einem Anbieter. Sobald das erste Token geschrieben ist, „sind der HTTP-200 OK-Status und die Header bereits gesetzt – sie können nicht mehr geändert werden“, ein Provider-Fehler „muss also als SSE-Ereignis im Stream ankommen“. Die dokumentierten Ursachen für diese Fehlerklasse:
- Provider-Verbindungsabbruch – die Upstream-Verbindung bricht nach partieller Ausgabe ab (Netzwerkproblem, Provider-Absturz, Load-Balancer-Timeout)
- Provider-Timeout – das Modell antwortet mitten in der Generierung nicht mehr und die Read-Frist läuft ab
- Token-Limit während der Generierung erreicht – das Modell erreicht
max_tokensoder der Kontextfenster füllt sich während der Ausgabe - Output-Content-Filter – ein Moderationssystem markiert generierten Text, nachdem bereits ein Teil gestreamt wurde
- Provider-Überlastung – der Upstream liefert nach Beginn des Streamings einen Rate-Limit- oder Kapazitätsfehler
OpenRouters Mid-Stream-Fehler-Payload überträgt den Fehler in einem normal aussehenden Chunk, mit einem finish_reason, der dir sagt, dass der Stream ungewöhnlich endete:
{"id":"gen-abc123","object":"chat.completion.chunk","created":1234567890,"model":"openai/gpt-4o","provider":"OpenAI","error":{"code":429,"message":"Rate limit exceeded","metadata":{"error_type":"rate_limit_exceeded"}},"choices":[{"index":0,"delta":{"content":""},"finish_reason":"error"}]}
So bestätigst du, dass dies deine Ursache ist: Protokolliere den rohen SSE-Stream (nicht nur die geparste Textausgabe) für einen fehlgeschlagenen Request. Wenn du irgendwo im Stream vor dem Abschlussereignis ein error-Ereignis oder einen Chunk mit einem Top-Level-Feld error siehst, ist es diese Ursache.
Lösung: Behandle Payloads mit finish_reason: "error" und event: error als wiederholbare Fehler, nicht als erfolgreiche Abschlüsse mit leerem Inhalt. Wiederhole mit Backoff basierend auf dem spezifischen Fehlertyp in der Payload, nicht auf dem HTTP-Status, da der HTTP-Status bereits 200 lautet.
Ursache 2: Read-Timeout oder Leerlauf-Stillstand des Streams
Ein Stillstand unterscheidet sich von einem harten Fehler: Die Verbindung bleibt offen, aber über einen längeren Zeitraum kommen keine neuen Tokens an. Die meisten HTTP-Clients vermischen zwei Timeout-Konzepte: ein Gesamt-Request-Timeout und ein Pro-Read-Timeout (Leerlauf-Timeout). Ein langer Stream kann bei einer großen Antwort legitim ein kurzes Gesamt-Timeout überschreiten, während ein wirklich festhängender Stream problemlos innerhalb eines Gesamt-Timeout-Fensters bleiben kann, wenn keine Leerlauf-Read-Prüfung existiert.
Das OpenAI Python SDK setzt standardmäßig ein Gesamt-Request-Timeout von 10 Minuten: „Standardmäßig laufen Requests nach 10 Minuten ab. Du kannst dies mit einer timeout-Option konfigurieren, die einen Float oder ein httpx.Timeout-Objekt akzeptiert… Bei Timeout wird eine APITimeoutError ausgelöst.“ Außerdem werden bestimmte Fehler automatisch wiederholt: „Verbindungsfehler (zum Beispiel aufgrund eines Netzwerkverbindungsproblems), 408 Request Timeout, 409 Conflict, 429 Rate Limit und >=500 Internal Errors werden standardmäßig wiederholt“, zweimal, konfigurierbar über max_retries.
Bestätige es: Miss den Abstand zwischen dem zuletzt empfangenen Token und dem Fehler. Eine feste Dauer, die deinem konfigurierten Timeout-Wert entspricht, deutet auf einen Timeout hin, nicht auf einen Provider-seitigen Fehler.
Lösung: Setze zwei Timeouts, nicht einen – ein Verbindungs-/Gesamt-Timeout für den Request-Lebenszyklus und ein separates Leerlauf-Read-Timeout, das auslöst, wenn innerhalb von N Sekunden kein Chunk ankommt. Damit kannst du unterscheiden zwischen „das Modell ist langsam“ und „der Stream ist gestorben“. 15–30 Sekunden sind ein angemessenes Leerlauf-Read-Timeout für Chat-Completions; erhöhe es, wenn dein Modell lange stille „Denk“-Phasen vor dem ersten Token hat.
Ursache 3: Rate-Limit (429) mitten im Stream
Rate-Limits lehnen einen Request normalerweise ab, bevor er beginnt. Aber einige Gateways wenden Limits pro Token oder pro Fenster an, sodass ein Request mit dem Streaming beginnen und dann abgeschnitten werden kann, sobald er mitten in der Generierung ein Budget überschreitet – dieselbe In-Band-Fehlerform wie Ursache 1, nur dass ein 429-Code im Stream ankommt, statt als anfänglicher Antwortstatus.
Bestätige es: Prüfe, ob Fehler sich bei Traffic-Bursts oder an einer konsistenten Requests-pro-Minute-Grenze häufen. Ein Retry-After-Header oder ein entsprechendes Feld in der Fehler-Payload bestätigt es.
Lösung: Respektiere Retry-After, wenn vorhanden, reduziere die Parallelität vor dem Wiederholen und warte exponentiell. Häufige Treffer sind ein Signal, die Anzahl paralleler Streams zu senken oder den Kontotyp zu erweitern, nicht aggressiver zu wiederholen.
Ursache 4: Fehlerhafter oder unerwarteter Chunk, der deinen Parser zerstört
Nicht jeder „Streaming-Fehler“ ist die Schuld des Modells. Manche sind vollständig clientseitig: Dein SSE-Parser nimmt an, dass jeder Chunk eine feste Form hat, und eine anders geformte Payload – einschließlich einer legitimen Fehler-Payload vom Provider – bricht diese Annahme und wirft eine unzusammenhängend wirkende Exception.
Ein dokumentierter Fall: Der Streaming-Handler ollama_chat von LiteLLM erwartete, dass jeder Chunk ein message-Feld enthält. Als Ollama stattdessen einen strukturierten Fehler wie {"error": "error parsing tool call: ..."} zurückgab, führte der Handler einen ungeschützten Zugriff chunk["message"] aus, was KeyError: 'message' auslöste. Ein breiterer Exception-Handler fing es ab und gab es als APIConnectionError erneut aus – ein nach Netzwerk klingender Fehler für das, was tatsächlich ein JSON-Parsing-Fehler in der Tool-Call-Ausgabe des Modells war. Der Bug-Report beziffert die Kosten ausdrücklich: Entwickler, die es debuggten, „protokollierten reason=timeout / LLM request timed out für das, was tatsächlich fehlerhaftes Tool-Call-JSON war“, was „zwei Tage Fehldiagnose“ kostete, während sie Netzwerk- und Infrastrukturursachen jagten, die nicht das Problem waren.
Das allgemeine Muster: Ein Provider sendet einen Fehler in einer Form, die dein Parsing-Code nicht erwartet, eine Low-Level-Exception feuert (KeyError, TypeError, ein JSON-Decode-Fehler), und ein Catch-all-except-Block verpackt sie in einen generischen Verbindungs- oder Streaming-Fehler, der die eigentliche Ursache auslöscht.
Bestätige es: Protokolliere den rohen Chunk, bevor dein Exception-Wrapping-Code läuft. Wenn die rohe Payload ein error-Feld mit einer echten Meldung hat, hat dein Client auf dem Weg zu dem generischen Fehler, den du siehst, nützliche Informationen verworfen.
Lösung: Prüfe vor dem Zugriff auf erwartete Felder wie message oder delta auf einen error-Schlüssel und verzweige explizit darauf. Erhalte die Upstream-Fehlermeldung und den Statuscode beim erneuten Auslösen, statt jeden Fehler in einen generischen Exception-Typ zu kollabieren.
Ursache 5: Abgebrochene Netzwerkverbindung
Manchmal ist die Ursache wirklich das Netzwerk: Ein Proxy oder Load Balancer schließt eine langlebige Verbindung nach einer Leerlaufzeit, oder das Netzwerk eines Clients wechselt mitten im Request. Das ähnelt Ursache 2, aber die Lösung unterscheidet sich – die Verbindung selbst ist weg, nicht nur untätig innerhalb deines eigenen Timeout-Fensters.
Bestätige es: Achte auf Verbindungs-Reset-Fehler (ECONNRESET, Broken pipe, Connection reset by peer) statt auf eine Application-Level-Timeout-Exception, und prüfe, ob die Fehler mit dem Leerlauf-Timeout eines bekannten Vermittlers korrelieren (viele Load Balancer standardisieren auf 60 Sekunden Inaktivität).
Lösung: Wenn du den Proxy oder Load Balancer kontrollierst, erhöhe dessen Leerlauf-Timeout über deine erwartete maximale Stream-Dauer oder füge Keep-Alive-Pings hinzu. Wenn der Abbruch außerhalb deiner Kontrolle liegt, behandle ihn als wiederholbar – die meisten Streaming-APIs unterstützen kein Fortsetzen eines partiellen Streams, ein Retry bedeutet also, die Generierung neu zu starten.
Ursache 6: Authentifizierungs- oder Kontingentfehler, der mitten im Stream auftaucht
Einige Gateways validieren Authentifizierung und Abrechnung träge – die Verbindung öffnet sich, dann findet die Authentifizierungs- oder Kontostandsprüfung während der Generierung des ersten Chunks statt, und ein Fehler dort wird als Streaming-Fehler gemeldet, statt als sauberer 401/402 zum Request-Zeitpunkt.
Bestätige es: Prüfe, ob der Fehler bei jedem Request mit einem bestimmten Schlüssel auftritt, statt nur gelegentlich, und ob er direkt nach einem Schlüsselwechsel, einem Abrechnungsereignis oder einem Umgebungswechsel begann (Dev-Schlüssel gegen eine Produktions-Basis-URL oder umgekehrt).
Lösung: Überprüfe das Schlüsselformat (Authorization: Bearer <key>, nicht den rohen Schlüssel), bestätige, dass der Schlüssel aktiv ist, und prüfe den Kontostand getrennt vom Request-Level-Debugging. Wenn jeder Request unabhängig von Prompt oder Modell identisch fehlschlägt, deutet das auf diese Ursache hin, nicht auf die vorherigen fünf.
Retry- und Backoff-Muster, das alle sechs Ursachen abdeckt
Eine einzige Retry-Strategie kann Ursachen 1 bis 5 abdecken, wenn sie den Stream-Body prüft, nicht nur den HTTP-Status:
import time
import openai
def stream_with_recovery(client, **kwargs):
max_attempts = 3
for attempt in range(max_attempts):
try:
collected = ""
stream = client.chat.completions.create(stream=True, **kwargs)
for chunk in stream:
choice = chunk.choices[0] if chunk.choices else None
if choice and getattr(choice, "finish_reason", None) == "error":
raise RuntimeError(f"mid-stream error: {chunk}")
if choice and choice.delta.content:
collected += choice.delta.content
return collected
except (openai.APITimeoutError, openai.APIConnectionError, openai.RateLimitError) as e:
if attempt == max_attempts - 1:
raise
time.sleep(2 ** attempt)
return collected
Dieses Beispiel verwendet die OpenAI-kompatible Client-Form, die auch mit Novita AIs OpenAI-kompatiblem Chat-Completions-Endpunkt funktioniert, indem base_url="https://api.novita.ai/openai" gesetzt wird. Drei Dinge sind über den Code hinaus wichtig:
- Prüfe den Chunk-Inhalt auf einen In-Band-Fehler, bevor du von einem normalen Abschluss ausgehst, gemäß Ursache 1.
- Verwende exponentielles Backoff (
2 ** attempt, gedeckelt) statt sofortiger Wiederholungen, insbesondere bei Rate-Limits und Überlastungsfehlern. - Protokolliere die rohe Upstream-Fehlermeldung, bevor du sie in deinen eigenen Exception-Typ verpackst, damit zukünftiges Debugging nicht die zweitägige Fehldiagnose aus Ursache 4 wiederholt.
Wenn ein bestimmter Provider oder ein bestimmtes Modell häufiger ausfällt als andere, ist es eine sinnvolle Absicherung, diese Request-Klasse auf ein anderes Modell umzuleiten, bevor man sie braucht – siehe Betrieb eines Multi-Provider-LLM-Dienstes mit definiertem Uptime-Ziel dazu, wie du diese Fallback-Richtlinie definierst, statt sie während eines Incidents zu improvisieren.
Fazit
Die Diagnose dieses Fehlers ist ein Prozess der Eliminierung, keine Ratespielerei: Verwende die Checkliste oben, um dein Symptom einer von sechs Ursachen zuzuordnen, und bestätige sie dann mit dem spezifischen Signal, das jeder Abschnitt nennt – ein In-Band-error-Ereignis, eine Stillstands-Dauer, die deinem Timeout entspricht, eine mit Bursts korrelierte 429, eine rohe Payload, an der dein Parser gescheitert ist, eine Connection-Reset-Meldung oder ein Fehler, der bei jedem Request mit einem Schlüssel wiederkehrt. Die Ursachen 1 bis 3 sind die häufigsten, und die Ursachen 1, 3 und 5 teilen sich dieselbe zugrunde liegende Lösung: Höre auf, dem HTTP-Statuscode zu vertrauen, sobald ein Stream begonnen hat, und wiederhole stattdessen mit Backoff basierend auf dem, was der Stream selbst meldet.
Wenn du die Retry-Logik einmal mit dem obigen Muster reparierst, schließt du die ersten fünf Ursachen gleichzeitig. Ursache 6 ist die Ausnahme – kein noch so häufiges Wiederholen behebt einen ungültigen Schlüssel oder ein leeres Guthaben. Behandle also identische Fehler bei jedem Request als Konfigurationsprüfung, nicht als Netzwerkproblem.
FAQ
Warum funktioniert mein LLM-API-Request manchmal und schlägt zu anderen Zeiten mit einem Streaming-Fehler fehl?
Gelegentliche Fehler deuten auf eine Ursache mitten im Stream hin, nicht auf ein Konfigurationsproblem – Konfigurationsfehler wie ein falscher Schlüssel oder falscher Endpoint schlagen jedes Mal fehl. Prüfe zuerst Provider-Überlastung und Rate-Limits, da beide vom Traffic abhängen.
Ist ein Streaming-Fehler dasselbe wie ein Timeout?
Nicht immer. Ein Timeout bedeutet, dass innerhalb deines konfigurierten Fensters keine Antwort eingetroffen ist. Ein Mid-Stream-Fehler bedeutet, dass eine Antwort begonnen hat, einige Tokens angekommen sind und dann ein Fehlerereignis im Stream gesendet wurde. Die Fehlerbehandlung sollte beides unterscheiden, da die Lösungen unterschiedlich sind.
Warum sagt meine Fehlermeldung „Verbindungsfehler“, obwohl das eigentliche Problem ein anderes war?
Viele Client-Bibliotheken verpacken unerwartete Exceptions in einen generischen Verbindungsfehler-Typ, wenn ein Chunk nicht der Form entspricht, die der Parser erwartet – siehe Ursache 4 für einen dokumentierten Fall, in dem ein JSON-Parsing-Fehler zwei Tage lang als Verbindungs-Timeout gemeldet wurde, bevor die eigentliche Ursache gefunden wurde.
Kann ich einen Stream nach einem Mid-Stream-Fehler fortsetzen, statt neu zu starten?
Die meisten OpenAI-kompatiblen und Anthropic-ähnlichen Streaming-APIs unterstützen kein Fortsetzen eines partiellen Streams an der Fehlerstelle. Wiederhole den vollständigen Request und verwerfe die partielle Ausgabe, statt sie anzuhängen, um doppelte Inhalte zu vermeiden.
Sollte ich für LLM-API-Aufrufe immer Streaming verwenden?
Streaming vermeidet, dass ein einzelner großer Request in einen Timeout läuft, führt aber den Modus des partiellen Ausgabefehlers ein, der in diesem Leitfaden behandelt wird. Bei kurzen Antworten, bei denen du keine partielle Ausgabe zeigen musst, ist ein Non-Streaming-Request einfacher in der Fehlerbehandlung.
Was bedeutet finish_reason: error in einer gestreamten Antwort?
Es ist ein Abschlusssignal, das einige Gateways an den letzten Chunk eines Streams anhängen, der mittendrin fehlschlug, abgegrenzt von normalen Werten wie stop oder length. Behandle es als fehlgeschlagene Generierung, auch wenn der HTTP-Status des Requests 200 war.
Empfohlene Artikel
- Bester Multi-Provider-LLM-Dienst für geringere Kosten und höhere Uptime? – SLO-Design, Provider-Health-Monitoring und Incident-Playbooks für Teams, die LLM-Traffic über mehrere Provider betreiben.
- OpenAI Python SDK: Installation, Setup und praktische Integration – Client-Konfiguration, Streaming, Retries und Timeout-Optionen für das SDK, das im obigen Codebeispiel verwendet wird.
- Was ist Rate Limiting? Ein praktischer Leitfaden für KI-Dienste – Wie Rate-Limits durchgesetzt werden und wie du Retry-Verhalten darauf auslegst.
