Capítulo 4 de 21 · básico
Respuestas en streaming
Qué cubre esta sesión
El capítulo de la primera llamada esperaba la respuesta completa antes de imprimir cualquier cosa. Eso funciona para una oración. Para un párrafo se siente roto — el lector se queda viendo cómo no pasa nada y luego le cae un bloque de texto de golpe. El streaming cambia eso por una máquina de escribir. El modelo manda tokens conforme los genera y la terminal los imprime conforme llegan. Mismo modelo, mismo prompt, misma latencia total. Lo que cambia es cómo se siente, y eso es la mayor parte de lo que los usuarios notan.
OpenAI
Un solo argumento de keyword. stream=True cambia la respuesta de "dame
el bloque final" a "dame una secuencia de eventos tipados". Al iterar
salen varios tipos de evento — created, in-progress, deltas de
output-text, done, completed. Solo los deltas llevan texto; el resto es
metadata que yo ignoro. Cada delta se imprime con end="" y
flush=True para que la terminal lo vaya tecleando en vez de guardar la
línea completa en el buffer.
"""Week 3 - Streaming responses (OpenAI).
Same one API call as week 1, with stream=True. The SDK yields typed
events as tokens come back from the server; we filter for the
output-text delta events and print each delta as it arrives. The
terminal shows the answer take shape instead of waiting for the full
blob.
"""
import os
import sys
from dotenv import find_dotenv, load_dotenv
from openai import OpenAI
load_dotenv(find_dotenv())
if not os.environ.get("OPENAI_API_KEY"):
sys.exit("OPENAI_API_KEY is not set. Put it in the course-root .env file.")
client = OpenAI()
# stream=True turns the call into a server-sent-event stream. Iterating
# the response yields one event per server-side delta; we only want the
# text deltas, so we filter by event.type.
stream = client.responses.create(
model="gpt-5.4-nano",
input="In one short paragraph, explain why streaming responses matter for chat UX.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
# end="" + flush=True is what turns this into a typewriter.
# Without flush the terminal buffers a whole line at a time.
print(event.delta, end="", flush=True)
print()
Algunas cosas a tener en cuenta. Los tipos de evento son estables en toda
la Responses API, así que el filtro sobre response.output_text.delta se
mantiene entre modelos. Los deltas son chunks del lado del servidor, no
tokens individuales — a veces una palabra, a veces una oración. No pasa
nada; la lección es "no esperes", no "haz algo con cada token". Y
flush=True no es negociable. Python bufferea stdout por defecto. Sin
flush, la máquina de escribir se desarma.
Gemini
Gemini apostó por la otra ergonomía. El streaming tiene su propio
método — generate_content_stream, el gemelo en streaming de
generate_content. El tipo de retorno es más simple: un iterador de
chunks, cada uno con un atributo .text que trae un delta de string o
viene vacío.
"""Week 3 - Streaming responses (Gemini).
The Gemini SDK exposes streaming as a separate method
(`generate_content_stream`) that returns an iterator of chunks. Each
chunk carries a partial text delta. Same teaching point as the OpenAI
example: print as you go, no buffering.
"""
import os
import sys
from dotenv import find_dotenv, load_dotenv
from google import genai
load_dotenv(find_dotenv())
if not os.environ.get("GEMINI_API_KEY"):
sys.exit("GEMINI_API_KEY is not set. Put it in the course-root .env file.")
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
# generate_content_stream is the streaming twin of generate_content.
# Each iteration yields a partial response carrying a `.text` delta.
stream = client.models.generate_content_stream(
model="gemini-3.1-flash-lite",
contents="In one short paragraph, explain why streaming responses matter for chat UX.",
)
for chunk in stream:
if chunk.text:
print(chunk.text, end="", flush=True)
print()
Misma UX, distinta forma en el SDK. OpenAI sobrecargó un método con una bandera; Google lo dividió en dos. Ambos funcionan. Elige el que se lea más claro en tu codebase y mantente consistente en todo el proyecto.
Ponlo a trabajar
Tres showcases, tres proyectos de docker-compose. Cada uno es una pestaña
del navegador con un formulario. bash bootstrap-secrets.sh, luego
docker compose up --build, luego http://localhost:3000. La barra
lateral del libro los lista como tarjetas. Cada tarjeta abre una landing
page con el código fuente, un widget de prueba que puedes usar en vivo y
una descarga en zip de un solo comando.
Showcase 1 — Explicador en vivo
Haz una pregunta y mira la respuesta tomar forma. La misma estructura de
una-llamada-a-la-API de la semana 1, apuntada a un prompt de preguntas y
respuestas, con stream=True haciendo el trabajo. El punto es la
sensación: esperar un bloque completo vs. ver la respuesta construirse.
Misma latencia total. Solo cambia la experiencia.
Showcase 2 — Traducción en streaming
Pega texto, elige un idioma destino y mira la traducción llegar oración
por oración. La traducción es donde el streaming se gana su lugar —
output medianamente largo, cero valor en agruparlo, y puedes empezar a
leer la primera oración mientras el modelo sigue en la tercera. El input
lleva el idioma destino en un pequeño encabezado target: <language>
para que el contrato de def run(input) siga siendo un solo string.
Showcase 3 — Reescritura en vivo
Pega cualquier borrador de mensaje. Una versión más ajustada llega en streaming. Mismos hechos, menos palabras. Este es el más cercano a una herramienta de uso diario — la UI en streaming hace que la reescritura se sienta como editar junto a alguien, que es la experiencia que quieres en un asistente de escritura.
En los tres, el diseño que mantiene el código honesto es la forma de dos
callables. run_stream produce tokens conforme llegan — esa es la
lección. run es un acumulador delgado sobre run_stream que devuelve
el string final. El frontend local le pega a /api/ai/stream para la
máquina de escribir; el widget de prueba del sitio le pega a /api/ai
para el texto acumulado. Una sola ruta de streaming, dos superficies,
cero lógica duplicada.
Córrelo
README.md tiene los prerequisitos, los comandos del CLI básico y los
tres bloques de ejecución por showcase. Versión corta: python code/openai/main.py para el demo más simple, o cd a cualquier
showcase, bash bootstrap-secrets.sh y docker compose up --build.
Conclusiones
El streaming está a un argumento de keyword de distancia de una app que
se siente mejor, y el modelo nunca se volvió más rápido. Ese truco —
cambiar la experiencia de la espera sin cambiar la latencia real — es una
de las victorias más baratas en el trabajo con AI. Vale la pena. Mantén
la lógica del proveedor en un generador run_stream para que tú
controles el loop, no el SDK. Acumula el stream en un string cuando
necesites uno en el borde. No uses streaming para respuestas de una
línea; el overhead no lo vale. Úsalo cuando el output sea lo bastante
largo como para que el lector se cambie de pestaña si no.