Erro de Streaming da API LLM: Causas e Correções

Erro de Streaming da API LLM: Causas e Correções

Uma mensagem de erro error: llm api error: an error occurred during streaming quase sempre vem de uma de seis fontes: uma falha do provedor no meio do stream enviada como um evento SSE após a conexão já ter retornado 200, um timeout de leitura no lado do cliente ou do gateway, um limite de taxa 429 atingido durante a geração, um chunk malformado ou inesperado que quebra seu parser SSE, uma conexão de rede perdida, ou um problema de autenticação/cota que só aparece quando o stream é aberto. Verifique nessa ordem — a maioria das falhas de streaming se enquadra nas três primeiras.

Combine o que você está observando com uma causa antes de prosseguir:

O que você observa Vá para
O stream inicia, alguns tokens chegam, depois é interrompido sem mudança no status HTTP Causa 1
O stream para por 10s+ sem novos tokens, depois seu cliente levanta um timeout Causa 2
O erro ocorre com mais frequência durante picos de tráfego ou logo após uma rajada de requisições Causa 3
A exceção menciona KeyError, JSONDecodeError, ou uma falha de parsing, não um término de rede Causa 4
O erro é genérico (APIConnectionError, ECONNRESET) e ocorre de forma inconsistente, inclusive em redes estáveis Causa 5
O erro ocorre na primeira requisição após rotacionar uma chave, atingir um limite de gastos ou trocar de ambiente Causa 6

A lista de verificação de diagnóstico completa abaixo repete isso com a causa provável detalhada por linha.

Principais Conclusões

  • Um erro de streaming pode chegar como um evento SSE error após o status HTTP já ter retornado 200, então a lógica de repetição baseada apenas no código de status não o captura.
  • A API Messages da Anthropic relata sobrecargas no meio do stream como event: error com "type": "overloaded_error" dentro do corpo do stream, não como um novo HTTP 529.
  • O OpenRouter documenta o mesmo padrão entre provedores: uma vez que o primeiro token é enviado, uma falha chega como um chat.completion.chunk com um campo error de nível superior e finish_reason: "error".
  • Bibliotecas cliente que assumem que todo chunk do stream tem formato de sucesso vão travar com um erro enganoso (um APIConnectionError em vez da causa real) quando um provedor envia um payload de erro estruturado no meio do stream.
  • A maioria das correções é a mesma independentemente do provedor: analise o corpo do stream em busca de eventos de erro, defina um timeout de parada no nível do token separado do timeout de conexão, e use backoff exponencial com base no tipo de erro, não apenas no status HTTP.

Lista de Verificação de Diagnóstico: Encontre Sua Causa Raiz Primeiro

Percorra isso antes de alterar qualquer código. Cada linha mapeia um sintoma que você pode observar para a seção que o corrige.

O que você observa Causa provável Vá para
O stream inicia, alguns tokens chegam, depois é interrompido sem mudança no status HTTP Erro do provedor no meio do stream (sobrecarga, filtro de conteúdo, falha do provedor) Causa 1
O stream para por 10s+ sem novos tokens, depois seu cliente levanta um timeout Timeout de leitura ou parada do stream ocioso Causa 2
O erro ocorre com mais frequência durante picos de tráfego ou logo após uma rajada de requisições Limite de taxa atingido no meio do stream Causa 3
A exceção menciona KeyError, JSONDecodeError, ou uma falha de parsing, não um término de rede Chunk malformado ou inesperado Causa 4
O erro é genérico (APIConnectionError, ECONNRESET) e ocorre de forma inconsistente, inclusive em redes estáveis Conexão perdida ou erro do provedor mascarado Causa 5 e Causa 4
O erro ocorre na primeira requisição após rotacionar uma chave, atingir um limite de gastos ou trocar de ambiente Falha de autenticação ou cota Causa 6

Se seus logs mostram apenas um nome de exceção genérico e nenhuma mensagem upstream, isso por si só é um sintoma — veja Causa 4 e Causa 5 para entender por que wrappers genéricos escondem a causa real.

Causa 1: Erro do Provedor no Meio do Stream (Sobrecarga, Filtro de Conteúdo, Falha do Provedor)

Esta é a causa que mais confunde as pessoas, porque a requisição parecia bem-sucedida. Quando uma resposta de streaming começa, o código de status HTTP e os cabeçalhos já foram confirmados para o cliente. Se o provedor então encontrar uma falha — exaustão de capacidade, um erro interno, um filtro de conteúdo acionado após saída parcial, ou o processo do modelo falhar — ele não pode mudar o status HTTP para um código de erro. A falha tem que viajar dentro do próprio stream como um evento especial.

