| name | vercel-ai-sdk |
| description | Vercel AI SDK: streaming AI responses to Next.js, useChat/useCompletion hooks, tool calls, generative UI, multi-provider routing (Claude, OpenAI, Gemini) |
Vercel AI SDK Skill
When to activate
- Building the frontend interface for an AI application in Next.js or React
- Streaming AI responses to a UI without managing SSE manually
- Implementing a chat interface with message history
- Calling AI tools and rendering their results as UI components
- Routing requests across multiple AI providers (Claude, OpenAI, Gemini)
- Building generative UI where the AI decides what component to render
When NOT to use
- Pure backend AI logic with no UI — use the Claude API skill directly
- Long-running autonomous agents with file system access — use the Claude Agent SDK
- When you need full control over the streaming protocol — raw fetch + SSE
Why Vercel AI SDK for frontend AI
The Vercel AI SDK handles what would otherwise take hundreds of lines of custom code: SSE parsing, stream concatenation, message state, abort controllers, tool result rendering, and provider switching. It is model-agnostic — you can route to Claude, OpenAI, or Gemini by changing one line.
Instructions
Installation
npm install ai @ai-sdk/anthropic
npm install ai @ai-sdk/openai
npm install ai @ai-sdk/anthropic @ai-sdk/openai @ai-sdk/google
Route handler (Next.js App Router)
import { anthropic } from '@ai-sdk/anthropic'
import { streamText } from 'ai'
export const maxDuration = 30
export async function POST(req: Request) {
const { messages } = await req.json()
const result = await streamText({
model: anthropic('claude-opus-4-7'),
system: 'You are a helpful assistant.',
messages,
maxTokens: 2048,
})
return result.toDataStreamResponse()
}
useChat hook — the main client hook
'use client'
import { useChat } from 'ai/react'
export function ChatInterface() {
const {
messages,
input,
handleInputChange,
handleSubmit,
isLoading,
stop,
reload,
error,
append,
} = useChat({
api: '/api/chat',
initialMessages: [],
onFinish: (message) => {
console.log('Final message:', message)
},
onError: (error) => {
console.error('Stream error:', error)
},
})
return (
<div className="flex flex-col h-screen">
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map(m => (
<div key={m.id} =` ${ === ? '' ''}`}>
{m.content}
))}
{isLoading && Thinking...}
{/* Input */}
Send
{isLoading && Stop}
)
}
Tool calls (AI decides to call functions)
import { anthropic } from '@ai-sdk/anthropic'
import { streamText, tool } from 'ai'
import { z } from 'zod'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = await streamText({
model: anthropic('claude-opus-4-7'),
messages,
tools: {
getWeather: tool({
description: 'Get current weather for a location',
parameters: z.object({
location: z.string().describe('City and country'),
unit: z.enum(['celsius', 'fahrenheit']).default('celsius'),
}),
execute: async ({ location, unit }) => {
const weather = await fetchWeather(location)
return { : weather., : weather., unit }
},
}),
: ({
: ,
: z.({
: z.(),
: z.().().().().(),
}),
: ({ query, limit }) => {
db..(query, limit)
},
}),
},
: ,
})
result.()
}
import { useChat } from 'ai/react'
function ChatWithTools() {
const { messages } = useChat({ api: '/api/chat' })
return (
<div>
{messages.map(m => (
<div key={m.id}>
{m.role === 'assistant' && m.content}
{/* Render tool invocations inline */}
{m.toolInvocations?.map(tool => (
<div key={tool.toolCallId}>
{tool.toolName === 'getWeather' && tool.state === 'result' && (
<WeatherCard data={tool.result} />
)}
</div>
))}
</div>
))}
</div>
)
}
generateText (non-streaming, server-side)
import { anthropic } from '@ai-sdk/anthropic'
import { generateText, generateObject } from 'ai'
import { z } from 'zod'
const { text } = await generateText({
model: anthropic('claude-opus-4-7'),
prompt: 'Summarize this article: ...',
})
const { object } = await generateObject({
model: anthropic('claude-opus-4-7'),
schema: z.object({
sentiment: z.enum(['positive', 'negative', 'neutral']),
confidence: z.number().min(0).max(1),
keywords: z.array(z.string()),
}),
prompt: 'Analyze the sentiment of: "I love this product but the delivery was slow"',
})
useCompletion hook (single turn, not chat)
'use client'
import { useCompletion } from 'ai/react'
function TextImprover() {
const { completion, complete, isLoading } = useCompletion({
api: '/api/improve',
})
return (
<div>
<textarea onChange={e => complete(e.target.value)} placeholder="Type to improve..." />
<div className="mt-4 p-4 bg-gray-50 rounded">
{completion || 'Improved text will appear here...'}
</div>
</div>
)
}
import { streamText } from 'ai'
import { anthropic } from '@ai-sdk/anthropic'
export async function POST(req: Request) {
const { prompt } = await req.json()
const result = await streamText({
model: anthropic('claude-opus-4-7'),
system: 'Improve the writing while keeping the same meaning. Be concise.',
prompt,
})
result.()
}
Multi-provider routing
import { anthropic } from '@ai-sdk/anthropic'
import { openai } from '@ai-sdk/openai'
import { google } from '@ai-sdk/google'
export function getModel(provider: 'anthropic' | 'openai' | 'google' = 'anthropic') {
switch (provider) {
case 'anthropic': return anthropic('claude-opus-4-7')
case 'openai': return openai('gpt-4o')
case 'google': return google('gemini-2.0-flash')
}
}
export async function POST(req: Request) {
const { messages, provider } = await req.json()
const result = await streamText({
model: getModel(provider),
messages,
})
result.()
}
Prompt caching with Claude (cost reduction)
import { anthropic } from '@ai-sdk/anthropic'
import { streamText } from 'ai'
const result = await streamText({
model: anthropic('claude-opus-4-7'),
messages: [
{
role: 'user',
content: [
{
type: 'text',
text: largeSystemDocument,
experimental_providerMetadata: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
},
{ type: 'text', text: 'Summarize section 3.' },
],
},
],
})
Example
User: Build an AI customer support chat for an e-commerce site — Claude can look up orders and product info via tools, stream the response, and the UI shows a typing indicator and tool results inline.
Expected output:
app/api/chat/route.ts — streamText with lookupOrder + searchProducts tools, maxSteps: 3
components/ChatWindow.tsx — useChat hook, messages list, tool result cards for orders/products
components/OrderCard.tsx — renders when toolInvocations[].toolName === 'lookupOrder'
app/support/page.tsx — wraps ChatWindow with layout