Elija Auto, TokenLab Verified o Official para cada solicitud, con los precios mostrados por adelantado. Ver las novedades

Reintento de respuestas de LLM en streaming sin duplicar el output

CryptoCrypto
·28 de septiembre de 2026·12 min de lectura·Actualizado 28 de septiembre de 2026·31 vistas
#streaming#API de respuestas#fiabilidad#websocket
Reintento de respuestas de LLM en streaming sin duplicar el output

Una solicitud de streaming solo es segura de repetir cuando se cumplen tres condiciones simultáneamente: nada ha llegado a su cliente, nada observable ha sido contabilizado y la solicitud no conlleva estado en el servidor. Después del primer evento de salida, la decisión correcta es informar del fallo en lugar de repetirlo.

TokenLab aplica esa regla en su gateway para el streaming de la Responses API, tanto en HTTP como en WebSocket. La ruta de WebSocket se modificó el 2026-09-28 para coincidir con HTTP.

Por qué un stream es diferente de una solicitud normal

Una llamada sin streaming devuelve un cuerpo o un error. Puede reintentar el error porque no recibió nada.

Un stream le entrega salida antes de que la solicitud haya finalizado. El primer evento de salida es el punto de no retorno. Si la conexión se interrumpe después de eso, usted se queda con texto parcial. Repetir la solicitud significa generar la misma respuesta de nuevo y pagar por ella dos veces. También podría duplicar una llamada a herramienta que su agente ya ejecutó.

La guía de streaming de TokenLab lo establece directamente:

Después de que llega el primer evento, un stream interrumpido está incompleto y no se reinicia automáticamente.

Por lo tanto, su cliente necesita un bit de estado local: saw_output. Cambia a verdadero en el momento en que cualquier salida llega a su código. Cada decisión de reintento lee ese bit primero.

Un stream que termina sin response.completed es un fallo. No asuma que el texto que tiene está completo. Maneje los eventos response.failed, response.incomplete y error.

La decisión de reintento, punto por punto

TokenLab repite una solicitud una vez en otra ruta disponible cuando se cumplen todas estas condiciones: la solicitud no tiene estado, nada ha llegado al cliente, no se observó ningún resultado o uso para el intento fallido, y el fallo es un evento previo a la salida reintentable o un error de lectura upstream antes del primer evento. Como máximo, se realiza un reintento por solicitud. Si el reemplazo también falla antes de la salida, ese fallo no se vuelve a intentar.

Fuente: Guía de streaming de TokenLab y comportamiento del gateway, observado el 2026-09-28.

Punto de fallo ¿Repetido por TokenLab? Razón
Evento previo a la salida reintentable (response.failed, o un evento de error marcado como reintentable, como un error upstream sobrecargado o interno) Sí, una vez, si la solicitud no tiene estado Nada llegó al cliente y no se observó uso, por lo que una segunda ejecución es invisible.
Interrupción del stream upstream (error de lectura) antes del primer evento Sí, una vez, si la solicitud no tiene estado Misma ventana. El cliente no tiene salida ni cargos.
Segundo fallo antes de la salida, después de un reintento No El presupuesto es de un reintento por solicitud.
Cualquier fallo después de que la salida llegó al cliente No El cliente ya tiene texto parcial. Un reintento duplicaría la salida y el costo.
Respuesta almacenada (store), continuación (previous_response_id) o una solicitud ligada al origen No Una segunda ejecución podría crear una segunda respuesta almacenada o divergir el estado de la conversación.
Tiempo de espera del primer evento No El upstream podría seguir generando. Un reintento podría ejecutar el mismo trabajo dos veces mientras el primer intento continúa.
Desbordamiento del búfer previo a la salida No El límite es local al gateway. El mismo prefijo de gran tamaño probablemente volvería a alcanzarlo en la siguiente ruta.
Cliente desconectado No El cliente dejó de escuchar.
Fallo determinista, por ejemplo, una solicitud no válida No Reintentar no puede cambiar el resultado. Entregado sin cambios.
Fallo que ya conlleva uso No El intento fue contabilizado. Entregado sin cambios.
No queda ninguna otra ruta No No hay dónde enviarlo. El cliente recibe el fallo con su propio código.

Cuando un fallo no se repite, o no queda ninguna otra ruta, usted lo recibe con su propio código de error. Ejemplos públicos: stream_read_error cuando el stream upstream se rompió, y upstream_stream_buffer_limit en caso de desbordamiento de búfer. Si la selección de ruta falla después de una decisión de reintento, el turno de WebSocket termina con websocket_response_failed (estado 500) y el cargo reservado es reembolsado.

La facturación sigue la misma línea. Usted paga solo por el intento entregado. Una solicitud repetida puede haberse ejecutado en el upstream dos veces, y ese costo adicional de upstream es de TokenLab, porque nada le llegó desde el primer intento. Un turno fallido que no entrega nada es reembolsado.