A API Messages da Anthropic documenta isso diretamente: a API “pode ocasionalmente enviar [erros] no stream de eventos” e dá este exemplo exato para uma condição de sobrecarga chegando no meio do stream:

event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}

Esta é uma lacuna real em muita lógica de repetição. Se seu código apenas inspeciona response.status_code, ele já viu 200 antes da falha ocorrer, então uma repetição nunca é acionada. Um relatório de incidente de terceiros descrevendo exatamente esse padrão afirma claramente: “A lógica de repetição baseada no código de status HTTP nunca é acionada porque o status já era 200. O resultado é uma resposta truncada silenciosamente” — e recomenda analisar o corpo do stream em busca de eventos de erro em vez de confiar apenas no código de status.

A documentação do OpenRouter descreve o mesmo problema estrutural entre os provedores que roteia, não apenas um fornecedor. Quando o primeiro token é escrito, “o status 200 OK e os cabeçalhos HTTP já foram confirmados — eles não podem ser alterados,” então uma falha do provedor “deve chegar em banda como um evento SSE.” Suas causas documentadas para essa classe de erro:

  • Desconexão do provedor — a conexão upstream cai após saída parcial (problema de rede, falha do provedor, timeout do balanceador de carga)
  • Timeout do provedor — o modelo para de responder durante a geração e o prazo de leitura expira
  • Limite de tokens atingido durante a geração — o modelo atinge max_tokens ou o contexto se enche enquanto produz saída
  • Filtro de conteúdo de saída — um sistema de moderação sinaliza o texto gerado depois que parte dele já foi transmitida
  • Sobrecarga do provedor — o upstream retorna um erro de limite de taxa ou capacidade após começar a transmitir

O payload de erro no meio do stream do OpenRouter carrega o erro dentro de um chunk de aparência normal, com um finish_reason que informa que o stream terminou anormalmente:

{"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"}]}

Como confirmar que esta é sua causa: registre o stream SSE bruto (não apenas a saída de texto analisada) para uma requisição com falha. Se você vir um evento error ou um chunk com um campo error de nível superior em qualquer lugar do stream antes do evento terminal, é isso.

Correção: trate payloads finish_reason: "error" e event: error como falhas passiveis de repetição, não como conclusões bem-sucedidas com conteúdo vazio. Repita com backoff no tipo de erro específico dentro do payload, não no status HTTP, já que o status HTTP já será 200.

Causa 2: Timeout de Leitura ou Parada do Stream Ocioso

Uma parada difere de um erro grave: a conexão permanece aberta, mas nenhum novo token chega por um período prolongado. A maioria dos clientes HTTP confunde dois conceitos de timeout — um timeout total de requisição e um timeout por leitura (ocioso). Um stream longo pode legitimamente exceder um timeout total curto em uma resposta grande, enquanto um stream realmente travado pode ficar bem dentro de uma janela de timeout total se não existir verificação de leitura ociosa.

O SDK Python da OpenAI define um timeout total padrão de 10 minutos: “Por padrão, as requisições expiram após 10 minutos. Você pode configurar isso com uma opção timeout, que aceita um float ou um objeto httpx.Timeout… Ao expirar, um APITimeoutError é lançado.” Ele também repete automaticamente certas falhas: “Erros de conexão (por exemplo, devido a um problema de conectividade de rede), 408 Request Timeout, 409 Conflict, 429 Rate Limit e erros >=500 Internos são todos repetidos por padrão,” duas vezes, configurável via max_retries.

Confirme: meça o intervalo entre o último token recebido e o erro. Uma duração fixa correspondente ao seu timeout configurado aponta para um timeout, não para uma falha do lado do provedor.

Correção: defina dois timeouts, não um — um timeout de conexão/total para o ciclo de vida da requisição e um timeout de leitura ociosa separado que dispare se nenhum chunk chegar dentro de N segundos. Isso permite distinguir “o modelo está lento” de “o stream morreu.” 15-30 segundos é um timeout de leitura ociosa razoável para chat completions; aumente se seu modelo tiver longas fases silenciosas de “pensamento” antes do primeiro token.

Causa 3: Limite de Taxa (429) no Meio do Stream

Limites de taxa geralmente rejeitam uma requisição antes que ela comece. Mas alguns gateways aplicam limites por token ou por janela, então uma requisição pode começar a transmitir e ser interrompida assim que cruza um orçamento durante a geração — o mesmo formato de erro em banda da Causa 1, com um código 429 chegando dentro do stream em vez do status de resposta inicial.

Confirme: verifique se as falhas se agrupam durante picos de tráfego ou em um limite consistente de requisições por minuto. Um cabeçalho Retry-After, ou um campo equivalente no payload de erro, confirma.

