Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Build conversational AI agents using Vercel AI SDK + OpenRouter. Use when creating Next.js frontends with streaming UI, tool calling, and multi-provider support.
.claude/skills/majiayu000-agent-builder-vercel-sdk/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | 88% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 125% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 177% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 158% | 0% |
Create streaming AI chat interfaces with minimal code using Vercel AI SDK and OpenRouter provider.
bashnpm install ai @openrouter/ai-sdk-provider zod
envOPENROUTER_API_KEY=sk-or-v1-... NEXT_PUBLIC_SITE_URL=http://localhost:3000
typescript// 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() }
typescriptimport { 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() }
typescript'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> ) }
typescript'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> ) }
typescriptconst 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 })
typescriptimport { 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 }
Based on: /Users/danielcarreon/Documents/AI/software/tldraw-agent/
typescript// 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 } } } }
typescriptimport { 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')
typescriptexport 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 } ) } }
typescriptimport { 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) }) })
typescriptconst 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) } }) }
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | 13,328 | 13,267 | -0% | 1 | 1 | 0% | 2,767 | 5,201 | +88% | 0 | 0 | — |
case-01 | fail→fail | 13,351 | 9,422 | -29% | 1 | 1 | 0% | 2,987 | 4,509 | +51% | 0 | 0 | — |
case-02 | fail→fail | 13,331 | 10,493 | -21% | 1 | 1 | 0% | 2,917 | 4,894 | +68% | 0 | 0 | — |
case-03 | pass→pass | 5,853 | 2,446 | -58% | 1 | 1 | 0% | 1,161 | 2,761 | +138% | 0 | 0 | — |
case-04 | fail→pass | 8,318 | 2,837 | -66% | 1 | 1 | 0% | 1,574 | 2,905 | +85% | 0 | 0 | — |
case-05 | pass→pass | 7,253 | 4,058 | -44% | 1 | 1 | 0% | 1,474 | 3,153 | +114% | 0 | 0 | — |
case-06 | pass→pass | 11,017 | 4,034 | -63% | 1 | 1 | 0% | 2,240 | 3,238 | +45% | 0 | 0 | — |
case-07 | pass→pass | 11,666 | 11,130 | -5% | 1 | 1 | 0% | 2,556 | 4,921 | +93% | 0 | 0 | — |
case-08 | pass→pass | 12,245 | 11,538 | -6% | 1 | 1 | 0% | 2,970 | 5,058 | +70% | 0 | 0 | — |
case-09 | fail→pass | 7,250 | 5,402 | -25% | 1 | 1 | 0% | 1,508 | 3,398 | +125% | 0 | 0 | — |
case-11 | fail→fail | 6,186 | 5,043 | -18% | 1 | 1 | 0% | 1,445 | 3,422 | +137% | 0 | 0 | — |
case-12 | pass→pass | 14,033 | 11,368 | -19% | 1 | 1 | 0% | 2,623 | 4,623 | +76% | 0 | 0 | — |
case-13 | fail→pass | 6,411 | 6,358 | -1% | 1 | 1 | 0% | 1,410 | 3,900 | +177% | 0 | 0 | — |
case-14 | pass→pass | 11,152 | 4,139 | -63% | 1 | 1 | 0% | 2,484 | 3,165 | +27% | 0 | 0 | — |
case-15 | pass→pass | 7,200 | 6,970 | -3% | 1 | 1 | 0% | 1,162 | 3,829 | +230% | 0 | 0 | — |
case-16 | fail→fail | 16,815 | 13,880 | -17% | 1 | 1 | 0% | 2,907 | 5,045 | +74% | 0 | 0 | — |
case-17 | fail→pass | 5,332 | 4,018 | -25% | 1 | 1 | 0% | 1,236 | 3,192 | +158% | 0 | 0 | — |
case-18 | fail→pass | 8,639 | 5,122 | -41% | 1 | 1 | 0% | 1,723 | 3,436 | +99% | 0 | 0 | — |
case-19 | pass→pass | 3,036 | 3,264 | +8% | 1 | 1 | 0% | 537 | 2,811 | +423% | 0 | 0 | — |
case-20 | pass→pass | 6,243 | 6,382 | +2% | 1 | 1 | 0% | 1,474 | 3,722 | +153% | 0 | 0 | — |
case-21 | pass→pass | 4,922 | 4,213 | -14% | 1 | 1 | 0% | 1,022 | 3,382 | +231% | 0 | 0 | — |
case-22 | pass→pass | 37,415 | 6,412 | -83% | 1 | 1 | 0% | 1,545 | 3,747 | +143% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +27 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.