cover

Guía completa de createUIMessageStream en AI SDK


Escuchar este post

Selecciona una voz y genera audio para escuchar este post

Una guía práctica para entender y usar createUIMessageStream del AI SDK, incluyendo cómo crear artefactos con streaming en tiempo real.


¿Qué es createUIMessageStream?

createUIMessageStream es una función del AI SDK que permite crear un ReadableStream para enviar mensajes y datos del servidor al cliente con capacidades avanzadas de streaming. Es especialmente útil cuando necesitas:

  • Enviar respuestas de IA en tiempo real
  • Transmitir datos personalizados junto con el texto
  • Crear experiencias como artefactos/documentos que se construyen progresivamente

Import

import { createUIMessageStream } from "ai";

Anatomía de createUIMessageStream

Parámetros principales

| Parámetro | Tipo | Descripción | |-----------|------|-------------| | execute | ({ writer }) => void | Función que recibe un writer para escribir chunks al stream | | onFinish | ({ messages }) => void | Callback cuando termina el streaming | | onError | (error) => string | Manejador de errores, retorna mensaje de error | | generateId | () => string | Función opcional para generar IDs personalizados | | originalMessages | UIMessage[] | Mensajes originales para modo persistencia |

Métodos del Writer

El objeto writer que recibes en execute tiene dos métodos principales:

  • writer.write(part) - Escribe un chunk de datos al stream
  • writer.merge(stream) - Fusiona otro ReadableStream (útil para integrar streamText)

Uso básico

El patrón más común es crear el stream, fusionar la respuesta de streamText, y retornarlo como Response:

import { createUIMessageStream, streamText, JsonToSseTransformStream } from "ai"; export async function POST(request: Request) { const stream = createUIMessageStream({ execute: ({ writer: dataStream }) => { // Crear el stream de texto con el modelo const result = streamText({ model: myProvider.languageModel("gpt-4"), system: "Eres un asistente útil", messages: conversationMessages, }); // Consumir el stream internamente result.consumeStream(); // Fusionar con el UI stream dataStream.merge( result.toUIMessageStream({ sendReasoning: true, }) ); }, generateId: () => crypto.randomUUID(), onFinish: async ({ messages }) => { // Guardar mensajes en base de datos await saveMessages({ messages }); }, onError: () => { return "Ocurrió un error inesperado"; }, }); // Retornar como Server-Sent Events return new Response(stream.pipeThrough(new JsonToSseTransformStream())); }

Enviando datos personalizados

La magia de createUIMessageStream está en poder enviar datos personalizados además del texto. Usas writer.write() con un objeto que tiene:

  • type: Identificador del tipo de dato (prefijo data- por convención)
  • data: Los datos a enviar
  • transient: Si es true, no se persiste en el historial de mensajes
dataStream.write({ type: "data-status", data: "Procesando tu solicitud...", transient: true, });

Creando artefactos con streaming

Los artefactos son documentos o piezas de contenido que se construyen progresivamente mientras el usuario observa. Aquí está el patrón completo:

Paso 1: Define los tipos de datos personalizados

Primero, define qué tipos de datos personalizados vas a enviar:

// lib/types.ts import type { UIMessage } from "ai"; export type CustomUIDataTypes = { textDelta: string; // Deltas de texto codeDelta: string; // Deltas de código imageDelta: string; // Deltas de imagen sheetDelta: string; // Deltas de hoja de cálculo id: string; // ID del artefacto title: string; // Título del artefacto kind: ArtifactKind; // Tipo de artefacto clear: null; // Señal para limpiar contenido finish: null; // Señal de finalización }; export type ChatMessage = UIMessage< MessageMetadata, CustomUIDataTypes, ChatTools >;

Paso 2: Crea la tool que genera el artefacto

// lib/ai/tools/create-document.ts import { tool, type UIMessageStreamWriter } from "ai"; import { z } from "zod"; type CreateDocumentProps = { session: Session; dataStream: UIMessageStreamWriter<ChatMessage>; }; export const createDocument = ({ session, dataStream }: CreateDocumentProps) => tool({ description: "Crea un documento para actividades de escritura o creación de contenido", inputSchema: z.object({ title: z.string(), kind: z.enum(["text", "code", "image", "sheet"]), }), execute: async ({ title, kind }) => { const id = crypto.randomUUID(); // 1. Envía metadata del artefacto dataStream.write({ type: "data-kind", data: kind, transient: true, }); dataStream.write({ type: "data-id", data: id, transient: true, }); dataStream.write({ type: "data-title", data: title, transient: true, }); // 2. Limpia contenido anterior dataStream.write({ type: "data-clear", data: null, transient: true, }); // 3. Genera el contenido (aquí llamarías a tu lógica) await generateContent({ id, title, kind, dataStream, session, }); // 4. Señala que terminó dataStream.write({ type: "data-finish", data: null, transient: true, }); return { id, title, kind, content: "Documento creado exitosamente", }; }, });

Paso 3: Genera contenido con streaming

async function generateContent({ dataStream, kind, title }) { // Ejemplo: generar texto progresivamente const result = await streamText({ model: myModel, prompt: `Genera contenido para: ${title}`, }); for await (const chunk of result.textStream) { dataStream.write({ type: "data-textDelta", data: chunk, transient: true, }); } }

Paso 4: Conecta la tool al stream principal

const stream = createUIMessageStream({ execute: ({ writer: dataStream }) => { const result = streamText({ model: myProvider.languageModel("gpt-4"), messages: conversationMessages, tools: { createDocument: createDocument({ session, dataStream }), // otras tools... }, }); result.consumeStream(); dataStream.merge(result.toUIMessageStream()); }, });

Patrones útiles

Stream vacío

Útil cuando necesitas retornar un stream que no hace nada:

const emptyStream = createUIMessageStream<ChatMessage>({ execute: () => {}, });

Restaurar un mensaje existente

Cuando necesitas enviar un mensaje que ya existe en la base de datos:

const restoredStream = createUIMessageStream<ChatMessage>({ execute: ({ writer }) => { writer.write({ type: "data-appendMessage", data: JSON.stringify(existingMessage), transient: true, }); }, });

Enviar información de uso/métricas

// Dentro del onFinish de streamText onFinish: async ({ usage }) => { dataStream.write({ type: "data-usage", data: { promptTokens: usage.promptTokens, completionTokens: usage.completionTokens, }, }); }

Flujo completo para artefactos

┌─────────────────────────────────────────────────────────────┐
│                        SERVIDOR                              │
├─────────────────────────────────────────────────────────────┤
│  1. createUIMessageStream crea el stream principal          │
│                         │                                    │
│  2. Se pasa dataStream (writer) a las tools                 │
│                         │                                    │
│  3. Tool escribe metadata:                                  │
│     - data-id                                               │
│     - data-kind                                             │
│     - data-title                                            │
│     - data-clear                                            │
│                         │                                    │
│  4. Handler escribe contenido progresivo:                   │
│     - data-textDelta                                        │
│     - data-codeDelta                                        │
│     - etc.                                                  │
│                         │                                    │
│  5. Tool señala fin: data-finish                            │
│                         │                                    │
│  6. Stream se transforma a SSE                              │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                        CLIENTE                               │
├─────────────────────────────────────────────────────────────┤
│  7. useChat recibe eventos SSE                              │
│                         │                                    │
│  8. Procesa cada tipo de data-*                             │
│                         │                                    │
│  9. Renderiza artefacto progresivamente                     │
└─────────────────────────────────────────────────────────────┘

Consideraciones importantes

  1. transient: true: Usa esto para datos que no deben persistir en el historial de mensajes (como deltas de contenido o señales de control).

  2. Orden de eventos: Envía siempre la metadata (id, kind, title) antes del contenido para que el cliente sepa cómo renderizar.

  3. Señal de finalización: Siempre envía data-finish cuando termines de generar el artefacto.

  4. Tipado fuerte: Define tus CustomUIDataTypes para tener autocompletado y validación de tipos.

  5. SSE Transform: No olvides usar JsonToSseTransformStream() al retornar la Response para que el cliente pueda consumir el stream correctamente.


Referencias

meta cover

Estilos de Salida en Claude Code: Guía Completa

Checa este otro Post

meta cover

3 Estilos de Salida Creativos que Realmente Marcan la Diferencia

Checa este otro Post

¡Nuevo curso!

Animaciones web con React + Motion 🧙🏻