Correção: respeite Retry-After quando presente, reduza a concorrência antes de repetir e faça backoff exponencial. Acessos frequentes são um sinal para reduzir o número de streams paralelos ou aumentar o nível da conta, não para repetir de forma mais agressiva.

Causa 4: Chunk Malformado ou Inesperado Quebrando Seu Parser

Nem todo “erro de streaming” é culpa do modelo. Alguns são inteiramente do lado do cliente: seu parser SSE assume que todos os chunks têm um formato fixo, e um payload de formato diferente — incluindo um payload de erro legítimo do provedor — quebra essa suposição e lança uma exceção de aparência não relacionada.

Um caso documentado: o manipulador de streaming ollama_chat do LiteLLM esperava que todo chunk contivesse um campo message. Quando o Ollama em vez disso retornou um erro estruturado como {"error": "error parsing tool call: ..."}, o manipulador fez uma busca desprotegida chunk["message"], levantando KeyError: 'message'. Um manipulador de exceção mais amplo capturou e relançou como APIConnectionError — um erro com som de rede para o que era na verdade uma falha de parsing JSON dentro da saída da chamada de ferramenta do modelo. O relatório de bug é explícito sobre o custo: engenheiros depurando “registraram reason=timeout / LLM request timed out para o que era na verdade JSON de tool-call malformado,” custando “dois dias de diagnóstico errado” perseguindo causas de rede e infraestrutura que não eram o problema.

O padrão geral: um provedor envia um erro em um formato que seu código de parsing não espera, uma exceção de baixo nível dispra (KeyError, TypeError, um erro de decode JSON), e um bloco except genérico encapsula em um erro de conexão ou straming genérico que apaga a causa real.

Confirme: registe o chunk bruto antes do seu código de encapsulamento de exceção execur. Se o payload bruto tem um campo error com uma mensagem real, seu cliente descartou informações úteis no caminho para o error genérico que está vendo.

Correção: verifique a existência de uma chave error antes de acessar campos esperados como message ou delta, e desvie para ela explicitamente. Preserve a mensagem de erro upstream e o código de status ao relançar, em vez de colapsar toda falha em um único tipo de exceção genérico.

Causa 5: Conexão de Rede Perdida

Às vezes a causa realmente é a rede: um proxy ou balanceador de carga fecha uma conexão de longa duração após um período ocioso, ou a rede de um cliene muda durante a requisição. Isso se assemelha à Causa 2, mas a c correção difere — a conexão em si não exise mais, não apeas ociosa dentro da sua própria janela de timeout.

Confirme: procure por erros do tipo reset de conexão (ECONNRESET, Broken pipe, Conection reset by peer) em vez de uma exceção de timeout no nível da aplicação, e verifique se as falhas correlam com um tempo limite de conexão ociosa conhecido de um intermediário (muitos balanceadores de carga padrão 60s de inatividade).

Correão: se você controla o proxy ou balanceador de carga, aume o timeout de conexão ociosa acima da duração máxima esperada do stream, ou adicioe pings keep-alive. Se a queda estiver fora do seu contrle, trate-a como passiva de repetição — a maioria das APIs de streaming não suporta retomar um stream parcial, enão repetir significa começar a geração do zero.

Causa 6: Fallha de Autenticação ou Cota que Aparece no Meio do Stream

Alguns gateways validam autenticação e cobrança de forma laz: a conexão abre, então a verificação de autenticação ou saldo ocorre enquano gera o prmeiro chunk, e uma falha lá é relada como um erro de streaming em vez de um 401/402 limpo no momento da requisição.

Confirme: verifique se o erro ocorre em toda requisição de uma determinada chave, em vez de intermitenteente, e se começou logo após uma rotação de chave, um evento de cobrança ou uma mudança de ambiente (chave de desenvolvimento contra uma URL base de produção, ou vice-versa).

Correção: verifique o formato da chave (Authorization: Bearer <chave>, não a chave crua), confisme se a chave está ativa e verifique o saldo da conta separadamente da depuração no nível da requisição. Toda requisição falhando de forma idêntica, independente do prompt ou modelo, aponta para aqui em vez das cinco causas anteriores.

Padrão de Repetição e Backoff que Lida com Todas as Seis

Uma única estratégia de repetição pode cobrir as Causas 1 a 5 se ela verificar o corpo do stream, não apenas o status HTTP:

import time
import openai

