I problemi di streaming dipendono raramente dal modello: il problema è l'infrastruttura tra te e il modello. I proxy con timeout di inattività interrompono le connessioni silenziose, i buffer di nginx consumano gli eventi e gli stream consumati solo parzialmente sembrano che "l'API abbia smesso di rispondere". Controlla prima l'infrastruttura.
L’errore
- 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 proxyCause e soluzioni in breve
| Causa | Soluzione |
|---|---|
| Timeout di inattività del proxy/bilanciatore del carico (60s predefiniti in molte configurazioni) | Aumenta i timeout di lettura per il percorso API; lunghe pause durante il ragionamento sembrano inattività per un proxy. |
| Buffering davanti a SSE (nginx proxy_buffering, alcuni CDN) | Disabilita il buffering per la route di streaming (X-Accel-Buffering: no / proxy_buffering off). |
| Il client smette di consumare (await mancante, iteratore eliminato) | Consuma lo stream fino alla fine o chiudilo esplicitamente: un iteratore raccolto dal GC nel mezzo dello stream è indistinguibile da un'interruzione. |
| Aspettarsi l'utilizzo senza richiederlo | OpenAI-wire: passa stream_options: {"include_usage": true} — l'utilizzo arriva nell'ultimo chunk. |
Riproduci con curl -N direttamente sull'API
Bypassa ogni proxy. Se lo SSE grezzo scorre correttamente per tutta la generazione, il problema è nel percorso della tua applicazione — reintroduci i passaggi uno alla volta:
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."}]}'Correggi il passaggio che lo interrompe
nginx: proxy_buffering off + proxy_read_timeout 300s per la route. Serverless: controlla i limiti della piattaforma per lo streaming delle risposte. Proxy aziendali: alcuni non supportano affatto SSE — in quel caso usa il fallback senza streaming.
Gestisci correttamente la parte finale
I dati di fatturazione dello streaming arrivano per ultimi: l'ultimo chunk contiene l'utilizzo (quando richiesto) prima di [DONE]. Aggrega i delta, leggi l'utilizzo dall'ultimo chunk e considera ritentabile una chiusura anticipata (senza finish_reason).
“SSE stream ended without [DONE]” — la risposta era completa?
Quel messaggio è un controllo del tuo client, non un errore inviato dall'API: la connessione si è chiusa prima della riga data: [DONE] con cui termina uno stream compatibile con OpenAI. Le cause sono tre: un passaggio intermedio ha chiuso la connessione (il timeout del proxy e il buffering descritti sopra); il server ha fallito durante la risposta e si è chiuso senza un frame terminale; oppure la risposta era completa e si è perso solo il sentinel. Decide l'ultimo chunk ricevuto: un finish_reason indica che il testo è completo, mentre l'assenza di finish_reason indica che è stato interrotto e va ritentato. Non lasciare questo controllo all'SDK: l'OpenAI Python SDK termina silenziosamente il ciclo quando la connessione si chiude senza [DONE] e genera un errore solo quando un chunk contiene un oggetto di errore. Inserisci il controllo nel tuo codice:
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))Uno stream vuoto: 200, poi nulla
A volte lo stream si apre con HTTP 200 e termina senza un singolo chunk di contenuto: niente testo, nessuna chiamata a uno strumento, talvolta nemmeno il chunk del ruolo. Quasi sempre significa che un upstream ha fallito dopo l'invio degli header: un provider sovraccarico, un gateway il cui upstream ha rifiutato la richiesta o un proxy che ha eliminato il body. Trattalo come uno stream interrotto e ritenta con backoff; registra inoltre il body grezzo di una risposta fallita, perché spesso la causa è un evento di errore interno allo stream che il tuo SDK ha ignorato. Claude Code reagisce alla stessa condizione ritentando la richiesta senza streaming.
Se chiami tramite Kunavo
Kunavo trasmette SSE standard OpenAI-wire (con supporto a stream_options.include_usage) ed eventi Anthropic-wire su /v1/messages, quindi la riproduzione con curl descritta sopra è anche il test di compatibilità. Per i modelli Claude e GPT su /v1/chat/completions, uno stream Kunavo termina con data: [DONE] indipendentemente dal fatto che la risposta sia completa: dopo il chunk con finish_reason quando lo è, e dopo un chunk di errore — type upstream_error, code upstream_disconnect o upstream_timeout — quando l'upstream si interrompe durante la risposta; in questo modo gli SDK OpenAI generano un APIError invece di consegnarti il frammento come risposta completa. Uno stream upstream che termina prima di qualsiasi contenuto, o fallisce prima del primo token, non ti arriva come un 200 vuoto: il tentativo viene ritentato su un altro canale quando disponibile, altrimenti viene restituito come errore HTTP. Una richiesta in streaming che fallisce prima che qualsiasi output ti raggiunga non viene fatturata; una che si interrompe durante la risposta non è garantito che sia gratuita — inviala di nuovo invece di presumere che lo sia.
Domande frequenti
Perché usage è null nelle mie risposte in streaming?
Nelle API OpenAI-wire usage non è incluso negli stream a meno che tu non passi stream_options: {"include_usage": true}; quindi arriva nell'ultimo chunk. Lo stream nativo di Anthropic comunica l'utilizzo negli eventi message_start/message_delta.
Come rilevo uno stream interrotto rispetto a uno completato?
Uno stream completato termina con un finish_reason (o con message_stop di Anthropic) e poi [DONE]. Una connessione che si chiude senza questi indicatori è stata interrotta: trattala come un errore ritentabile, non come una risposta breve.
Che cosa significa “SSE stream ended without [DONE]”?
Il client ha letto lo stream fino alla fine della connessione e non ha mai visto la riga data: [DONE] che chiude uno stream compatibile con OpenAI. L'API non ha inviato quel messaggio: lo ha scritto il tuo client o agente. Se l'ultimo chunk conteneva un finish_reason, la risposta è completa e si è persa solo la chiusura. Se non lo conteneva, la risposta è stata interrotta da un proxy, un timeout o un errore lato server e la richiesta va ritentata.
Che cosa significa “stream ended without finish_reason”?
Il client ha letto lo stream fino alla fine e nessun chunk conteneva un finish_reason: il campo usato da uno stream compatibile con OpenAI per indicare che la risposta è completa (stop, length, tool_calls). Senza di esso, il testo disponibile è un frammento, per quanto naturale sembri l'ultima frase. Ritenta la richiesta; se il problema si ripete, cerca un timeout del proxy o buffering tra te e l'API.
Perché un'API LLM dovrebbe restituire uno stream vuoto?
Perché il problema si è verificato dopo l'invio degli header HTTP: il provider era sovraccarico, l'upstream di un gateway ha rifiutato la richiesta oppure un proxy ha eliminato il body. La riga di stato indica comunque 200, quindi i controlli dello stato non lo rilevano. Tratta uno stream senza chunk di contenuto come una richiesta fallita, ritenta con backoff e registra una risposta grezza per individuare l'evento di errore al suo interno.
È sicuro ignorare un [DONE] mancante se ho già del testo?
Solo quando l'ultimo chunk conteneva un finish_reason. Senza di esso, il testo disponibile è un frammento che può terminare a metà frase o a metà chiamata a uno strumento, con gli argomenti JSON incompleti. Ritenta invece di salvarlo come risposta.
Guide correlate
- Guida alle API compatibili con OpenAI — dall'endpoint locale di Ollama ai modelli frontier ospitati
- “Streaming interrupted. Waiting for the complete message” — significato e soluzione
- Claude Code “Risposta bloccata a metà dello streaming” e “La risposta in streaming è terminata prima che venissero ricevuti dati completi” — cosa interrompe lo stream
Ulteriori dettagli sul significato degli errori sono disponibili in riferimento agli errori; ottenere una chiave richiede un minuto tramite registrazione e la guida all’autenticazione.