Expert-level Vercel AI SDK v5 patterns for production chatbots. Use for: (1) Chat persistence with Drizzle/PostgreSQL, (2) Generative UI with typed tool parts, (3) Human-in-the-loop tool confirmations, (4) Custom data streaming with reconciliation, (5) Anthropic provider with extended thinking/reasoning, (6) Type-safe message metadata with token tracking. Covers advanced patterns only - assumes basic AI SDK knowledge. NOT for AI SDK v6.
/plugin marketplace add horuz-ai/claude-plugins/plugin install nextjs@horuzThis skill inherits all available tools. When active, it can use any tool Claude has access to.
cookbook/ai/tools.tscookbook/api/route.tscookbook/components/chat.tsxcookbook/components/generative-ui/confirmation-card.tsxcookbook/components/generative-ui/proposal-card.tsxcookbook/components/generative-ui/weather-card.tsxcookbook/db/actions.tscookbook/db/relations.tscookbook/db/schema.tscookbook/types/message.tscookbook/utils/message-mapping.tsreferences/anthropic-config.mdreferences/persistence.mdreferences/streaming.mdreferences/tools-and-generative-ui.mdreferences/types.mdProduction patterns for AI chatbots with persistence, generative UI, and streaming.
| Need | Reference |
|---|---|
| Database schema & persistence | references/persistence.md |
| Tools & generative UI | references/tools-and-generative-ui.md |
| Custom data streaming | references/streaming.md |
| Type definitions | references/types.md |
| Anthropic + reasoning | references/anthropic-config.md |
| Working examples | cookbook/ directory |
Client (useChat) → API Route (streamText) → DB (Drizzle)
↑ ↓
└── UIMessageStream ←┘
// Core
import { streamText, convertToModelMessages, UIMessage } from 'ai'
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai'
// Client
import { useChat } from '@ai-sdk/react'
import { DefaultChatTransport } from 'ai'
// Provider
import { anthropic } from '@ai-sdk/anthropic'
streamText().toUIMessageStreamResponse()createUIMessageStream() + writer.write()createUIMessageStream() + writer.merge(result.toUIMessageStream())executeaddToolOutputonToolCall handlermessageMetadatawriter.write()This skill uses agents instead of chats for all database tables, routes, and methods:
agents (not chats)agentId (not chatId)createAgent(), loadAgent(), deleteAgent()Follow feature-based architecture:
features/
└── agents/
├── data/
│ └── get-agent.ts
├── actions/
│ └── create-agent.ts
├── types/
│ └── message.ts
└── components/
├── server/
│ └── agent-messages.tsx
└── client/
└── chat-input.tsx
db/
├── schema.ts # Table definitions
├── relations.ts # Drizzle relations
└── actions.ts # DB operations (upsert, load, delete)
// Client
transport: new DefaultChatTransport({
api: '/api/agent',
prepareSendMessagesRequest: ({ messages, id }) => ({
body: { message: messages.at(-1), agentId: id }
})
})
// Server: Load history, append new message
const previous = await loadAgent(agentId)
const messages = [...previous, message]
return result.toUIMessageStreamResponse({
originalMessages: messages,
onFinish: async ({ messages }) => {
await upsertMessages({ agentId, messages })
}
})
const result = streamText({ ... })
result.consumeStream() // No await - ensures completion even on disconnect
return result.toUIMessageStreamResponse({ ... })
const tools = { myTool: tool({ ... }) } satisfies ToolSet
type MyTools = InferUITools<typeof tools>
type MyUIMessage = UIMessage<MyMetadata, MyDataParts, MyTools>
tool-${toolName} not generic tool-callid for reconciliation - same ID updates existing partonData - never in message.partssendStart: false when using custom start - avoid duplicate start eventsresult.consumeStream() - call without await for disconnect handling