def stream_ith_recovery(client, **kwags):
    max_atempts = 3
    for atempt in range(max_atempts):
        try:
            collected = ""
            stream = client.hat.comletions.reate(stream=True, **kwags)
            for chunk in stream:
                hoice = chunk.hoices[0] if chunk.hoices else None
                if hoice and getattr(hoice, "finish_reason", None) == "error":
                    raise RuntimeError(f"mid-strea error: {chunk}")
                if hoice and hoice.elta.ontent:
                    collected += hoice.delta.ontent
            return collected
        except (openai.APITimeoutError, openai.APIConnectionError, openai.RateLmitError) as e:
            if atempt == max_ttempts - 1:
                raise
            time.eep(2 ** atempt)
    return collected

Este exemplo usa o formato de cliente compatível com OpenAI que também funciona contra o endpoint de chat completions compatível com OpenAI da Novita AI, definindo base_url="https://api.novita.ai/openai". Três coisas importam além do código acima:

  • Verifique o conteúdo do chunk em busca de um erro em banda antes de assumir uma conclusão normal, conforme a Causa 1.
  • Use backoff exponencial (2 ** tentativa, com limite) em vez de tentativas imediatas, especialmente para limites de taxa e erros de sobrecarga.
  • Registre a mensagem de erro upstream bruta antes de encapsulá-la em seu próprio tipo de exceção, para que depurações futuras não repitam o diagnóstico errado de dois dias na Causa 4.

Se um provedor ou modelo específico estiver falhando com mais frequência que outros, rotear essa classe de requisição para um modelo diferente é uma mitigação que vale a pena ter em vigor antes de precisar — veja operando um serviço LLM multi-provedor com um objetivo de uptime definido para como definir essa política de fallback em vez de improvisá-la durante um incidente.

Conclusão

Diagnosticar esse erro é um processo de eliminação, não de adivinhação: use a lista de verificação no início para corresponder seu sintoma a uma de seis causas, então confirme com o sinal específico que cada seção aponta — um evento error em banda, uma duração de parada correspondendo ao seu timeout, um 429 correlacionado a picos de tráfego, um payload bruto em que seu parser engasgou, uma string de reset de conexão, ou uma falha que se repete em toda requisição de uma chave. As causas 1 a 3 são as mais comuns, e as causas 1, 3 e 5 compartilham a mesma correção fundamental: pare de confiar no código de status HTTP uma vez que um stream começou, e em vez disso repita com backoff com base no que o próprio stream relata.

Corrigir a lógica de repetição uma vez, usando o padão acima, fecha as cinco primeiras causas ao mêmeo tempo. A causa 6 é a exceção — nenhuma quatidade de repetições conserta uma chave inválida ou um saldo vazio, então trate falhas idênticas em toda requisição como uma verificação de configução, não um problema de rede.

Pergntas Frequentes

Por que minha requisição à API LLM funciona às vezes e falha com um erro de streaming outras vezes?

Falhas intermitentes apontam para uma causa no meio do stream, em vez de um problema de configuração — erros de configuração como uma chave ruim ou endpoint errado falham todas as vezes. Verifique sobrecarga do provedor e limites de taxa primeiro, já que ambos dependem de tráfego.

Um erro de streaming é o mesmo que um timeout?

Nem sempre. Um timeout significa que nenhuma resposta chegou dentro da sua janela configurada. Um erro no meio do stream significa que uma resposta começou, alguns tokens chegaram e um evento de falha foi então enviado dentro do stream. O tratamento de erros deve distinguir os dois, pois as correções diferem.

Por que minha mensagem de erro diz “erro de conexão” quando o problema real era outra coisa?

Muitas bibliotecas cliente encapsulam exceções inesperadas em um tipo de erro de conexão genérico quando um chunk não corresponde ao formato que o parser espera — veja a Causa 4 para um caso documentado onde um erro de parsing JSON foi relatado como um timeout de conexão por dois dias antes da causa real ser encontrada.

Posso retomar um stream após um erro no meio do stream em vez de começar de novo?

A maioria das APIs de streaming compatíveis com OpenAI e estilo Anthropic não suporta retomar um stream parcial a partir do ponto da falha. Repita a requisição completa, descartando a saída parcial em vez de anexar a ela, para evitar conteúdo duplicado.

Devo sempre usar streaming para chamadas de API LLM?

Streaming evita que uma única requisição grande atinja timeout, mas introduz o modo de falha de saída parcial abordado ao longo deste guia. Para respostas curtas onde você não precisa mostrar saída parcial, uma requisição sem streaming é mais simples de tratar erros.

O que significa finish_reason: error em uma resposta transmitida?

É um sinal terminal que alguns gateways anexam ao chunk final de um stream que falhou no meio do caminho, distinto de valores normais como stop ou length. Trate como uma geração falha, mesmo que o status HTTP da requisição tenha sido 200.

Artigos Recomendados