Claude-skill-registry agent-builder-vercel-sdk
Build conversational AI agents using Vercel AI SDK + OpenRouter. Use when creating Next.js frontends with streaming UI, tool calling, and multi-provider support.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/agent-builder-vercel-sdk" ~/.claude/skills/majiayu000-claude-skill-registry-agent-builder-vercel-sdk && rm -rf "$T"
manifest:
skills/data/agent-builder-vercel-sdk/SKILL.mdsafety · automated scan (low risk)
This is a pattern-based risk scan, not a security review. Our crawler flagged:
- references .env files
- references API keys
Always read a skill's source content before installing. Patterns alone don't mean the skill is malicious — but they warrant attention.
source content
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) } }) }