Un descuento en los planes premium al registrarteConsigue tu descuento

blog/developers·12 ago 2026·4 min·por el equipo de eroq

Streaming de respuestas de IA con SSE: guía práctica

Server-sent events de principio a fin: parsear los chunks, retransmitir el stream por tu backend y los bugs de buffering que solo salen en producción.


El streaming es la diferencia entre un personaje que está presente y un spinner de carga. La mecánica de SSE es sencilla; los bugs están todos en los detalles que cubre esta guía.

El formato de transmisión

Con stream: true, /v1/chat/completions responde text/event-stream. Cada frame es una línea data: seguida de una línea en blanco; los deltas llegan con la forma estándar de chunk:

data: {"id":"cmpl_…","object":"chat.completion.chunk","choices":[{"delta":{"content":"That "}}]}

data: {"id":"cmpl_…","object":"chat.completion.chunk","choices":[{"delta":{"content":"noise"}}]}

data: {"object":"chat.completion.usage","usage":{"credits_spent":3,"credits_remaining":997}}

data: [DONE]

Hay dos frames propios de eroq que conviene conocer: el evento de uso, justo antes de [DONE], lleva el contador, y un evento de error ({"error":{…}}) sustituye al fallo que, si no, tendrías que deducir de una conexión cortada. Si el stream da error antes de que llegue ningún contenido, la llamada ya se ha reembolsado sola.

Parsear sin el bug clásico

El bug clásico: tratar cada chunk de red como un frame completo. TCP no respeta tus saltos de línea: un frame puede llegar partido entre varias lecturas. Acumula en un buffer, divide por \n\n y guarda el resto:

const reader = res.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
  const { done, value } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })   // stream: true matters for UTF-8
  const frames = buffer.split('\n\n')
  buffer = frames.pop() ?? ''                          // last piece may be incomplete
  for (const frame of frames) {
    const data = frame.replace(/^data: /, '')
    if (data === '[DONE]') continue
    const parsed = JSON.parse(data)
    if (parsed.error) throw new Error(parsed.error.message)
    const delta = parsed.choices?.[0]?.delta?.content
    if (delta) render(delta)
  }
}

El { stream: true } de decoder.decode no es decorativo: sin él, un carácter multibyte partido entre dos chunks se convierte en mojibake, caracteres ilegibles. Un roleplay cargado de emojis encuentra este bug en menos de una hora.

Retransmitir desde tu backend

Nunca envíes tu clave de API a un navegador: retransmite el stream a través del backend que ya guarda tu clave y tus fichas de personaje. Ese relay es una capa fina, pero dos detalles deciden si funciona o no:

// Node/Express-style relay
app.post('/chat', async (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream')
  res.setHeader('Cache-Control', 'no-cache')
  res.flushHeaders()                                   // 1. headers out immediately

  const upstream = await fetch('https://eroq.ai/v1/chat/completions', { /* … */ })
  for await (const chunk of upstream.body) {
    res.write(chunk)                                   // 2. relay bytes, re-frame nothing
  }
  res.end()
})
  1. Envía los encabezados de inmediato (flush), o tu proxy inverso puede acumular toda la respuesta en el buffer y entregarla de golpe: un streaming que llega en bloque. En nginx, añade también X-Accel-Buffering: no.
  2. Retransmite los bytes tal cual. Parsear y volver a serializar los frames en el relay duplica tu superficie de bugs sin aportar nada. Parsea en el cliente, donde renderizas.

Detalles de UX que separan lo bueno de lo excelente

  • Renderiza con un temporizador corto (30–50ms), no en cada delta: escribir en el DOM token a token da tirones en los teléfonos.
  • Muestra rápido el primer token y luego deja que fluya. La latencia percibida depende casi por completo del tiempo hasta el primer token.
  • Si falla a mitad del stream, conserva el texto parcial y ofrece una forma de reintentar. Media respuesta que se queda es mejor que una respuesta que desaparece; y como los streams interrumpidos después de la primera salida sí se generaron, conservar el texto respeta lo que se pagó.
  • Deja que el usuario cancele. Cerrar la respuesta de tu relay debería cerrar la solicitud upstream; un stream abandonado que sigues consumiendo es dinero gastado en generar para nadie.

Preguntas frecuentes

¿Cómo parseo correctamente los chunks SSE de la API de eroq?

Acumula los bytes en un buffer, divide por \n\n y guarda el fragmento final para la siguiente lectura: un frame puede llegar partido entre chunks de red. Pasa { stream: true } a TextDecoder.decode para que los caracteres multibyte sobrevivan a un corte, y sáltate el centinela [DONE] antes de parsear el JSON.

¿El streaming cuesta más que una respuesta normal?

No. Los créditos se cobran por respuesta completa, no por token, así que una respuesta en streaming y una con buffer cuestan lo mismo: 1 crédito en RP mini o 3 en RP+, tanto si la respuesta se queda en 50 tokens como si llega al tope de 1,200 tokens. No hay ninguna razón de precio para dejar stream: false.

¿Qué es el evento de uso al final del stream?

Un frame justo antes de data: [DONE] que lleva credits_spent y credits_remaining, para que puedas actualizar el saldo en pantalla sin una segunda llamada a la API. Un frame de error con la forma {"error":{…}} sustituye a la conexión cortada que, si no, tendrías que interpretar, y un stream que falla antes de enviar contenido ya se ha reembolsado solo.

¿Por qué mi stream llega todo de golpe y no token a token?

Casi siempre es un proxy que acumula la respuesta en el buffer. Envía los encabezados (flush) antes de la primera escritura y, en nginx, pon X-Accel-Buffering: no. El segundo sospechoso habitual es volver a serializar los frames dentro de tu relay: retransmite los bytes tal cual y parsea en el cliente, donde renderizas.

El precio fijo tiene una consecuencia más aquí: el streaming cuesta exactamente lo mismo que el buffering, los mismos 1 o 3 créditos. No hay ninguna razón para no hacer streaming, y por eso todos los ejemplos de la documentación lo hacen.

Etiquetasstreamingssechat api

Crea esto con los modelos detrás del artículo: empieza con 50 créditos gratis o explora todos los motores y sus precios.