Vercel AI SDK Chat Template
WorkflowNext.js chat UI template using Vercel AI SDK with streaming and tool calling.
Vercel AI SDK Chat Template
Overview
This template provides a complete Next.js chat UI using the Vercel AI SDK. It includes streaming responses, tool calling, structured outputs, and a production-ready chat interface.
The Vercel AI SDK provides a unified API for calling any LLM, with excellent TypeScript support and seamless Next.js integration.
Prerequisites
- Node.js 18+
- An LLM provider API key (OpenAI, Anthropic, etc.)
Project Structure
vercel-ai-chat/
├── app/
│ ├── api/
│ │ └── chat/
│ │ └── route.ts # Chat API endpoint
│ ├── page.tsx # Main chat UI page
│ └── layout.tsx # Root layout
│ └── globals.css # Global styles
├── components/
│ └── chat-ui.tsx # Reusable chat component
├── lib/
│ └── tools.ts # Tool definitions
│ └── utils.ts # Utility functions
├── .env.local # Environment variables
├── next.config.js
├── package.json
└── tsconfig.json
Installation
# Create Next.js app
npx create-next-app@latest vercel-ai-chat --typescript --tailwind --app
# Install AI SDK and provider
cd vercel-ai-chat
npm install ai @ai-sdk/openai
# or for Anthropic:
# npm install ai @ai-sdk/anthropic
# Create .env.local
echo "OPENAI_API_KEY=sk-..." > .env.local
Configuration
Environment Variables
.env.local:
OPENAI_API_KEY=sk-your-openai-api-key
# or
ANTHROPIC_API_KEY=sk-ant-your-anthropic-api-key
Next.js Configuration
next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
// AI SDK works out of the box with Next.js App Router
};
module.exports = nextConfig;
Core Implementation
1. Chat API Route
app/api/chat/route.ts:
import { openai } from '@ai-sdk/openai';
import { streamText, convertToCoreMessages } from 'ai';
import { getWeather, searchWeb } from '@/lib/tools';
// Allow streaming responses up to 30 seconds
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openai('gpt-4o'),
messages: convertToCoreMessages(messages),
tools: {
getWeather,
searchWeb,
},
system: `You are a helpful AI assistant. Be concise and helpful.
If you need to use tools, explain what you're doing.
If you don't know something, say so honestly.`,
});
return result.toDataStreamResponse();
}
2. Tool Definitions
lib/tools.ts:
import { tool } from 'ai';
import { z } from 'zod';
export const getWeather = tool({
description: 'Get the current weather for a location',
parameters: z.object({
location: z.string().describe('The city name, e.g., "San Francisco"'),
}),
execute: async ({ location }) => {
// Replace with actual weather API call
// Example: const response = await fetch(`https://api.weather.com/...`);
return {
location,
temperature: 72,
condition: 'sunny',
humidity: 45,
};
},
});
export const searchWeb = tool({
description: 'Search the web for current information',
parameters: z.object({
query: z.string().describe('The search query'),
}),
execute: async ({ query }) => {
// Replace with actual search API (Brave, Serper, etc.)
return {
results: [
{ title: `Search result for: ${query}`, url: 'https://example.com' },
],
};
},
});
3. Chat UI Component
app/page.tsx:
'use client';
import { useChat } from 'ai/react';
import { useState } from 'react';
export default function ChatPage() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: '/api/chat',
onFinish: (message) => {
console.log('Finished streaming:', message);
},
onError: (error) => {
console.error('Chat error:', error);
},
});
return (
<div className="flex flex-col h-screen max-w-3xl mx-auto">
{/* Header */}
<header className="border-b p-4">
<h1 className="text-xl font-bold">AI Chat</h1>
</header>
{/* Messages */}
<div className="flex-1 overflow-y-auto p-4 space-y-4">
{messages.map((m) => (
<div
key={m.id}
className={`flex ${
m.role === 'user' ? 'justify-end' : 'justify-start'
}`}
>
<div
className={`max-w-[80%] rounded-lg p-4 ${
m.role === 'user'
? 'bg-blue-500 text-white'
: 'bg-gray-100 text-gray-900'
}`}
>
<div className="font-semibold text-sm mb-1">
{m.role === 'user' ? 'You' : 'AI'}
</div>
<div className="whitespace-pre-wrap">{m.content}</div>
{/* Show tool calls */}
{m.toolInvocations?.map((toolCall) => (
<div key={toolCall.toolCallId} className="mt-2 text-sm text-gray-500">
{toolCall.state === 'call' && (
<div>Calling {toolCall.toolName}...</div>
)}
{toolCall.state === 'result' && (
<div>
<strong>{toolCall.toolName}:</strong>
<pre className="mt-1 bg-gray-50 p-2 rounded text-xs overflow-auto">
{JSON.stringify(toolCall.result, null, 2)}
</pre>
</div>
)}
</div>
))}
</div>
</div>
))}
{isLoading && (
<div className="flex justify-start">
<div className="bg-gray-100 rounded-lg p-4">
<div className="animate-pulse">Thinking...</div>
</div>
</div>
)}
</div>
{/* Input */}
<form onSubmit={handleSubmit} className="border-t p-4">
<div className="flex gap-2">
<input
className="flex-1 border rounded-lg p-3 focus:outline-none focus:ring-2 focus:ring-blue-500"
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-500 text-white rounded-lg p-3 px-6 disabled:opacity-50"
>
Send
</button>
</div>
</form>
</div>
);
}
Advanced Features
Structured Outputs
// In your API route
import { streamObject } from 'ai';
import { z } from 'zod';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamObject({
model: openai('gpt-4o'),
messages,
schema: z.object({
summary: z.string(),
sentiment: z.enum(['positive', 'neutral', 'negative']),
keyPoints: z.array(z.string()),
}),
});
return result.toTextStreamResponse();
}
Multi-Model Support
import { createOpenAI } from '@ai-sdk/openai';
import { createAnthropic } from '@ai-sdk/anthropic';
import { createFallback } from '@ai-sdk/fallback';
const openai = createOpenAI({ apiKey: process.env.OPENAI_API_KEY });
const anthropic = createAnthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
const model = createFallback({
models: [openai('gpt-4o'), anthropic('claude-3-5-sonnet-20241022')],
fallbackStrategy: 'on-error',
});
RAG Integration
// lib/vector-store.ts
import { embed } from 'ai';
import { openai } from '@ai-sdk/openai';
export async function searchKnowledgeBase(query: string) {
const { embedding } = await embed({
model: openai('text-embedding-3-small'),
value: query,
});
// Query your vector database
// const results = await vectorDb.query({ embedding, k: 5 });
return results;
}
// In your chat route
import { searchKnowledgeBase } from '@/lib/vector-store';
const relevantDocs = await searchKnowledgeBase(userQuery);
const result = await streamText({
model: openai('gpt-4o'),
messages,
prompt: `Context: ${relevantDocs}\n\nQuestion: ${userQuery}`,
});
Styling
app/globals.css:
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #555;
}
Running the Project
npm run dev
Open http://localhost:3000 to see the chat UI.
Deployment
# Deploy to Vercel
vercel
# Or build for production
npm run build
npm run start
