# Vercel AI SDK

Run streamText on your server, re-frame its text deltas as OpenAI-style SSE, and let AI/UI render the stream — one route, any model.

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

`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:

```ts
// app/api/chat/route.ts

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

> **caution:** `toUIMessageStreamResponse()` and `toTextStreamResponse()` don't emit OpenAI's chat-completions SSE. The first uses the SDK's own data-stream protocol; the second is raw text with no framing. Re-frame `textStream` as above so AI/UI's parser can read it.

## 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](/guides/recipes/streaming/) — reference it rather than rewriting it here.

## 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:

```ts

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` |

> **note:** A bare `'gpt-4o'` won't route through the gateway — the slash form is what triggers it. Use `'creator/model-name'` for the gateway, or an explicit provider instance otherwise.

## 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](/components/tool/) and [reasoning](/components/reasoning/) components for what AI/UI draws.

## Next steps

- **[Connect any backend](/integrations/connect-any-backend/)** — the `messages` contract this route feeds.
- **[Streaming](/guides/recipes/streaming/)** — the reader loop on the browser side.
- **[Connect any model](/integrations/connect-any-model/)** — the gateway pattern, end to end.
