Falhas de streaming raramente são culpa do modelo — são da infraestrutura entre você e ele. Proxies com timeout de inatividade encerram conexões silenciosas, o buffering do nginx engole eventos e streams parcialmente consumidos parecem que “a API parou de responder”. Verifique primeiro a infraestrutura.
O erro
- 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 proxyCausas e soluções em resumo
| Causa | Solução |
|---|---|
| Timeout de inatividade do proxy/balanceador de carga (60s por padrão em muitas configurações) | Aumente os timeouts de leitura para o caminho da API; pausas longas durante o raciocínio parecem inatividade para um proxy. |
| Buffering na frente do SSE (nginx proxy_buffering, algumas CDNs) | Desabilite o buffering para a rota de streaming (X-Accel-Buffering: no / proxy_buffering off). |
| O cliente para de consumir (await ausente, iterador descartado) | Consuma até o fim ou feche explicitamente — um iterador coletado pelo GC no meio do stream é indistinguível de um corte. |
| Esperar uso sem solicitá-lo | OpenAI-wire: passe stream_options: {"include_usage": true} — o uso chega no chunk final. |
Reproduza com curl -N diretamente na API
Ignore todos os proxies. Se o SSE bruto fluir corretamente durante toda a geração, o problema está no caminho do seu aplicativo — reintroduza os saltos um de cada vez:
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."}]}'Corrija o salto que o interrompe
nginx: proxy_buffering off + proxy_read_timeout 300s para a rota. Serverless: verifique os limites de streaming de respostas da plataforma. Proxies corporativos: alguns não conseguem lidar com SSE — nesses casos, use o modo sem streaming.
Trate o final corretamente
Os dados de cobrança em streaming chegam por último: o chunk final traz o uso (quando solicitado) antes de [DONE]. Agregue os deltas, leia o uso do último chunk e considere um fechamento antecipado (sem finish_reason) passível de nova tentativa.
“O fluxo SSE terminou sem [DONE]” — a resposta estava completa?
Essa mensagem é uma verificação do seu cliente, não um erro enviado pela API: a conexão foi fechada antes da linha data: [DONE] com a qual um fluxo compatível com a OpenAI termina. Três coisas podem causá-la: um intermediário fechou a conexão (pelas causas de timeout e buffering do proxy mencionadas acima); o servidor falhou no meio da resposta e fechou sem um frame terminal; ou a resposta estava completa e apenas o sentinela foi perdido. O último chunk recebido decide qual caso ocorreu: um finish_reason nele significa que o texto está completo, e a ausência de finish_reason significa que ele foi interrompido e deve ser tentada novamente. Não deixe essa verificação a cargo do SDK — o SDK Python da OpenAI encerra silenciosamente o loop quando a conexão fecha sem [DONE] e só lança uma exceção quando um chunk contém um objeto de erro. Faça a verificação no seu próprio código:
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))Um fluxo vazio: 200 e depois nada
Às vezes o fluxo é aberto com HTTP 200 e termina sem um único chunk de conteúdo — sem texto, sem chamada de ferramenta e, às vezes, nem mesmo o chunk de role. Quase sempre isso indica que um upstream falhou depois que os cabeçalhos foram enviados: um provedor sobrecarregado, um gateway cujo próprio upstream recusou a solicitação ou um proxy que descartou o corpo. Trate-o como um fluxo interrompido e tente novamente com backoff; registre o corpo bruto de uma resposta com falha, pois a causa costuma ser um evento de erro dentro do fluxo que seu SDK ignorou. O Claude Code reage à mesma condição tentando novamente a solicitação sem streaming.
Se você estiver chamando pela Kunavo
O Kunavo transmite SSE no padrão OpenAI (com suporte a stream_options.include_usage) e eventos no padrão Anthropic em /v1/messages, portanto a reprodução com curl acima também é o teste de compatibilidade. Para os modelos Claude e GPT em /v1/chat/completions, um fluxo do Kunavo termina com data: [DONE], independentemente de a resposta ter terminado: depois do chunk com finish_reason quando terminou e depois de um chunk de erro — type upstream_error, code upstream_disconnect ou upstream_timeout — quando o upstream caiu no meio da resposta; assim, os SDKs da OpenAI lançam um APIError em vez de entregar o fragmento como se fosse a resposta completa. Um fluxo upstream que termina antes de qualquer conteúdo, ou falha antes do primeiro token, nunca chega até você como um 200 vazio: a tentativa é repetida em outro canal quando houver um para o modelo e, caso contrário, é retornada como um erro HTTP. Uma solicitação em streaming que falha antes de qualquer saída chegar até você não é cobrada; uma que é interrompida no meio da resposta não tem essa garantia — envie-a novamente em vez de presumir que foi gratuita.
Perguntas frequentes
Por que o uso é null nas minhas respostas em streaming?
Nas APIs no padrão OpenAI, o uso não é incluído nos fluxos a menos que você passe stream_options: {"include_usage": true}; nesse caso, ele chega no chunk final. O fluxo nativo da Anthropic informa o uso nos eventos message_start/message_delta.
Como detecto se um fluxo foi interrompido ou terminou?
Um fluxo concluído termina com um finish_reason (ou message_stop da Anthropic) e depois [DONE]. Uma conexão que fecha sem esses marcadores foi interrompida — trate-a como uma falha passível de nova tentativa, não como uma resposta curta.
O que significa “O fluxo SSE terminou sem [DONE]”?
Seu cliente leu o fluxo até o fim da conexão e nunca viu a linha data: [DONE] que encerra um fluxo compatível com a OpenAI. A API não enviou essa mensagem; seu cliente ou agente a escreveu. Se o último chunk continha um finish_reason, a resposta está completa e apenas o fechamento foi perdido. Se não continha, a resposta foi interrompida por um proxy, um timeout ou uma falha no servidor, e a solicitação deve ser tentada novamente.
O que significa “o fluxo terminou sem finish_reason”?
O cliente leu o fluxo até o fim e nenhum chunk continha um finish_reason — o campo que um fluxo compatível com a OpenAI usa para indicar que a resposta está completa (stop, length, tool_calls). Sem ele, o texto que você tem é um fragmento, por mais natural que pareça a última frase. Tente a solicitação novamente; se isso se repetir, procure um timeout de proxy ou buffering entre você e a API.
Por que uma API de LLM retornaria um fluxo vazio?
Porque a falha ocorreu depois que os cabeçalhos HTTP foram enviados: o provedor estava sobrecarregado, o upstream de um gateway recusou a solicitação ou um proxy descartou o corpo. A linha de status ainda informa 200, portanto as verificações de status não detectam o problema. Trate um fluxo sem chunk de conteúdo como uma solicitação com falha, tente novamente com backoff e registre uma resposta bruta para encontrar o evento de erro dentro dela.
É seguro ignorar a ausência de [DONE] se eu já tiver texto?
Somente quando o último chunk contiver um finish_reason. Sem ele, o texto que você tem é um fragmento que pode terminar no meio de uma frase ou de uma chamada de ferramenta, com seus argumentos JSON incompletos. Tente novamente em vez de salvá-lo como resposta.
Guias relacionados
- Guia de APIs compatíveis com OpenAI — do endpoint local do Ollama aos modelos de ponta hospedados
- “Streaming interrompido. Aguardando a mensagem completa” — o que significa e como resolver
- Claude Code “Resposta interrompida no meio do fluxo” e “A resposta em fluxo terminou antes que quaisquer dados completos fossem recebidos” — o que interrompe o fluxo
Mais detalhes sobre o significado dos erros estão em referência de erros; obter uma chave leva um minuto por meio de cadastro e da guia de autenticação.