- Puntos clave
- Lista de verificación de diagnóstico: encuentre su causa raíz primero
- Causa 1: Error del proveedor en mitad del flujo (sobrecarga, filtro de contenido, fallo del proveedor)
- Causa 2: Tiempo de espera de lectura o estancamiento del flujo ocioso
- Causa 3: Límite de tasa (429) en miad del flujo
- Causa 4: Fragmento malformado o inesperado que rompe su analizador
- Causa 5: Conexión de red caída
- Causa 6: Fallo de autenticación o cuota que surge en mitad del flujo
- Patrón de reintento y retroceso que maneja las seis
- Conclusión
- FAQ
- Artículos recomendados
Un mensaje de error como error: llm api error: an error occurred during streaming casi siempre proviene de una de seis fuentes: un fallo del proveedor en mitad del flujo enviado como un evento SSE después de que la conexión ya haya devuelto 200, un tiempo de espera de lectura del lado del cliente o del gateway, un límite de tasa 429 alcanzado durante la generación, un fragmento inesperado o malformado que rompe su analizador SSE, una coneión de red caída, o un problema de autentiación/cuota que solo surge una vez que el flujo se abre. Revíselos en ese orden: la mayoría de los fallos de streaming se enmarcan en las tres primeras.
Compare lo que observa con una causa antes de leer más:
| Lo que observa | Vaya a |
|---|---|
| El flujo comienza, llegan algunos tokens, luego se corta sin cambio en el estado HTTP | Causa 1 |
| El flujo se detiene por 10s+ sin nuevos tokens, luego su cliente genera un tiempo de espera | Causa 2 |
| El error ocurre más a menudo durante picos de tráfico o justo después de una ráfaga de solicitudes | Causa 3 |
La excepción menciona KeyError, JSONDecodeError, o un fallo de análisis, no un término de red |
Causa 4 |
El error es genérico (APIConnectioError, ECONNRESET) y ocurre de forma inconsistente, incluso en redes estables |
Causa 5 |
| El error ocurre en la primera solicitud tras rotar una clave, alcanzar un límite de gasto o cambiar de entorno | Causa 6 |
La lista de verificación de diagnóstico completa a continuación repite esto con la causa probable indicada por fila.
Puntos clave
- Un error de streaming puede llegar como un evento SSE
errordespués de que el estado HTTP ya haya devuelto 200, por lo que la lógica de reintento basada solo en el código de estado lo pasa por alto. - La API de mensajes de Anthropic reporta sobrecargas en mitad del flujo como
event: errorcon"type": "overloaded_error"dentro del cuerpo del flujo, no como un nuevo HTTP 529. - OpenRouter documenta el mismo patrón en todos los proveedores: una vez que se envía el primer token, un fallo llega como un
chat.completion.chunkcon un campoerrorde nivel superior yfinish_reason: "error". - Las bibliotecas cliente que asumen que cada fragmento transmitido tiene forma de éxito fallarán con un error engañoso (un
APIConnectioErroren lugar de la causa real) cuando un proveedor envía una carga útil de error estruturada en mitad del flujo. - La mayoría de las soluciones son las mismas independientemente del proveedor: analice el cuerpo del flujo en busca de eventos de error, establezca un tiempo de espera de bloqueo a nivel de token separado del tiempo de espera de coneión, y use retroceso exponencial claveado por el tipo de error, no solo por el estado HTTP.
Lista de verificación de diagnóstico: encuentre su causa raíz primero
Revise esto antes de cambiar cualquier código. Cada fila asigna un síntoma que puede observar a la sección que lo soluciona.
| Lo que observa | Causa probable | Vaya a |
|---|---|---|
| El flujo comienza, llegan algunos tokens, luego se corta sin cambio en el estado HTTP | Error del proveedor en mitad del flujo (sobrecarga, filtro de contenido, fallo del proveedor) | Causa 1 |
| El flujo se detiene por 10s+ sin nuevos tokens, luego su cliente genera un tiempo de espera | Tiempo de espera de lectura o estancamiento ocioso | Causa 2 |
| El error ocurre más a menudo durante picos de tráfico o justo después de una ráfaga de solicitudes | Límite de tasa alcanzado en mitad del flujo | Causa 3 |
La excepción menciona KeyError, JSONDecodeError, o un fallo de análisis, no un término de red |
Fragmento malformado o inesperado | Causa 4 |
El error es genérico (APIConnectionError, ECONNRESET) y ocurre de forma inconsistente, incluso en redes estables |
Conexión caída o error del proveedor enmascarado | Causa 5 y Causa 4 |
| El error ocurre en la primera solicitud tras rotar una clave, alcanzar un límite de gasto o cambiar de entorno | Fallo de autenticación o cuota | Causa 6 |
Si sus registros solo muestran un nombre de excepción genérico y ningún mensaje ascendente, eso mismo es un síntoma; consulte Causa 4 y Causa 5 para entender por qué los envoltorios genéricos ocultan la causa real.
Causa 1: Error del proveedor en mitad del flujo (sobrecarga, filtro de contenido, fallo del proveedor)
Esta es la causa que confunde a más personas, porque la solicitud parecía exitosa. Una vez que comienza una respuesta de streaming, el código de estado HTTP y los encabezados ya están comprometidos con el cliente. Si luego el proveedor sufre un fallo — agotamiento de capacidad, un error interno, un filtro de contenido que se activa después de una salida parcial, o el proeso del modelo se bloquea — no puede cambiar el código de estado HTTP a un código de error. El fallo tiene que viajar dentro del propio flujo como un evento especial.
La API de mensajes de Anthropic documenta esto directamente: la API “ocasionalmente puede enviar [errores] en el flujo de eventos,” y da este ejmplo exato para una condición de sobrecarga que llega en mitad del flujo:
event: error
data: {"type": "error", "error": {"type": "overloaded_error", "message": "Overloaded"}}
Esta es una brecha real en mucha lógica de reintento. Si su código solo inspecciona response.status_code, ya vio 200 antes de que ocurriera el fallo, por lo que un reintento nunca se activa. Un informe de incidente de terceros que describe este patrón exacto lo dice claramente: “La lógica de reintento que se basa en el código de estado HTTP nunca se activa porque el estado ya era 200. El resultao es una respuesta truncada silenciosamente” — y recomienda analizar el cuerpo del flujo en busca de eventos de error en lugar de confiar solo en el código de estado.
La documentación de OpenRouter describe el mismo problema estructural en todos los proveedores a los que rutea, no solo en un vendedor. Una vez que se escrit el primer token, “el estado HTTP 200 OK y los encabezados ya están comprometidos — no se pueden cambiar,” por lo que un fallo del proveedor “debe llegar en banda como un evento SSE.” Sus causas documentadas para esta clase de error:
- Desconexión del proveedor: la conexión ascendente se cae después de una salida parcial (problema de red, caída del proveedor, tiempo de espera del balanceador de carga)
- Tiempo de espera del proveedor: el modelo deja de responder en medio de la generación y expira el plazo de lectura
- Límite de tokens alcanzado durante la generación: el modelo alcanza
max_tokenso la ventana de contexto se llena mientras produce salida - Filtro de contenido de salida: un sistema de moderación marca el texto generado después de que ya se ha transmitido parte de él
- Sobrecarga del proveedor: el ascendente devuelve un límite de tasa o de capacidad después de comenzar a transmitir
La carga útil de error de OpenRouter en mitad del flujo lleva el error dentro de un fragmento de aspecto normal, con un finish_reason que le indica que el flujo terminó 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"}]}
Cómo confirmar que esta es su causa: registre el flujo SSE crudo (no solo la salida de texto analizada) para una solicitud que falla. Si ve un evento error o un fragmento con un campo error de nivel superior en cualquier parte del flujo antes del evento terminal, esta es la causa.
Solución: trate las cargas útiles finish_reason: "eror" y event: error como falos reintentables, no como completaciones exitosas con contenido vacío. Reintente con retroceso en el tipo de error específico dentro de la carga útil, no en el estado HTTP, ya que el estado HTTP ya leerá 200.
Causa 2: Tiempo de espera de lectura o estancamiento del flujo ocioso
Un estancamiento difiere de un error grave: la conexión permanece abierta, pero no llegan nuevos tokens durante un período prolongado. La mayoría de los clientes HTTP confunden dos conceptos de tiempo de espera: un tiempo de espera total de solicitud y un tiempo de espera por lectura (ocio). Un flujo largo puede legítimamente superar un tiempo de espera total corto en una respuesta grande, mientras que un flujo realmente atascado puede permanecer dentro de una ventana de tiempo de espera total si no existe una verificación de lectura ociosa.
El SDK de Python de OpenAI establece un tiempo de espera total de solicitud predeterminado de 10 minutos: “Por defecto, las solicitudes superan el tiempo de espera después de 10 minutos. Puede configurar esto con una opción timeout, que acepta un flotante o un objeto httpx.Timeout… Al exceder el tiempo, se lanza un APITimeoutError”. También reintenta automáticamente ciertos fallos: “Los errores de conexión (por ejemplo, debido a un problema de conectividad de red), 408 Solicitud con tiempo excedido, 409 Conflicto, 429 Límite de tasa y errores >=500 Internos se reintentan por defecto”, dos veces, configurable mediante max_retries.
Confirmelo: mida el espacio entre el último token recibido y el error. Una duración fija que coincida con su valor de tiempo de espera configurado aputa a un tiempo de espera, no a un fallo del lado del proveedor.
Solución: establezca dos tiempos de espera, no uno: un tiempo de espera de conexión/total para el ciclo de vida de la solicitud, y un tiempo de espera de lectura ociosa separado que se active si no llega ningún fragmento dentro de N segundos. Eso le permite distinguir “el modelo es lento” de “el flujo murió”. 15-30 segundos es un tiempo de espera de lectura ociosa razonable para completaciones de chat; auméntelo si su modelo tiene fases largas de “pensamiento” silencioso antes del primer token.
Causa 3: Límite de tasa (429) en miad del flujo
Los límites de tasa generalmente rechazan una solicitud antes de que comenzara. Pero algunas puertas de enlace aplican límites por token o por ventana, por lo que una solicitud puede comenzar a transmitir y cortarse una vez que cruza un presupuesto en medio de la generación, la misma forma de error en banda que la Causa 1, con un código 429 que llega dentro del flujo en lugar de como el estado de respuesta inicial.
Confírmelo: compruebe si los fallos se agrupan durante ráfagas de tráfico o en un umbral consistente de solicitudes por minuto. Un encabezado Retry-After, o un campo equivalente en la carga útil del error, lo confirma.
Solución: respete Retry-After cuando esté presente, reduzca la concurrencia antes de reintentar y retroceda exponencialmente. Los golpes frecuentes son una señal para reducir el número de flujos paralelos o actualizar el nivel de cuenta, no para reintentar de manera más agresiva.
Causa 4: Fragmento malformado o inesperado que rompe su analizador
No todos los “errores de streaming” son culpa del modelo. Algunos son completamente del lado del cliente: su analizador SSE asume que cada fragmento tiene una forma fija, y una carga útil con forma diferente, incluida una carga útil de error legítima del proveedor, rompe esa suposición y lanza una excepción de aspecto no relacionado.
Un caso documentado: el manejador de streaming ollama_chat de LiteLLM esperaba que cada fragmento contuviera un campo message. Cuando Ollama en su lugar devolvió un error estructurado como {"error": "error parsing tool call: ..."}, el manejador hizo una búsqueda no protegida chunk["message"], generando KeyError: 'message'. Un manejador de excepciones más amplio lo capturó y lo volvió a lanzar como APIConnectionError — un error con sonido de red para lo que en realidad era un fallo de análisis JSON dentro de la salida de llamada a herramienta del modelo. El informe de error es explícito sobre el coste: los ingenieros que lo depuraban “registraron reason=timeout/LLM request timed out para lo que en realidad era un JSON de llamada a herramienta malformado,” costando “dos días de diagnóstico erróneo” persiguiendo causas de red e infraestructura que no eran el problema.
El patrón general: un proveedor envía un error en una forma que su código de análisis no espera, se dispara una excepción de bajo nivel (KeyError, TypeError, un error de decodificación JSON), y un bloque except general lo envuelve en un error genérico de conexión o streaming que borra la causa real.
Confirmelo: registre el fragmento crudo antes de que se ejecute su código de envoltura de excepcciones. Si la carga útil cruda tiene un campo error con un mensaje real, su cliente descartó información útil en el camino hacia el error genérico que está viendo.
Solución: verifique si hay una clave error antes de acceder a campos esperados como message o delta, y bifurque en ella explícitamente. Conserve el mensaje de error ascendente y el código de estado al volver a lanzar, en lugar de colapsar cada fallo en un tipo de excepción genérico.
Causa 5: Conexión de red caída
A veces la causa realmente es la red: un proxy o balanceador de carga cierra una conexión de larga duración después de un período de inactividad, o la red de un cliente cambia en medio de la solicitud. Esto se asemeja a la Causa 2 pero la solución difiere: la conexión en sí misma se ha ido, no solo inactiva dentro de su propia ventana de tiempo de espera.
Confirmelo: busque errores de estilo de restablecimiento de conexión (ECONNRESET, Broken pipe, Connection reset by peer) en lugar de una excepción de tiempo de espera a nivel de aplicación, y verifique si los fallos se correlacionan con el tiempo de espera de conexión inactiva de un intermediario conocido (muchos balanceadores de carga predeterminan 60s de inactividad).
Solución: si controla el proxy o el balanceador de carga, aumente su tiempo de espera de coneión inaciva por encima de la duración máxima esperada del flujo, o agregue ping de mantenimiento. Si la caída está fuera de su control, trátela como reintentable: la mayoría de las APIs de streaming no admiten reanudar un flujo parcial, por lo que reintentar significa comenzar la generación de nuevo.
Causa 6: Fallo de autenticación o cuota que surge en mitad del flujo
Algunas puertas de enlace validan la autenticación y la facturación de forma perezosa: la conexión se abre, luego la verificación de autenticación o saldo ocurre mientras se genera el primer fragmento, y un fallo allí se reporta como un error de streaming en lugar de un 401/402 limpio en el momento de la solicitud.
Confírmelo: compruebe si el error ocurre en cada solicitud de una clave determinada, en lugar de de forma intermitente, y si comenzó justo después de una rotación de clave, un evento de facturación o un cambio de entorno (clave de desarrollo contra una URL base de producción, o viceversa).
Solución: verifique el formato de la clave (Authorization: Bearer <key>, no la clave cruda), confirme que la clave está activa y verifique el saldo de la cuenta por separado de la depuración a nivel de solicitud. Cada solicitud que falla idénticamente independientemente del prompt o del modelo apunta aquí en lugar de a las cinco causas anteriores.
Patrón de reintento y retroceso que maneja las seis
Una única estrategia de reintento puede cubrir las Causas 1 a 5 si verifica el cuerpo del flujo, no solo el estado HTTP:
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.creat(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.APIConnectioError, openai.RateLimitError) as e:
if attempt == max_attempts - 1:
raise
time.sleep(2 ** attempt)
return collected
Este ejemplo utiliza la forma de cliente compatible con OpenAI que también funciona contra el punto final de completaciones de chat compatible con OpenAI de Novita AI estableciendo base_url="https://apiproxy.novita.ai/openai". Tres cosas importan más allá del código anterior:
- Verifique el contenido del fragmento en busca de un error en banda antes de asumir una finalización normal, según Causa 1.
- Use retroceso exponencial (
2 ** attempt, con tope) en lugar de reintentos inmediatos, especialmente para límites de tasa y errores de sobrecarga. - Registre el mensaje de error ascendente crudo antes de envolverlo en su propio tipo de excepción, para que la depuración futura no repita el diagnóstico erróneo de dos días de Causa 4.
Si un proveedor o modelo específico falla con más frecuencia que otros, enrutar esa clase de solicitud a un modelo diferente es una mitigación que vale la pena tener en su lugar antes de necesitarla; consulte operar un servicio LLM multimroveedor con un objetivo de tiempo de actividad definido para saber cómo definir esa política de conmutación por error en lugar de improvisarla durante un incidente.
Conclusión
Diagnosticar este error es un proceso de eliminación, no de adivinación: use la lista de verificación al principio para emparejar su síntoma con una de las seis causas, luego confírmelo con la señal específica que cada sección señala: un evento error en banda, una duración de estancamiento que coincide con su tiempo de espera, un 429 correlacionado con ráfagas, una carga útil cruda que su analizador no pudo procesar, una cadena de restablecimiento de conexión, o un fallo que se repite en cada solicitud de una clave. Las causas 1 a 3 son las más comunes, y las causas 1, 3 y 5 comparten la misma solución fundamental: deje de confiar en el código de estado HTTP una vez que un flujo ha comenzado, y en su lugar reintente con retroceso basado en lo que el propio flujo reporta.
Arreglar la lógica de reintento una vez, usando el patrón anterior, cierra las primeras cinco causas al mismo tiempo. La causa 6 es la excepción: ninguna cantidad de reintentos arregla una clave inválida o un saldo vacío, así que trate los fallos idénticos en cada solicitud como una verificación de configuración, no como un problema de red.
FAQ
¿Por qué mi solicitud a la API LLM funciona a veces y falla con un error de streaming otras veces?
Los fallos intermitentes apuntan a una causa en mitad del flujo en lugar de a un problema de configuración: los errores de configuración como una clave incorrecta o un punto final equivocado fallan todas las veces. Verifique primero la sobrecarga del proveedor y los límites de tasa, ya que ambos dependen del tráfico.
¿Es lo mismo un error de streaming que un tiempo de espera?
No siempre. Un tiempo de espera significa que no llegó ninguna respuesta dentro de su ventana configurada. Un error en mitad del flujo significa que una respuesta comenzó, llegaron algunos tokens y luego se envió un evento de fallo dentro del flujo. El manejo de errores debe distinguir ambos, ya que las soluciones difieren.
¿Por qué mi mensaje de error dice “error de conexión” cuando el problema real era otro?
Muchas bibliotecas cliente envuelven excepciones inesperadas en un tipo de error de conexión genérico cuando un fragmento no coincide con la forma que el analizador espera; consulte Causa 4 para un caso documentado donde un error de análisis JSON fue reportado como un tiempo de espera de conexión durante dos días antes de encontrar la causa real.
¿Puedo reanudar un flujo después de un error en mitad del flujo en lugar de empezar de nuevo?
La mayoría de las APIs de streaming compatibles con OpenAI y estilo Anthropic no admiten reanudar un flujo parcial en el punto de fallo. Reintente la solicitud completa, descartando la salida parcial en lugar de agregarla, para evitar contenido duplicado.
¿Debería usar siempre streaming para las llamadas a la API LLM?
El streaming evita que una sola solicitud grande supere el tiempo de espera, pero introduce el modo de fallo de salida parcial cubierto a lo largo de esta guía. Para respuestas cortas donde no necesita mostrar salida parcial, una solicitud sin streaming es más simple de manejar con errores.
¿Qué significa finish_reason: error en una respuesta transmitida?
Es una señal terminal que algunas puertas de enlace adjuntan al fragmento final de un flujo que falló a medio camino, distinto de valores normales como stop o length. Trátelo como una generación fallida, aunque el estado HTTP de la solicitud fuera 200.
Artículos recomendados
- Mejor servicio LLM multi-proveedor para menor costo y mayor tiempo de actividad? — Diseño de SLO, monitoreo de salud del proovedor y guiones de incidentes para equipos que manejan tráfico LLM en múltiples proovedores.
- SDK Python de OpenAI: Instalación, configuración e integración práctica — Configuración del cliente, streaming, reintentos y opciones de tiempo de espera para el SDK utilizado en el ejemplo de código anterir.
- ¿Qué es la limitación de tasa? Una guía práctica para servicios de IA — Cómo se aplican los límites de tasa y cómo diseñar el comportamiento de reintento en torno a ellos.
