Streaming UI con Vercel AI SDK: chat y UI generativa en Next.js
Tutorial técnico de streaming UI con Vercel AI SDK 6 y Next.js App Router: streaming de texto con streamText, el hook useChat, UI estructurada con Output y UI generativa con herramientas.
El streaming es lo que hace que una interfaz de IA se sienta viva: en vez de esperar a que el modelo termine, el usuario ve la respuesta aparecer token a token, reduciendo la latencia percibida. En Cognitiva, agencia chilena de IA, construimos estas interfaces con Vercel AI SDK, cuya versión vigente es la 6 (GA en diciembre de 2025). Este tutorial muestra el patrón completo en Next.js App Router: streaming de texto en el servidor con streamText, el hook useChat para consumirlo en el cliente, UI estructurada con la API Output (que reemplaza al ahora deprecado streamObject) y UI generativa con herramientas. Todo el código es fiel a la documentación oficial de AI SDK 6. Si vienes de versiones anteriores, hay cambios importantes que marco en el camino: los mensajes ya no usan content sino parts, y la generación de objetos se hace con Output, no con streamObject.
Qué es streaming UI y por qué importa
Los modelos generan texto de forma incremental. Sin streaming, la aplicación espera a que se complete toda la respuesta antes de mostrar algo: el usuario percibe una pausa larga. Con streaming, los tokens aparecen a medida que se producen, reduciendo drásticamente el tiempo hasta el primer token (TTFT) y la sensación de espera.
Hay dos niveles. El streaming de texto transmite la respuesta como string que el cliente renderiza progresivamente. El streaming de UI estructurada o generativa va más allá: el modelo decide invocar herramientas y el cliente mapea esos resultados a componentes React —una tarjeta, un gráfico, una lista—, no solo texto. AI SDK 6 cubre ambos con una API unificada.
Streaming de texto en el servidor
En el App Router, un Route Handler llama a streamText, convierte los mensajes de UI a mensajes de modelo con convertToModelMessages y devuelve la respuesta en streaming. Nota clave: streamText no se await (devuelve de inmediato y expone streams perezosos).
// app/api/chat/route.ts
import { streamText, convertToModelMessages, UIMessage } from "ai";
export const maxDuration = 30; // permite respuestas largas en streaming
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: "anthropic/claude-sonnet-4.5", // string del AI Gateway (ver Proveedor)
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}El método toUIMessageStreamResponse() entrega el formato de mensajes con partes tipadas que consume useChat. Para texto plano (por ejemplo con la API de objetos) se usa toTextStreamResponse().
El hook de cliente: useChat
En el cliente, useChat (de @ai-sdk/react) maneja el estado del chat y el streaming. Cambio importante respecto a versiones previas: los mensajes ya no tienen content plano, sino un array parts tipado, y se envían con sendMessage({ text }); el input lo controlas tú.
"use client";
import { useChat } from "@ai-sdk/react";
import { useState } from "react";
export default function Chat() {
const [input, setInput] = useState("");
const { messages, sendMessage, status, stop, error, regenerate } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role === "user" ? "Tú: " : "IA: "}</strong>
{m.parts.map((part, i) =>
part.type === "text" ? <span key={i}>{part.text}</span> : null
)}
</div>
))}
{(status === "submitted" || status === "streaming") && (
<button onClick={() => stop()}>Detener</button>
)}
{error && <button onClick={() => regenerate()}>Reintentar</button>}
<form
onSubmit={(e) => {
e.preventDefault();
sendMessage({ text: input });
setInput("");
}}
>
<input value={input} onChange={(e) => setInput(e.target.value)} />
</form>
</div>
);
}El hook expone status ("submitted" | "streaming" | "ready" | "error"), stop() para cancelar y regenerate() para reintentar. La doc recomienda mostrar un mensaje de error genérico y no filtrar el detalle del servidor.
UI estructurada: Output (no streamObject)
Cambio crítico en AI SDK 6: streamObject y generateObject quedaron deprecados. El patrón vigente es streamText/generateText con la opción output usando la clase Output (Output.object, Output.array), que ya es estable.
// app/api/notificaciones/route.ts
import { streamText, Output } from "ai";
import { z } from "zod";
const schema = z.object({
notificaciones: z.array(
z.object({
titulo: z.string(),
mensaje: z.string().describe("Mensaje breve, sin emojis."),
})
),
});
export async function POST(req: Request) {
const { contexto } = await req.json();
const result = streamText({
model: "openai/gpt-4.1",
output: Output.object({ schema }),
prompt: "Genera 3 notificaciones para: " + contexto,
});
return result.toTextStreamResponse();
}El objeto parcial se va completando a medida que llega; se itera con result.partialOutputStream (antes era partialObjectStream). En el cliente, experimental_useObject permite consumirlo, aunque sigue marcado como experimental en v6.
UI generativa: del texto al componente
El patrón recomendado hoy para renderizar componentes (no RSC) es definir herramientas con execute en el servidor; el modelo decide invocarlas y el cliente renderiza un componente según la parte tipada tool-<nombre> y su estado.
// servidor: permite varios pasos (tool call -> respuesta)
import { streamText, convertToModelMessages, stepCountIs } from "ai";
import { tools } from "@/ai/tools";
const result = streamText({
model: "anthropic/claude-sonnet-4.5",
system: "Eres un asistente útil.",
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5),
tools,
});
return result.toUIMessageStreamResponse();// cliente: dentro del map de m.parts
if (part.type === "tool-mostrarClima") {
switch (part.state) {
case "input-available":
return <div key={i}>Cargando clima...</div>;
case "output-available":
return <Clima key={i} {...part.output} />;
case "output-error":
return <div key={i}>Error: {part.errorText}</div>;
}
}Sin stopWhen, el modelo se detiene tras la primera tool call; con stepCountIs(n) permite que invoque la herramienta y luego responda en el mismo turno.
Cómo encaja con RSC y App Router
El patrón estándar hoy es híbrido, no RSC puro:
- El **Route Handler** corre en el servidor, llama a streamText y devuelve el stream. Es un endpoint, no un componente de servidor.
- El **componente de chat es Client Component** ("use client") porque useChat necesita estado e interactividad.
- Los **React Server Components** sirven para el shell estático (layout, data fetching de la página) que envuelve al chat y le pasa datos iniciales como props.
Sobre AI SDK RSC (el módulo con streamUI): la documentación oficial lo marca experimental y no recomendado para producción —entre otras razones, no se puede abortar el stream desde Server Actions y hay problemas de re-montaje de componentes—. Para UI generativa estable, usa AI SDK UI (useChat + herramientas + parts tipadas), no RSC.
El proveedor: AI Gateway
En AI SDK 6 el proveedor global por defecto es el AI Gateway de Vercel, así que basta pasar un string "proveedor/modelo" sin importar paquetes ni gestionar varias API keys; el Gateway centraliza routing, failover y seguimiento de costos.
import { streamText } from "ai";
const result = streamText({
model: "anthropic/claude-sonnet-4.5", // o "openai/gpt-4.1", etc.
prompt: "...",
});Si prefieres ir directo al proveedor con tu propia key, instala el paquete correspondiente (por ejemplo @ai-sdk/anthropic) y pasa anthropic("claude-sonnet-4.5"). Para el stack de Cognitiva (Claude + Vercel), el patrón natural es AI Gateway con un string de Anthropic, alineado con nuestro cornerstone del stack.
Buenas prácticas y gotchas
- **Declara maxDuration** en el Route Handler para que Vercel permita respuestas largas en streaming.
- **Usa stopWhen: stepCountIs(n)** en flujos con herramientas, o el modelo se detiene tras la primera llamada.
- **Cancelación:** stop() funciona con streaming HTTP del Route Handler; en Server Actions con RSC no se puede abortar (otra razón para evitar RSC).
- **Errores:** usa el error del hook con regenerate() y muestra un mensaje genérico; en el servidor maneja onError de streamText.
- **Persistencia:** guarda los UIMessage como fuente de verdad (traen metadata y parts) y convierte a mensajes de modelo solo al llamar al modelo.
- **use client** solo en el componente interactivo; deja el resto como Server Component.
Montar streaming UI con AI SDK en Next.js
De endpoint a chat con streaming.
- Paso 1
Servidor
El endpoint de streaming.
- Crear Route Handler con streamText
- Convertir mensajes con convertToModelMessages
- Devolver toUIMessageStreamResponse()
- Paso 2
Cliente
Consumir el stream.
- Componente con useChat
- Renderizar message.parts
- Agregar stop() y regenerate()
- Paso 3
Estructura y UI
Más allá del texto.
- Objetos con Output.object
- UI generativa con herramientas y parts tool-<nombre>
- Paso 4
Producción
Robustez.
- maxDuration y stopWhen
- Manejo de errores y cancelación
- Proveedor vía AI Gateway
Preguntas frecuentes
Da el siguiente paso
Hablemos
Cuéntanos sobre tu negocio. Respondemos en 24 horas.
Otros canales
Escríbenos por WhatsApp o agenda una llamada de 30 minutos.