Skip to content
kitn AI/UI

Vercel AI SDK

The Vercel AI SDK gives you streamText and a provider for any model; AI/UI renders the stream. Write one /api/chat route, bridge its text deltas to the OpenAI-style SSE that <kai-chat> reads, and you’re done.

streamText({ model, messages }) runs the model and returns a result that streams lazily. kai-chat posts its messages as { role, content } pairs, which streamText takes directly — no conversion needed. The result exposes textStream, an async iterable of string deltas, and that’s the primitive you re-frame into SSE.

AI/UI reads OpenAI-format SSE: lines of data: {choices:[{delta:{content}}]} ending with data: [DONE]. The SDK’s own toUIMessageStreamResponse() emits a different protocol, so wrap textStream into OpenAI frames yourself:

app/api/chat/route.ts
import { streamText } from 'ai';
export const maxDuration = 30; // allow long streaming responses
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: 'openai/gpt-4o', // AI Gateway id; needs AI_GATEWAY_API_KEY
messages,
});
const encoder = new TextEncoder();
const sse = new ReadableStream({
async start(controller) {
for await (const delta of result.textStream) {
const chunk = { choices: [{ delta: { content: delta } }] };
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
}
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
controller.close();
},
});
return new Response(sse, { headers: { 'Content-Type': 'text/event-stream' } });
}

streamText isn’t awaited — it returns synchronously and streams as you iterate textStream.

The front end is the same as every other backend: <kai-chat> posts to /api/chat and reads the SSE into messages. That reader loop is the Streaming recipe — reference it rather than rewriting it here.

A string id in creator/model-name form (like openai/gpt-4o) routes through Vercel AI Gateway with no provider package — authenticate with AI_GATEWAY_API_KEY, or keylessly with OIDC on Vercel. Change the id and the same route reaches a different model.

To pin one vendor, import its provider instead:

import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
const result = streamText({
model: openai('gpt-4o'), // needs OPENAI_API_KEY
messages,
});
PathModel valueEnv var
AI Gateway (string id)'openai/gpt-4o'AI_GATEWAY_API_KEY
Direct provideropenai('gpt-4o')OPENAI_API_KEY

streamText also surfaces tool calls (result.fullStream) and reasoning. They map onto the message contract: a tool call becomes an entry in a message’s tools[] whose state advances input-streaminginput-availableoutput-available (or output-error), and reasoning sets reasoning.text. The text-only SSE bridge above doesn’t carry them — to render them, emit extra frames from fullStream and read them on the browser. See the tool and reasoning components for what AI/UI draws.