Un detalle de tiempo es importante para su manejo de errores. Antes de que comience la salida, el gateway retiene response.created y response.in_progress hasta que llega el primer evento de salida o un fallo, durante un máximo de 10 segundos. Esos eventos retenidos le llegan junto con la primera salida, o con el evento terminal. El orden y el contenido no cambian. Solo los ve un poco más tarde. Esos 10 segundos son un máximo, no un retraso típico.

Qué cambió para WebSocket el 2026-09-28

TokenLab sirve la Responses API mediante streaming HTTP ("stream": true, server-sent events) y mediante WebSocket en wss://api.tokenlab.sh/v1/responses, donde el cliente envía eventos response.create. Las respuestas de WebSocket siempre se transmiten en streaming. No admiten background ni response.cancel. Cada conexión maneja una respuesta activa a la vez durante un máximo de 60 minutos.

Antes del cambio, las dos rutas no coincidían. HTTP retenía los eventos del ciclo de vida y repetía los fallos previos a la salida sin estado. WebSocket reenviaba response.created de inmediato y entregaba los fallos previos a la salida al cliente, reembolsándolos. El mismo contratiempo en el upstream producía una respuesta limpia en HTTP y un error en WebSocket.

La ruta de WebSocket ahora sigue la regla de HTTP, incluyendo el reintento de un stream que se interrumpe antes de que llegue cualquier evento. Internamente, la mayoría de los fallos de upstream vistos en los turnos de WebSocket ocurrían antes de cualquier salida. Esa es exactamente la ventana donde un reintento es seguro.

El gateway mejora el caso de fallo previo a la salida. No garantiza que un stream se complete.

Cómo se implementó el cambio sin romper otros comportamientos

El trabajo siguió un proceso diseñado para detectar cambios de comportamiento silenciosos.

  • Bloqueo de comportamiento. Antes del cambio, cada escenario de turno de WebSocket se registró como una prueba: los marcos que recibe el cliente, las llamadas upstream realizadas y el resultado de facturación. La suite creció a 63 escenarios registrados durante este trabajo. Un cambio de comportamiento debe declararse por adelantado. Solo pueden cambiar las pruebas mencionadas en esa declaración. Todas las demás deben permanecer idénticas byte a byte.
  • Comprobaciones de mutación. Cada nueva regla de decisión se probó invirtiéndola deliberadamente, como repetir un tiempo de espera del primer evento o no repetir un fallo de lectura, y confirmando que el bloqueo fallaba.
  • Una revisión de control. La primera versión también hizo que el caso de desbordamiento de búfer fuera repetible, bajo la premisa de paridad con HTTP. La revisión mostró que HTTP nunca repite ese caso, por la razón expuesta en la tabla. Un seguimiento restauró el comportamiento anterior y añadió escenarios límite: un segundo fallo de lectura no se repite, no queda ninguna ruta, un fallo después de un response.created retenido, y un stream de reemplazo que luego se rompe.

Los registros de solicitud de un turno que tuvo éxito después de un reintento ahora también registran el intento fallido anterior, como ya hacía HTTP.

Código de cliente que posee la decisión de reintento

Establezca los reintentos automáticos del SDK en 0 para llamadas de streaming. Eso mantiene la decisión en su código. Mantenga la decisión de reintento en un solo lugar, no dispersa entre manejadores. Para errores HTTP, respete retryable y retry_after como se describe en la guía de manejo de errores, y mantenga los IDs de solicitud.

SSE sobre HTTP

import os

from openai import OpenAI

with OpenAI(
    api_key=os.environ["TOKENLAB_API_KEY"],
    base_url="https://api.tokenlab.sh/v1",
    timeout=30.0,
    max_retries=0,  # poseer la decisión de reintento en lugar de reenviar un stream leído a medias
) as client:
    completed, saw_output = False, False
    with client.responses.create(
        model="gpt-5.6-terra",
        input="Responde con una frase corta sobre los reintentos.",
        stream=True,
    ) as stream:
        for event in stream:
            if event.type == "response.output_text.delta":
                saw_output = True
                print(event.delta, end="", flush=True)
            elif event.type == "response.completed":
                completed = True
            elif event.type in {"response.failed", "response.incomplete", "error"}:
                raise RuntimeError(f"{event.type} after_output={saw_output}")
    if not completed:
        raise RuntimeError(f"stream cerrado antes de response.completed, after_output={saw_output}")
    print()

El ejemplo utiliza el SDK de OpenAI 2.15.0 contra https://api.tokenlab.sh/v1 con max_retries=0. Rastrea saw_output y lanza una excepción en eventos response.failed, response.incomplete y error, y en un stream que se cierra antes de response.completed. Verificado en producción el 2026-09-28 con gpt-5.6-terra.

Si el fallo llega con saw_output == false y la solicitud era elegible (sin estado, con un fallo repetible), TokenLab ya la ha repetido una vez; las respuestas almacenadas, las continuaciones y los tiempos de espera del primer evento no se repitieron en absoluto. Decida a nivel de aplicación si una solicitud nueva es aceptable, porque una solicitud nueva es una generación nueva. Si saw_output == true, informe del fallo y muestre lo que tiene, o descarte el texto parcial deliberadamente.

