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.
The route
Section titled “The route”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:
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 browser side
Section titled “The browser side”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.
One route, any model
Section titled “One route, any model”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,});| Path | Model value | Env var |
|---|---|---|
| AI Gateway (string id) | 'openai/gpt-4o' | AI_GATEWAY_API_KEY |
| Direct provider | openai('gpt-4o') | OPENAI_API_KEY |
Tool calls and reasoning
Section titled “Tool calls and reasoning”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-streaming → input-available → output-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.
Next steps
Section titled “Next steps”- Connect any backend — the
messagescontract this route feeds. - Streaming — the reader loop on the browser side.
- Connect any model — the gateway pattern, end to end.