Vercel AI SDK - Integración unificada de modelos IA con streaming y hooks React
Receta
npm install ai @ai-sdk/openai @ai-sdk/anthropic// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-4o"),
messages,
});
return result.toDataStreamResponse();
}// app/chat/page.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
<div>
{messages.map((m) => (
<div key={m.id}>
<strong>{m.role}:</strong> {m.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Enviar</button>
</form>
</div>
);
}Cuándo usarlo: Necesitas integrar chat LLM, completions, o llamadas a herramientas en una aplicación React o Next.js con soporte de streaming y boilerplate mínimo.
Ejemplo Funcional
// app/components/ChatWithTools.tsx
"use client";
import { useChat } from "@ai-sdk/react";
export default function ChatWithTools() {
const { messages, input, handleInputChange, handleSubmit, isLoading } =
useChat({
api: "/api/chat-tools",
onError: (error) => console.error("Error de chat:", error),
});
return (
<div className="mx-auto max-w-2xl p-4">
<div className="space-y-4">
{messages.map((message) => (
<div
key={message.id}
className={`rounded-lg p-3 ${
message.role === "user" ? "bg-blue-100 ml-auto" : "bg-gray-100"
}`}
>
<p className="text-sm font-medium">{message.role}</p>
<p>{message.content}</p>
{message.toolInvocations?.map((tool, i) => (
<pre key={i} className="mt-2 text-xs bg-gray-200 p-2 rounded">
{JSON.stringify(tool, null, 2)}
</pre>
))}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="mt-4 flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Haz una pregunta..."
className="flex-1 border rounded px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
>
{isLoading ? "Pensando..." : "Enviar"}
</button>
</form>
</div>
);
}// app/api/chat-tools/route.ts
import { streamText, tool } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { z } from "zod";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
system: "Eres un asistente útil que puede buscar el clima.",
messages,
tools: {
getWeather: tool({
description: "Obtener el clima actual para una ubicación",
parameters: z.object({
location: z.string().describe("Nombre de la ciudad"),
}),
execute: async ({ location }) => {
// Reemplaza con una llamada real a la API
return { location, temperature: 72, condition: "sunny" };
},
}),
},
maxSteps: 5,
});
return result.toDataStreamResponse();
}Lo que esto demuestra:
- Interfaz de chat completa con streaming usando el hook
useChat - Llamadas a herramientas con validación de esquema Zod
- Ejecución de herramientas multi-paso con
maxSteps - Gestión de estado de carga y manejo de errores
- Uso del modelo Claude de Anthropic
Análisis Profundo
Cómo Funciona
- El AI SDK proporciona una interfaz unificada entre proveedores (OpenAI, Anthropic, Google, Mistral, etc.) para que puedas intercambiar modelos cambiando una línea
streamTextdevuelve un objetoStreamTextResultque se convierte en unaResponsea través detoDataStreamResponse(), enviando Server-Sent Events al clienteuseChatgestiona el estado completo de la conversación: historial de mensajes, estado de entrada, carga, error y controlador de aborto- Las llamadas a herramientas utilizan esquemas Zod para validación de parámetros; el SDK maneja automáticamente el bucle de llamada/resultado de herramientas hasta
maxStepsiteraciones generateTextes la contraparte sin streaming para procesamiento por lotes o generación del lado del servidor- El SDK utiliza un Data Stream Protocol que codifica deltas de texto, llamadas de herramientas, resultados de herramientas y razones de finalización en un único stream
Variaciones
Usando OpenRouter para acceso a modelos:
npm install @openrouter/ai-sdk-providerimport { createOpenRouter } from "@openrouter/ai-sdk-provider";
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
const result = streamText({
model: openrouter("anthropic/claude-sonnet-4-20250514"),
messages,
});Generación sin streaming:
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
const { text, usage } = await generateText({
model: openai("gpt-4o"),
prompt: "Resume este artículo en 3 puntos.",
});Salida estructurada con generateObject:
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
recipe: z.string(),
ingredients: z.array(z.string()),
steps: z.array(z.string()),
}),
prompt: "Genera una receta para galletas de chocolate.",
});useCompletion para prompts de un solo turno:
"use client";
import { useCompletion } from "@ai-sdk/react";
export default function Completion() {
const { completion, input, handleInputChange, handleSubmit } = useCompletion({
api: "/api/completion",
});
return (
<div>
<p>{completion}</p>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
);
}Notas de TypeScript
- El tipo
Messagedeaiincluyeid,role,contenty el opcionaltoolInvocations - Los parámetros de herramientas se infieren de esquemas Zod, proporcionando seguridad de tipos completa en funciones
execute useChatdevuelvemessages: Message[]fuertemente tipado- Las opciones específicas del proveedor se pueden pasar a través del campo
providerOptions
import type { Message } from "ai";
// Los tipos de resultado de herramientas se infieren del esquema Zod
const weatherTool = tool({
parameters: z.object({ city: z.string() }),
execute: async ({ city }) => {
// city está tipado como string
return { temp: 72 }; // el tipo de retorno se infiere
},
});Gotchas
-
Claves API faltantes — El SDK lanza un error en tiempo de ejecución si la variable de entorno de clave API del proveedor no está configurada. Solución: Establece
OPENAI_API_KEY,ANTHROPIC_API_KEY, etc. en.env.local. Cada proveedor tiene su propio nombre de variable de entorno esperado. -
useChat no se actualiza — Si los mensajes no se transmiten, la ruta de API puede no devolver una respuesta de datos en stream. Solución: Siempre devuelve
result.toDataStreamResponse(), noresult.texto una respuesta JSON simple. -
Llamadas a herramientas no se ejecutan — Herramientas definidas pero
maxStepsno configurado tiene valor predeterminado de 1, por lo que el uso de herramientas multi-paso no funcionará. Solución: EstablecemaxSteps: 5(o superior) para permitir que el modelo procese resultados de herramientas y continúe. -
Errores CORS en desarrollo — Las rutas de API deben estar en la misma aplicación Next.js. Solución: Usa rutas de API de Next.js (
app/api/) en lugar de llamar a un servidor externo directamente desdeuseChat. -
Bundle grande de importaciones de proveedor — Importar múltiples proveedores aumenta el tamaño del bundle del cliente. Solución: Las importaciones de proveedores son solo del servidor; mantenlas en rutas de API o Server Actions, nunca en archivos
"use client".
Alternativas
| Librería | Mejor Para | Ventaja/Desventaja |
|---|---|---|
| Vercel AI SDK | Aplicaciones React/Next.js IA full-stack | Opinada, vinculada a convenciones de ecosistema Vercel |
| LangChain.js | Cadenas complejas, RAG, agentes | Abstracción más pesada, curva de aprendizaje más pronunciada |
| OpenAI SDK directamente | Uso simple solo OpenAI | Sin hooks React en streaming, proveedor único |
| Anthropic SDK directamente | Uso simple solo Claude | Sin hooks React en streaming, proveedor único |
| LlamaIndex.ts | Indexación y recuperación de datos | Enfocado en RAG, menos en interfaz de chat |
FAQs
¿Qué devuelve streamText y cómo llega al cliente?
streamTextdevuelve un objetoStreamTextResult- Llama a
.toDataStreamResponse()para convertirlo en unaResponsecon Server-Sent Events - El hook
useChaten el cliente consume este stream y actualiza los mensajes en tiempo real - Nunca devuelvas
result.texto JSON simple -- el hook espera el protocolo de stream de datos
¿Cómo cambio entre modelos de OpenAI y Anthropic?
import { openai } from "@ai-sdk/openai";
import { anthropic } from "@ai-sdk/anthropic";
// Solo cambia la línea del modelo:
const result = streamText({
model: anthropic("claude-sonnet-4-20250514"),
messages,
});La interfaz unificada significa que solo el parámetro model cambia.
¿Qué es maxSteps y por qué mis llamadas de herramientas no se ejecutan sin él?
maxStepscontrola cuántos turnos de llamada-a-herramienta-y-resultado puede realizar el modelo- El valor predeterminado es 1, lo que significa que el modelo puede llamar a una herramienta pero no puede procesar su resultado
- Establece
maxSteps: 5o superior para uso de herramientas multi-paso - Cada paso = un turno del modelo (texto o llamada de herramienta) seguido de un resultado de herramienta
¿Cómo defino una herramienta con validación de esquema Zod?
import { tool } from "ai";
import { z } from "zod";
const weatherTool = tool({
description: "Obtener clima para una ciudad",
parameters: z.object({
city: z.string().describe("Nombre de la ciudad"),
}),
execute: async ({ city }) => {
return { temp: 72, condition: "sunny" };
},
});Los tipos de parámetros de herramientas se infieren automáticamente del esquema Zod.
Gotcha: ¿Por qué los mensajes no se transmiten en mi componente useChat?
- La ruta de API debe devolver
result.toDataStreamResponse(), no una respuesta JSON - Verifica que la prop
apienuseChatapunte a la ruta correcta - Revisa las herramientas del navegador para errores -- las claves API faltantes causan lanzamientos del lado del servidor
- Asegúrate de que la ruta esté en la misma aplicación Next.js para evitar problemas CORS
¿Cuál es la diferencia entre streamText y generateText?
streamTextenvía tokens al cliente conforme se generan (streaming)generateTextespera la respuesta completa y la devuelve de una sola vez- Usa
streamTextpara interfaces de chat donde quieras mostrar tokens en tiempo real - Usa
generateTextpara procesamiento por lotes o generación del lado del servidor donde el streaming no es necesario
¿Cómo genero salida JSON estructurada en lugar de texto libre?
import { generateObject } from "ai";
import { z } from "zod";
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: z.object({
title: z.string(),
tags: z.array(z.string()),
}),
prompt: "Genera metadatos para una publicación de blog sobre React.",
});¿Cómo uso useCompletion para prompts de un solo turno en lugar de chat multi-turno?
useCompletiongestiona un único ciclo de prompt/respuesta, no una conversación- Devuelve
completion(el texto de respuesta),inputy manejadores de formulario - Apunta
apia una ruta que usastreamTextcon unprompten lugar demessages
Gotcha: ¿Por qué importar un proveedor en un componente cliente infla mi bundle?
- Los paquetes de proveedores (
@ai-sdk/openai,@ai-sdk/anthropic) son solo del servidor - Importarlos en un archivo
"use client"los incluye en el JavaScript del cliente - Mantén todas las importaciones de proveedores en rutas de API o Server Actions
useChatyuseCompletionson los únicos imports del AI SDK seguros para componentes del cliente
¿Cómo se definen los tipos de TypeScript para parámetros de herramientas y mensajes de useChat?
- Los parámetros de herramientas se infieren de esquemas Zod -- no se necesitan tipos manuales
useChatdevuelvemessages: Message[]dondeMessageincluyeid,role,contenty el opcionaltoolInvocations- Importa
Messagede"ai"si necesitas tipificar props que acepten mensajes
import type { Message } from "ai";¿Cómo uso OpenRouter como proveedor para acceso a modelos?
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
const result = streamText({
model: openrouter("anthropic/claude-sonnet-4-20250514"),
messages,
});Relacionado
- TanStack Query — Cachea y gestiona datos de respuestas IA
- Next.js Server Actions — Alternativa a rutas de API para llamadas IA
- TypeScript React — Patrones de tipos utilizados con AI SDK