WebSocket

import asyncio
import json
import os

import websockets

URL = "wss://api.tokenlab.sh/v1/responses"
TERMINAL = {"response.completed", "response.failed", "response.incomplete", "error"}


async def run_turn(prompt: str) -> str:
    headers = {"Authorization": f"Bearer {os.environ['TOKENLAB_API_KEY']}"}
    async with websockets.connect(URL, additional_headers=headers, max_size=None) as ws:
        await ws.send(json.dumps({
            "type": "response.create",
            "model": "gpt-5.6-terra",
            "input": prompt,
            "store": False,
        }))
        text, saw_output = [], False
        async for raw in ws:
            event = json.loads(raw)
            kind = event.get("type")
            if kind == "response.output_text.delta":
                saw_output = True
                text.append(event["delta"])
            elif kind in TERMINAL:
                if kind != "response.completed":
                    # Después de que la salida ha comenzado, un fallo es definitivo para este turno.
                    # Reenvíe solo si su aplicación puede descartar el texto parcial.
                    raise RuntimeError(f"{kind} after_output={saw_output}: {json.dumps(event)[:300]}")
                return "".join(text)
        raise RuntimeError(f"socket cerrado antes de un evento terminal, after_output={saw_output}")


print(asyncio.run(run_turn("Responde con una frase corta sobre los reintentos.")))

El ejemplo utiliza websockets 16.0, se conecta a wss://api.tokenlab.sh/v1/responses con un encabezado Bearer, envía un response.create con store: false y recopila response.output_text.delta. Lanza una excepción con after_output en cualquier evento terminal no completado o un cierre anticipado. Verificado en producción el 2026-09-28 con gpt-5.6-terra.

El flag after_output es la misma idea que saw_output. Le indica a su código de llamada si un turno nuevo es siquiera posible sin duplicar efectos secundarios.

Lista de verificación para su propia lógica de reintento

  • Trate un stream que termina sin response.completed como un fallo, siempre.
  • Rastree un booleano para saber si la salida llegó a su código. Cámbielo en el primer evento de salida, no en el primer evento del ciclo de vida.
  • Un fallo previo a la salida en una solicitud elegible ya ha tenido su único reintento de gateway; un intento adicional es su decisión.
  • Después de una salida parcial, reenvíe solo si su aplicación puede descartar el texto parcial y aceptar pagar por dos generaciones.
  • En bucles de agentes, verifique si el stream parcial ya contenía una llamada a herramienta sobre la que su código actuó. No repita un turno cuyos efectos secundarios no pueda deshacer.
  • Para respuestas almacenadas y continuaciones de previous_response_id, inspeccione qué estado existe antes de reenviar nada.
  • Establezca los reintentos de streaming en 0 en su SDK y mantenga la decisión de reintento en una sola función.
  • Registre los IDs de solicitud para que pueda hacer coincidir una respuesta entregada con los intentos detrás de ella.

Preguntas frecuentes

¿TokenLab reinicia un stream después de una salida parcial?

No. Una vez que la salida ha llegado a su cliente, se informa de un fallo y nunca se repite. Usted tiene texto parcial, por lo que un reinicio duplicaría la salida y el costo. Su aplicación decide si mostrar, truncar o descartar lo que tiene.

¿Se me cobrará dos veces si el gateway repite mi solicitud?

No. Usted paga solo por el intento entregado. Una solicitud repetida puede haberse ejecutado en el upstream dos veces, pero nada le llegó desde el primer intento, y ese costo adicional de upstream es de TokenLab. Un turno fallido que no entrega nada es reembolsado.

¿Por qué no se reintenta un tiempo de espera del primer evento?

Porque el upstream podría seguir generando. Un reintento podría ejecutar el mismo trabajo dos veces mientras el primer intento continúa. Un tiempo de espera del primer evento se trata de manera diferente a un error de lectura que rompe el stream antes del primer evento.

¿Puedo repetir una respuesta almacenada o una continuación de previous_response_id?

No automáticamente. TokenLab nunca repite respuestas almacenadas, continuaciones o solicitudes ligadas al origen, porque una segunda ejecución podría crear una segunda respuesta almacenada o divergir el estado de la conversación. Verifique qué estado existe antes de reenviar nada, y reenvíe solo si su aplicación puede reconciliar ese estado.

Si desea observar el stream de eventos sin procesar usted mismo, cree una API key y registre cada tipo de evento que recibe su cliente. La guía de streaming y la guía de manejo de errores cubren el conjunto completo de eventos. Para obtener información sobre cómo el gateway enruta y se recupera, consulte Infraestructura de confiabilidad de la API de TokenLab AI y Responses API vs Chat Completions para agentes.

Fuentes

Modelos relacionados

Modelos lanzados recientemente

Construye con los modelos de esta guía

Compara precios, prueba rutas y convierte la investigación en una llamada API real.