@dariomvg/create
vercel-ai-sdkchatgeministreamingintegracion

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

  1. Implementá y registrá un ConversationStore. Este módulo no tiene base de datos propia: la persistencia depende por completo de vos. Implementá la interfaz de types.ts y 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_CONFIGURED en lugar de romperse.

  2. Copiá stream/route.ts a tu proyecto en:

    app/api/ai-sdk/chat/stream/route.ts
    

    (dejá stream/handler.ts al lado, ya que la route lo importa por ruta relativa)

  3. 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

columnatiponotas
idtext/uuidclave primaria
titletext
created_attimestamp
updated_attimestampactualizar con cada mensaje nuevo

messages

columnatiponotas
idtext/uuidclave primaria
conversation_idtext/uuidclave foránea → conversations.id
roletextuser | assistant | system
contenttext
created_attimestamp

Filas de ejemplo, para una conversación con un intercambio:

conversations

idtitlecreated_atupdated_at
conv_1"Trip to Japan"2026-09-10T10:00:00Z2026-09-10T10:00:05Z

messages

idconversation_idrolecontentcreated_at
msg_1conv_1user"Best time to visit Kyoto?"2026-09-10T10:00:00Z
msg_2conv_1assistant"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.