---
name: vercel-ai-sdk
description: >
  Vercel AI SDK integration with Durable Streams. createDurableChatTransport()
  for useChat(), toDurableStreamResponse() for server-side streaming,
  resumable chat sessions with reconnectToStream(), read proxy pattern for
  auth. Load when building chat apps with Vercel AI SDK (@ai-sdk/react) and
  durable streams.
type: composition
library: durable-streams
library_version: "0.2.1"
requires:
  - getting-started
sources:
  - "durable-streams/durable-streams:packages/aisdk-transport/src/client.ts"
  - "durable-streams/durable-streams:packages/aisdk-transport/src/server.ts"
  - "durable-streams/durable-streams:packages/aisdk-transport/src/types.ts"
---

This skill builds on durable-streams/getting-started. Read it first for setup and offset basics.

# Durable Streams — Vercel AI SDK

Drop-in transport for `useChat()` that writes AI responses to durable streams.
Chat sessions survive page refreshes and can be resumed mid-generation.

## Setup

### Client

```typescript
import { useMemo } from "react"
import { useChat } from "@ai-sdk/react"
import { createDurableChatTransport } from "@durable-streams/aisdk-transport"

function Chat({ id, initialMessages }) {
  const transport = useMemo(
    () => createDurableChatTransport({ api: "/api/chat" }),
    []
  )

  const { messages, sendMessage, status } = useChat({
    id,
    messages: initialMessages,
    transport,
    resume: true, // reconnect to in-flight generation on page reload
  })
}
```

### Server — POST /api/chat

```typescript
import { streamText, convertToModelMessages } from "ai"
import { toDurableStreamResponse } from "@durable-streams/aisdk-transport"

export async function POST(request: Request) {
  const { messages, id } = await request.json()

  const result = streamText({
    model: openai("gpt-4o-mini"),
    messages: await convertToModelMessages(messages),
  })

  const streamPath = `chat/${id}/${crypto.randomUUID()}`
  await saveChat({ id, activeStreamId: streamPath })

  return toDurableStreamResponse({
    source: result.toUIMessageStream({
      originalMessages: messages,
      onFinish: ({ messages: finalMessages }) => {
        void saveChat({ id, messages: finalMessages, activeStreamId: null })
      },
    }),
    stream: {
      writeUrl: buildWriteStreamUrl(streamPath),
      readUrl: buildReadProxyUrl(request, streamPath), // never expose writeUrl
      headers: WRITE_HEADERS,
    },
  })
}
```

`mode: "immediate"` (default) returns `201` immediately; writes continue in background. Use `mode: "await"` when the runtime needs an active request to keep running.

### Reconnect endpoint — GET /api/chat/:id/stream

Required for `resume: true`. Returns the active stream URL or 204 if no generation is in flight:

```typescript
export async function GET(request, { params }) {
  const { id } = await params
  const chat = await loadChat(id)

  if (!chat?.activeStreamId) {
    return new Response(null, { status: 204 })
  }

  const streamUrl = buildReadProxyUrl(request, chat.activeStreamId)
  return Response.json(
    { streamUrl },
    { status: 200, headers: { Location: streamUrl } }
  )
}
```

The transport defaults to `${api}/${chatId}/stream`. Pass `reconnectApi` to override.

### Read proxy

Always proxy reads through an app route so write credentials stay server-side. See `examples/chat-aisdk/app/api/chat-stream/route.ts` for the full implementation. Pass the proxy URL as `readUrl` in `toDurableStreamResponse()`.

## Common Mistakes

### CRITICAL Not persisting activeStreamId for resume

Save the active stream path before returning and clear it in `onFinish`. Without it, the reconnect endpoint has nothing to return and `resume: true` silently fails. See the server setup above for the correct pattern.

Source: examples/chat-aisdk/app/api/chat/route.ts

### CRITICAL Exposing write URLs to the client

Wrong: omitting `readUrl` — defaults to `writeUrl`, leaking credentials in the `Location` header.
Fix: always set `readUrl` to a read proxy route.

Source: packages/aisdk-transport/src/server.ts

### HIGH Not using waitUntil on serverless runtimes

In `immediate` mode, the response returns before writes finish. Without `waitUntil`, serverless runtimes may kill the process and drop chunks.

Fix: pass `waitUntil: ctx.waitUntil.bind(ctx)` to `toDurableStreamResponse()`.

Source: packages/aisdk-transport/src/server.ts

### HIGH Not clearing activeStreamId on finish

A stale `activeStreamId` causes the reconnect endpoint to return a completed stream. Always clear it in `onFinish`. See the server setup above.

Source: examples/chat-aisdk/app/api/chat/route.ts

### MEDIUM Missing reconnect endpoint

`resume: true` calls `GET ${api}/${chatId}/stream` on mount. If this endpoint doesn't exist, reconnection fails silently with a 404. See the reconnect endpoint setup above.

Source: packages/aisdk-transport/src/client.ts

## See also

- [getting-started](../../client/skills/getting-started/SKILL.md) — Stream creation and reading basics
- [writing-data](../../client/skills/writing-data/SKILL.md) — Low-level append and IdempotentProducer
- [go-to-production](../../client/skills/go-to-production/SKILL.md) — Production readiness checklist

## Version

Targets @durable-streams/aisdk-transport v0.2.1.
