Vercel AI SDK Agent Builder
Purpose
Create streaming AI chat interfaces with minimal code using Vercel AI SDK and OpenRouter provider.
When to Use
-
Building Next.js frontend with chat UI
-
Need streaming responses with SSE
-
Want type-safe tool calling in TypeScript
-
Switching between multiple AI providers
-
Building agentic loops with stopWhen/prepareStep
Quick Start
Installation
npm install ai @openrouter/ai-sdk-provider zod
Environment Variables
OPENROUTER_API_KEY=sk-or-v1-... NEXT_PUBLIC_SITE_URL=http://localhost:3000
Backend Setup (Route Handler)
Basic Chat Endpoint
// app/api/chat/route.ts import { OpenRouter } from '@openrouter/ai-sdk-provider' import { streamText } from 'ai'
const openrouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY })
export async function POST(req: Request) { const { messages } = await req.json()
const result = streamText({ model: openrouter('openai/gpt-4o'), system: 'You are a helpful assistant', messages, })
return result.toDataStreamResponse() }
With Tool Calling
import { z } from 'zod' import { tool } from 'ai'
const tools = { generateImage: tool({ description: 'Generate images using AI', parameters: z.object({ prompt: z.string().describe('Image description'), numImages: z.number().min(1).max(10).default(1) }), execute: async ({ prompt, numImages }) => { // Your implementation const images = await generateImages(prompt, numImages) return { images } } }) }
export async function POST(req: Request) { const { messages } = await req.json()
const result = streamText({ model: openrouter('openai/gpt-4o'), system: 'You are a helpful assistant', messages, tools, maxSteps: 5 // Enable agentic loop })
return result.toDataStreamResponse() }
Frontend Integration
Using useChat Hook
'use client'
import { useChat } from 'ai/react'
export default function Chat() { const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return ( <div className="flex flex-col h-screen"> {/* Messages */} <div className="flex-1 overflow-y-auto p-4"> {messages.map(m => ( <div key={m.id} className={m.role === 'user' ? 'text-right' : 'text-left'}> <div className="inline-block p-3 rounded-lg"> {m.content} </div> </div> ))} </div>
{/* Input */}
<form onSubmit={handleSubmit} className="p-4 border-t">
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
disabled={isLoading}
className="w-full px-4 py-2 border rounded"
/>
</form>
</div>
) }
With Tool Results Display
'use client'
import { useChat } from 'ai/react'
export default function ChatWithTools() { const { messages, input, handleInputChange, handleSubmit } = useChat()
return ( <div> {messages.map(m => ( <div key={m.id}> {m.content}
{/* Display tool calls */}
{m.toolInvocations?.map(tool => (
<div key={tool.toolCallId} className="bg-gray-100 p-2 rounded">
<strong>{tool.toolName}</strong>
{tool.state === 'result' && (
<pre>{JSON.stringify(tool.result, null, 2)}</pre>
)}
</div>
))}
</div>
))}
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
</form>
</div>
) }
Advanced Patterns
Multi-Step Agentic Loop
const result = streamText({ model: openrouter('openai/gpt-4o'), messages, tools, maxSteps: 5,
// Control loop behavior
onStepFinish: ({ stepType, text, toolCalls }) => {
console.log(Step finished: ${stepType})
},
// Stop condition experimental_continueSteps: true })
Custom Streaming with streamUI
import { streamUI } from 'ai/rsc'
export async function generateUI(prompt: string) { const result = streamUI({ model: openrouter('openai/gpt-4o'), prompt, text: ({ content }) => <p>{content}</p>, tools: { showImage: { description: 'Display an image', parameters: z.object({ url: z.string() }), generate: async ({ url }) => <img src={url} /> } } })
return result.value }
tldraw Agent Pattern
Based on: /Users/danielcarreon/Documents/AI/software/tldraw-agent/
// Incremental JSON parsing pattern async function* streamActions(model, prompt) { const { textStream } = streamText({ model, system: systemPrompt, messages, maxOutputTokens: 8192, temperature: 0 })
let buffer = '{"actions": [{"_type":'
for await (const text of textStream) { buffer += text
// Parse incremental JSON
const partialObject = closeAndParseJson(buffer)
if (!partialObject) continue
const actions = partialObject.actions
if (!Array.isArray(actions)) continue
// Yield actions as they complete
for (const action of actions) {
if (action.complete) {
yield action
}
}
} }
OpenRouter Provider Setup
import { OpenRouter } from '@openrouter/ai-sdk-provider'
const openrouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY, // Optional: customize baseURL: 'https://openrouter.ai/api/v1', headers: { 'HTTP-Referer': process.env.NEXT_PUBLIC_SITE_URL, 'X-Title': 'My App' } })
// Use different models const gpt4 = openrouter('openai/gpt-4o') const claude = openrouter('anthropic/claude-3-5-sonnet') const gemini = openrouter('google/gemini-2.0-flash-exp')
Error Handling
export async function POST(req: Request) { try { const { messages } = await req.json()
const result = streamText({
model: openrouter('openai/gpt-4o'),
messages,
onError: (error) => {
console.error('Stream error:', error)
}
})
return result.toDataStreamResponse()
} catch (error) { return new Response( JSON.stringify({ error: error.message }), { status: 500 } ) } }
Testing
import { streamText } from 'ai' import { OpenRouter } from '@openrouter/ai-sdk-provider'
describe('Chat API', () => { it('should stream response', async () => { const openrouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY })
const result = streamText({
model: openrouter('openai/gpt-4o'),
prompt: 'Say hello'
})
const chunks = []
for await (const chunk of result.textStream) {
chunks.push(chunk)
}
expect(chunks.length).toBeGreaterThan(0)
}) })
Best Practices
-
Type Safety: Use Zod for tool parameters
-
Error Boundaries: Wrap chat UI in ErrorBoundary
-
Loading States: Show loading UI during streaming
-
Optimistic Updates: Update UI before server response
-
Tool Results: Display tool executions to user
-
Rate Limiting: Implement rate limits on API routes
-
Context Management: Limit message history to avoid token overflow
Common Patterns
Image Generation Agent
const tools = { generateAvatar: tool({ description: 'Generate avatar with DANI identity', parameters: z.object({ prompt: z.string(), numImages: z.number().default(3) }), execute: async ({ prompt, numImages }) => { const response = await fetch('/api/generate', { method: 'POST', body: JSON.stringify({ prompt, numImages }) }) return await response.json() } }),
combineImages: tool({ description: 'Combine multiple images', parameters: z.object({ imageUrls: z.array(z.string()), prompt: z.string() }), execute: async ({ imageUrls, prompt }) => { // Nano Banana integration return await combineWithNanoBanana(imageUrls, prompt) } }) }
Resources
-
Vercel AI SDK Docs
-
OpenRouter Provider
-
Tool Calling Guide