Vercel AI SDK — Módulo de Chat
Un módulo autocontenido para chat usando el Vercel AI SDK con Google (Gemini) como proveedor. Cubre chat completion, chat con streaming, historial de conversaciones/mensajes, múltiples conversaciones, gestión de conversaciones y gestión de contexto. Speech-to-text y text-to-speech están expuestos como stubs (mirá "Voz" más abajo).
Es independiente: no depende del módulo vercel-ai-sdk-text-generation.
Dependencias
ai— el core del Vercel AI SDK (generateText,streamText,APICallError)@ai-sdk/google— el adaptador del proveedor Google (Gemini)
Estructura de archivos
types.ts Shared types, defaults, rate limit + context config, ConversationStore contract
service.ts Internal logic: calls the AI SDK, trims context, normalizes errors — do not modify
store.ts Conversation store registry — register your own persistence here
actions.ts Server Actions — import into Server Components
client.ts Client-side streaming helper — import into Client Components
stream/handler.ts Internal streaming logic + message persistence, used by the route handler
stream/route.ts Route handler — copy to app/api/ai-sdk/chat/stream/route.ts
.env.local Environment variable for the Google API key
Configuración
-
Implementá y registrá un
ConversationStore. Este módulo no tiene base de datos propia: la persistencia depende por completo de vos. Implementá la interfaz detypes.tsy registrala una sola vez, al iniciar la app (por ejemplo, en un módulo solo de servidor que se ejecute temprano):import { registerConversationStore } from "@/lib/ai-chat/store"; import type { ConversationStore } from "@/lib/ai-chat/types"; const myStore: ConversationStore = { async createConversation(title) { /* ... */ }, async getConversation(id) { /* ... */ }, async listConversations() { /* ... */ }, async renameConversation(id, title) { /* ... */ }, async deleteConversation(id) { /* ... */ }, async addMessage(message) { /* ... */ }, async getMessages(conversationId) { /* ... */ }, }; registerConversationStore(myStore);Hasta que se registre, cada action que toque conversaciones o mensajes devuelve un error
STORE_NOT_CONFIGUREDen lugar de romperse. -
Copiá
stream/route.tsa tu proyecto en:app/api/ai-sdk/chat/stream/route.ts(dejá
stream/handler.tsal lado, ya que la route lo importa por ruta relativa) -
Agregá tu API key de Google AI en
.env.local. Podés conseguir una en https://aistudio.google.com/apikey
Ejemplo de esquema de almacenamiento
ConversationStore no exige ninguna base de datos en particular, pero esta es la forma en la que está pensado: dos tablas, que coinciden con Conversation y Message de types.ts. Usalo como punto de partida al implementar la interfaz con Supabase, Prisma o cualquier otra cosa.
conversations
| columna | tipo | notas |
|---|---|---|
id | text/uuid | clave primaria |
title | text | |
created_at | timestamp | |
updated_at | timestamp | actualizar con cada mensaje nuevo |
messages
| columna | tipo | notas |
|---|---|---|
id | text/uuid | clave primaria |
conversation_id | text/uuid | clave foránea → conversations.id |
role | text | user | assistant | system |
content | text | |
created_at | timestamp |
Filas de ejemplo, para una conversación con un intercambio:
conversations
| id | title | created_at | updated_at |
|---|---|---|---|
conv_1 | "Trip to Japan" | 2026-09-10T10:00:00Z | 2026-09-10T10:00:05Z |
messages
| id | conversation_id | role | content | created_at |
|---|---|---|---|---|
msg_1 | conv_1 | user | "Best time to visit Kyoto?" | 2026-09-10T10:00:00Z |
msg_2 | conv_1 | assistant | "Spring (March–May) for cherry blossoms, or..." | 2026-09-10T10:00:05Z |
getMessages(conversationId) debería devolver todas las filas de ese conversation_id, en orden cronológico: service.ts se encarga de recortarlas hasta CONTEXT_CONFIG.maxMessages antes de llamar al modelo, así que no necesitás paginar ni limitar la consulta por esa razón.
Uso
Gestionar conversaciones (servidor)
import { createConversation, listConversations } from "@/lib/ai-chat/actions";
const created = await createConversation("Trip to Japan");
if (created.success) {
console.log(created.data.id);
}
const all = await listConversations();
Enviar un mensaje, sin streaming (servidor)
import { sendMessage } from "@/lib/ai-chat/actions";
const result = await sendMessage({
conversationId: "conv_1",
message: "What's the best time to visit Kyoto?",
system: "You are a helpful travel assistant.",
});
if (result.success) {
console.log(result.data.message.content);
} else {
console.error(result.error.message);
}
Enviar un mensaje con streaming (cliente)
"use client";
import { useState } from "react";
import { streamChat } from "@/lib/ai-chat/client";
export function ChatBox({ conversationId }: { conversationId: string }) {
const [reply, setReply] = useState("");
const [error, setError] = useState<string | null>(null);
async function handleSend(message: string) {
setReply("");
setError(null);
await streamChat(
{ conversationId, message },
{
onChunk: (chunk) => setReply((prev) => prev + chunk),
onError: (err) => setError(err.message),
}
);
}
return (
<div>
{error ? <p role="alert">{error}</p> : <p>{reply}</p>}
</div>
);
}
Tanto el mensaje del usuario como la respuesta completa del asistente se guardan en el ConversationStore automáticamente cuando termina el stream: no hace falta una llamada de guardado aparte desde el cliente.
Voz (no implementado)
transcribeAudio y synthesizeSpeech en actions.ts son stubs que siempre devuelven un error NOT_IMPLEMENTED. @ai-sdk/google no tiene modelos de speech-to-text ni de text-to-speech: los modelos de voz de Google solo se pueden usar desde el AI SDK a través de AI Gateway (@ai-sdk/gateway, un paquete aparte con su propia API key), o cambiando esas dos funciones a otro proveedor (por ejemplo, Whisper + TTS de OpenAI). Implementá alguna de esas opciones directamente en actions.ts si lo necesitás.
Manejo de errores
Mismo formato normalizado que el módulo text-generation:
{
code: "RATE_LIMITED" | "INVALID_REQUEST" | "PROVIDER_ERROR" | "STORE_NOT_CONFIGURED" | "NOT_IMPLEMENTED" | "UNKNOWN",
message: string, // safe to show in the UI
retryAfter?: number, // seconds, only present for RATE_LIMITED
}
Rate limiting
Mismo enfoque de dos capas que el módulo text-generation: un limitador local de ventana deslizante en memoria (RATE_LIMIT_CONFIG en types.ts, por defecto 20 requests/minuto por instancia del servidor) más el manejo normalizado de las respuestas 429 del propio proveedor.
Gestión de contexto
CONTEXT_CONFIG.maxMessages en types.ts (por defecto 20) limita cuántos mensajes previos se envían al modelo en cada turno. Es un simple límite por cantidad de mensajes, no un contador de tokens: ajustá el número directamente si tus conversaciones son largas y llegás a la ventana de contexto del modelo, o reemplazalo por una estrategia basada en tokens en trimContext de service.ts si necesitás más precisión.
Limitación conocida
Los errores del proveedor a mitad del streaming se incrustan como el último fragmento del texto transmitido, la misma simplificación que en el módulo text-generation: mirá el SETUP.md de ese módulo para más detalles.