Great job take all of the information we have covered so far and create a full file.
Keep in mind the tone you saw from the transcript. Be concise in code snippets while also giving information about them.
Additionally if it helps use the transcipt as your guide on how the conversation would flow. Niko for example speaks well when talking about drizzle and SQL
complete-ai-sdk-guide.md
It's almost identical to how we described it in English! And remember how I said we could switch providers with one line? Here's the same call with Google's Gemini:
const { text } = await generateText({
model: google('gemini-pro'), // Just this line changed!
prompt: 'Tell me a joke'
})That's the power of a unified API. But let's dive deeper into why this matters so much.
Look, this isn't just about convenience. This is about strategic flexibility. Before the AI SDK, switching AI providers was like switching databases - a massive undertaking that could take weeks.
Here's what the old world looked like:
// OpenAI - completely different API
const openaiResponse = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
temperature: 0.7,
max_tokens: 1000,
})
const openaiText = openaiResponse.choices[0].message.content
// Anthropic - totally different structure
const anthropicResponse = await anthropic.messages.create({
model: "claude-3-sonnet-20240229",
max_tokens: 1000,
temperature: 0.7,
messages: [{ role: "user", content: prompt }],
})
const anthropicText = anthropicResponse.content[0].textNow with the AI SDK, it's the same interface everywhere:
// Same interface, just change the model
const openaiResult = await generateText({
model: openai('gpt-4o'),
prompt,
})
const anthropicResult = await generateText({
model: anthropic('claude-3-sonnet-20240229'),
prompt,
})
const xaiResult = await generateText({
model: xai('grok-beta'),
prompt,
})But we can take this further. Here's a pattern I love - building a provider management system that handles fallbacks automatically:
// lib/ai-providers.ts
export const modelConfigs = {
"gpt-4o": {
provider: "openai",
model: "gpt-4o",
supportsVision: true,
supportsTools: true,
costPerToken: 0.00003,
},
"claude-3-sonnet": {
provider: "anthropic",
model: "claude-3-sonnet-20240229",
supportsVision: true,
supportsTools: true,
costPerToken: 0.000015,
},
"grok-beta": {
provider: "xai",
model: "grok-beta",
supportsVision: false,
supportsTools: true,
costPerToken: 0.000002,
},
}
export function getModelInstance(configKey: string) {
const config = modelConfigs[configKey]
switch (config.provider) {
case "openai": return openai(config.model)
case "anthropic": return anthropic(config.model)
case "xai": return xai(config.model)
}
}
// Now you can switch models based on requirements
export function selectOptimalModel(requirements: {
needsVision?: boolean
maxCost?: number
priority: "speed" | "cost" | "quality"
}) {
// Smart model selection logic
const candidates = Object.entries(modelConfigs).filter(([_, caps]) => {
if (requirements.needsVision && !caps.supportsVision) return false
if (requirements.maxCost && caps.costPerToken > requirements.maxCost) return false
return true
})
// Return the best match based on priority
return candidates[0][0]
}This is powerful because now you can optimize different parts of your application with different models without changing your core logic.
And let's talk about streaming - because this isn't just "nice to have" anymore. Users expect real-time responses. The difference between generateText and streamText is literally just the function name:
// Generate all at once
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing'
})
// Stream in real-time
const result = streamText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing'
})
for await (const delta of result.textStream) {
process.stdout.write(delta) // Real-time output!
}But in a React application, you don't even need to handle the streaming manually. The useChat hook does all the heavy lifting:
// components/chat.tsx
"use client"
import { useChat } from 'ai/react'
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div>
{messages.map(message => (
<div key={message.id} className={`message ${message.role}`}>
<strong>{message.role}:</strong> {message.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
disabled={isLoading}
/>
<button type="submit">Send</button>
</form>
</div>
)
}That useChat hook is managing conversation state, handling streaming responses, clearing inputs, error states - all the gnarly stuff you'd otherwise build from scratch every time.
Here's something that's really important to understand: AI responses are non-deterministic. You can't predict exactly what you'll get or how long it'll take. This creates unique state management challenges:
// hooks/use-ai-stream.ts
export interface StreamState {
content: string
isStreaming: boolean
error: string | null
isComplete: boolean
}
export function useAIStream(modelKey = "gpt-4o") {
const [state, setState] = useState<StreamState>({
content: "",
isStreaming: false,
error: null,
isComplete: false,
})
const startStream = useCallback(async (prompt: string) => {
setState({ content: "", isStreaming: true, error: null, isComplete: false })
try {
const model = getModelInstance(modelKey)
const result = streamText({ model, prompt })
let accumulatedContent = ""
for await (const delta of result.textStream) {
accumulatedContent += delta
setState(prev => ({ ...prev, content: accumulatedContent }))
}
setState(prev => ({ ...prev, isStreaming: false, isComplete: true }))
} catch (error) {
setState(prev => ({
...prev,
isStreaming: false,
error: error.message
}))
}
}, [modelKey])
return { ...state, startStream }
}This hook handles all the complexity of streaming state management, so your components can focus on the UI.
Here's what's wild - prompt engineering isn't just a buzzword anymore. It's becoming as fundamental as knowing SQL or understanding REST APIs. The difference is, with SQL you're querying structured data. With prompts, you're querying intelligence itself.
And honestly, one of the things that language models have brought to the surface is how badly most of us are at communicating. This is a tool, and like any tool, you need to be really intentional with how you use it.
Let me show you what good prompting looks like. A good prompt has four parts:
Here's a real example:
const promptTemplates = {
codeReview: `You are a senior software engineer conducting a code review.
Analyze the following code and provide:
1. Potential issues or bugs
2. Performance improvements
3. Best practice recommendations
4. Security considerations
Code to review:
{code}
Provide your feedback in a structured format.`,
userStoryGeneration: `Convert the following feature description into proper user stories using the format:
"As a [user type], I want [functionality] so that [benefit]"
Include:
- Main user story
- 2-3 acceptance criteria
- Edge cases to consider
Feature description:
{feature}`,
}
export function applyPromptTemplate(
template: string,
variables: Record<string, string>
): string {
return template.replace(/{(\w+)}/g, (match, key) => variables[key] || match)
}These templates become reusable intelligence patterns. You can version control your AI interactions the same way you version control your code. That's a paradigm shift.
We're also seeing completely new UX patterns emerge. Users need to understand when the AI is thinking, when it's generating, when it might fail. Here are some patterns I've found essential:
// components/ai-ux-patterns.tsx
export function AILoadingStates() {
return (
<div className="space-y-4">
{/* Thinking State */}
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>AI is thinking...</span>
</div>
{/* Progressive Disclosure */}
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-green-500" />
<span>Analyzing your request</span>
</div>
<div className="flex items-center gap-2">
<Loader2 className="w-4 h-4 animate-spin" />
<span>Generating response...</span>
</div>
</div>
{/* Streaming Indicator */}
<div className="relative">
<div className="whitespace-pre-wrap">
{streamingText}
<span className="inline-block w-2 h-5 bg-blue-500 animate-pulse ml-1" />
</div>
</div>
</div>
)
}This isn't just "nice UX" - this is AI-specific UX. It's building empathy into our interfaces for AI processes.
Frontend engineers who master this aren't just adding a skill - they're becoming AI-native developers. Here are the specializations I'm seeing emerge:
The question isn't whether AI will change frontend development. The question is: are you ready to be part of that change?
Let me show you something really cool. This is a pattern we use in production at Vercel for the Next.js repo. We get thousands of GitHub issues every week, and we need to classify them automatically.
Here's how you'd traditionally approach this - with just text generation:
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
const supportRequests = [
"I'm having trouble logging into my account, can you please assist?",
"The export feature isn't working correctly",
"How do I upgrade my plan to Enterprise?",
// ... more requests
]
const { text } = await generateText({
model: openai('gpt-4o-mini'),
prompt: `Classify the following support requests into categories:
billing, product-issues, enterprise-sales, account-issues, product-feedback
${JSON.stringify(supportRequests)}`
})
console.log(text) // Gets messy, unstructured outputThe problem? You get back unstructured text that's hard to work with. But here's where the AI SDK gets really magical - structured object generation:
import { generateObject } from 'ai'
import { z } from 'zod'
const classificationSchema = z.object({
request: z.string(),
category: z.enum(['billing', 'product-issues', 'enterprise-sales', 'account-issues', 'product-feedback']),
urgency: z.enum(['low', 'medium', 'high']),
language: z.string().describe('The language of the support request (full name like "English", "Spanish")')
})
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: classificationSchema,
prompt: 'Classify this support request',
output: 'array' // Get an array of classified requests
})
// Now you have fully typed, structured data!
object.forEach(item => {
console.log(`${item.category} - ${item.urgency}`) // Fully typed!
})This is honestly magical. We've moved from unstructured text to fully typed, validated data structures. And that .describe() function on the Zod schema? That's giving the AI more context about exactly what we want for that field.
Here's something that blew my mind when I first built it - a chat interface that handles text, images, and files all in one conversation:
// app/api/multi-modal-chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o'), // Vision-capable model
messages: messages.map(message => ({
role: message.role,
content: message.content.map(part => {
if (part.type === "text") {
return { type: "text", text: part.text }
}
if (part.type === "image") {
return { type: "image", image: part.image }
}
return part
}),
})),
})
return result.toDataStreamResponse()
}And on the frontend:
// components/multi-modal-chat.tsx
export default function MultiModalChat() {
const [attachments, setAttachments] = useState<Attachment[]>([])
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/multi-modal-chat'
})
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
const content = []
if (input.trim()) content.push({ type: "text", text: input })
attachments.forEach(attachment => {
if (attachment.type === "image") {
content.push({ type: "image", image: attachment.base64 })
}
})
handleSubmit(e, { data: { content } })
}
return (
<div>
{/* Messages display */}
{messages.map(message => (
<div key={message.id}>{message.content}</div>
))}
{/* File upload and input */}
<form onSubmit={onSubmit}>
<input type="file" onChange={handleFileUpload} accept="image/*" />
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
)
}What's incredible here is that text, images, and files are all just content types now. The AI doesn't care if you're sending it a paragraph or a screenshot. That's revolutionary.
RAG (Retrieval-Augmented Generation) is like giving the AI perfect memory of your business. Here's a simplified but powerful implementation:
// lib/vector-store.ts
import { embed, generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
export class SimpleVectorStore {
private documents: Document[] = []
async addDocument(doc: { id: string, content: string, metadata: any }) {
// Generate embedding for the document
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: doc.content,
})
this.documents.push({ ...doc, embedding })
}
async similaritySearch(query: string, limit = 5) {
// Generate embedding for the query
const { embedding: queryEmbedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
})
// Find most similar documents
const similarities = this.documents.map(doc => ({
document: doc,
similarity: this.cosineSimilarity(queryEmbedding, doc.embedding),
}))
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit)
.map(item => item.document)
}
async generateAnswer(query: string) {
// Find relevant documents
const relevantDocs = await this.similaritySearch(query, 3)
// Create context from relevant documents
const context = relevantDocs
.map(doc => `Source: ${doc.metadata.title}\n${doc.content}`)
.join('\n\n')
// Generate answer with context
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: `Answer the question based on the provided context.
Context:
${context}
Question: ${query}
Answer:`,
})
return { answer: text, sources: relevantDocs }
}
}This is contextual AI. The AI isn't just generating generic responses - it's generating responses based on YOUR data, YOUR documents, YOUR business context.
Here's where things get really exciting - AI that can actually DO things, not just generate text:
// lib/ai-tools.ts
import { tool } from 'ai'
import { z } from 'zod'
export const tools = {
calculator: tool({
description: 'Perform mathematical calculations',
parameters: z.object({
expression: z.string().describe('Mathematical expression to evaluate'),
}),
execute: async ({ expression }) => {
try {
const result = Function(`"use strict"; return (${expression})`)()
return { result: result.toString(), expression }
} catch (error) {
return { error: 'Invalid expression', expression }
}
},
}),
getWeather: tool({
description: 'Get current weather for a location',
parameters: z.object({
location: z.string().describe('City name or location'),
}),
execute: async ({ location }) => {
// In real app, call weather API
return {
location,
temperature: '22°C',
condition: 'sunny',
humidity: '65%'
}
},
}),
scheduleEvent: tool({
description: 'Schedule an event or reminder',
parameters: z.object({
title: z.string(),
date: z.string().describe('Date in YYYY-MM-DD format'),
time: z.string().describe('Time in HH:MM format'),
}),
execute: async ({ title, date, time }) => {
// In real app, integrate with calendar API
return {
eventId: Math.random().toString(36),
message: `Event "${title}" scheduled for ${date} at ${time}`,
status: 'confirmed'
}
},
}),
}
// Using tools in your AI application
const { text, toolCalls } = await generateText({
model: openai('gpt-4o'),
prompt: 'What\'s 15% of 2,450 plus 300? Also, what\'s the weather in Tokyo?',
tools,
maxToolRoundtrips: 3,
})Each tool represents a capability extension. You're not just building one AI feature - you're building an ecosystem of AI capabilities that can work together.
I like to think of the AI SDK as an ORM for LLMs. Just like Drizzle abstracts database differences while letting you write SQL, the AI SDK abstracts provider differences while letting you write natural language.
// One interface, any provider
const providers = [
{ name: 'OpenAI', model: openai('gpt-4o') },
{ name: 'Anthropic', model: anthropic('claude-3-sonnet-20240229') },
{ name: 'Google', model: google('gemini-pro') },
{ name: 'xAI', model: xai('grok-beta') },
]
// Test the same prompt across all providers
for (const provider of providers) {
const { text } = await generateText({
model: provider.model,
prompt: 'Explain quantum computing in simple terms',
})
console.log(`${provider.name}: ${text.slice(0, 100)}...`)
}This is provider independence. You're not locked into any single AI company's ecosystem. You can mix and match based on what works best for each use case.
The streaming implementation works identically across frameworks:
// React
const { messages, input, handleSubmit } = useChat()
// Vue
const { messages, input, handleSubmit } = useChat()
// Svelte
const { messages, input, handleSubmit } = useChat()Same hook, same API, different framework. This is ecosystem unification - the AI layer is becoming as universal as HTTP.
This is probably my favorite feature. You can force the AI to return data that fits a predefined schema:
const recipeSchema = z.object({
name: z.string(),
ingredients: z.array(z.object({
name: z.string(),
amount: z.string(),
})),
steps: z.array(z.string()),
cookingTime: z.number(),
difficulty: z.enum(['easy', 'medium', 'hard']),
})
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: recipeSchema,
prompt: 'Generate a lasagna recipe',
})
// object is fully typed!
console.log(object.name) // string
console.log(object.ingredients) // Array<{name: string, amount: string}>
console.log(object.difficulty) // 'easy' | 'medium' | 'hard'This represents reliable AI. The output isn't just text anymore - it's structured, typed, validated data that integrates seamlessly with your existing systems.
For production applications, you need observability and control:
const result = await generateText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing',
experimental_transform: {
transformRequestBody: (body) => {
// Log requests
console.log('AI Request:', {
model: body.model,
timestamp: new Date().toISOString()
})
// Add safety instructions
if (body.messages) {
body.messages.unshift({
role: 'system',
content: 'Follow content safety guidelines. Be helpful, harmless, and honest.'
})
}
return body
},
transformResponseBody: (body, request) => {
// Log performance
const duration = Date.now() - request._startTime
console.log('Performance:', {
duration: `${duration}ms`,
tokensUsed: body.usage?.total_tokens
})
// Calculate costs
const cost = (body.usage?.total_tokens || 0) * 0.00003
console.log('Cost:', `$${cost.toFixed(6)}`)
return body
},
},
})This is production-ready AI - logging, performance monitoring, cost tracking, content filtering - all the operational concerns you need.
AI systems can fail in unique ways. Here's how to build resilient applications:
class ResilientAI {
private providers = [
{ name: 'openai', model: openai('gpt-4o'), priority: 1 },
{ name: 'anthropic', model: anthropic('claude-3-sonnet-20240229'), priority: 2 },
{ name: 'xai', model: xai('grok-beta'), priority: 3 },
]
async generateWithFallback(prompt: string, options = {}) {
const { maxRetries = 3, timeout = 30000 } = options
const errors = []
for (const provider of this.providers) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
const result = await generateText({
model: provider.model,
prompt,
abortSignal: controller.signal,
})
clearTimeout(timeoutId)
return { ...result, provider: provider.name, attempt }
} catch (error) {
errors.push({ provider: provider.name, error: error.message })
// Exponential backoff
if (attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
)
}
}
}
}
throw new Error(`All providers failed: ${JSON.stringify(errors)}`)
}
}class AIRateLimiter {
private requests = new Map<string, number[]>()
private costs = new Map<string, { amount: number, resetTime: number }>()
canMakeRequest(userId: string, limits = {}) {
const { requestsPerMinute = 10, costPerHour = 1.0 } = limits
const now = Date.now()
const userRequests = this.requests.get(userId) || []
const recentRequests = userRequests.filter(time => now - time < 60 * 1000)
if (recentRequests.length >= requestsPerMinute) {
return {
allowed: false,
reason: 'Rate limit exceeded',
resetTime: Math.min(...recentRequests) + 60 * 1000
}
}
// Check cost limits...
return { allowed: true }
}
recordRequest(userId: string, cost = 0) {
const now = Date.now()
const userRequests = this.requests.get(userId) || []
userRequests.push(now)
this.requests.set(userId, userRequests)
if (cost > 0) {
const userCosts = this.costs.get(userId) || { amount: 0, resetTime: now }
userCosts.amount += cost
this.costs.set(userId, userCosts)
}
}
}// Response caching
class AIResponseCache {
private cache = new Map<string, CacheEntry>()
get(prompt: string, modelKey: string): string | null {
const key = `${modelKey}:${btoa(prompt).slice(0, 50)}`
const entry = this.cache.get(key)
if (!entry || Date.now() - entry.timestamp > entry.ttl) {
this.cache.delete(key)
return null
}
return entry.response
}
set(prompt: string, modelKey: string, response: string, ttl = 5 * 60 * 1000) {
const key = `${modelKey}:${btoa(prompt).slice(0, 50)}`
this.cache.set(key, {
response,
timestamp: Date.now(),
ttl,
})
}
}
// Lazy loading providers
const loadProvider = async (providerName: string) => {
switch (providerName) {
case 'openai':
const { openai } = await import('@ai-sdk/openai')
return openai
case 'anthropic':
const { anthropic } = await import('@ai-sdk/anthropic')
return anthropic
// Reduces initial bundle size
}
}Alright, let's build something! I'm going to walk you through creating a support ticket classifier - the same pattern we use in production at Vercel.
npm create next-app@latest my-ai-app
cd my-ai-app
npm install ai @ai-sdk/openai zodAdd your OpenAI API key to .env.local:
OPENAI_API_KEY=your_api_key_here// app/api/classify/route.ts
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const classificationSchema = z.object({
request: z.string(),
category: z.enum(['billing', 'technical', 'sales', 'general']),
priority: z.enum(['low', 'medium', 'high', 'urgent']),
sentiment: z.enum(['positive', 'neutral', 'negative']),
})
export async function POST(req: Request) {
const { requests } = await req.json()
try {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: classificationSchema,
prompt: `Classify these support requests:
Categories: billing, technical, sales, general
Priority: low, medium, high, urgent
Sentiment: positive, neutral, negative
Requests: ${JSON.stringify(requests)}`,
output: 'array',
})
return Response.json({ classifications: object })
} catch (error) {
return Response.json({ error: 'Classification failed' }, { status: 500 })
}
}// app/page.tsx
'use client'
import { useState } from 'react'
export default function Home() {
const [requests, setRequests] = useState('')
const [classifications, setClassifications] = useState([])
const [loading, setLoading] = useState(false)
const handleClassify = async () => {
setLoading(true)
const requestList = requests.split('\n').filter(r => r.trim())
const response = await fetch('/api/classify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requests: requestList }),
})
const data = await response.json()
setClassifications(data.classifications)
setLoading(false)
}
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">AI Support Ticket Classifier</h1>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium mb-2">
Support Requests (one per line):
</label>
<textarea
value={requests}
onChange={(e) => setRequests(e.target.value)}
className="w-full h-32 p-3 border rounded-lg"
placeholder="I can't log into my account The app is running slowly How do I upgrade my plan?"
/>
</div>
<button
onClick={handleClassify}
disabled={loading || !requests.trim()}
className="bg-blue-600 text-white px-6 py-2 rounded-lg disabled:opacity-50"
>
{loading ? 'Classifying...' : 'Classify Requests'}
</button>
{classifications.length > 0 && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Classifications:</h2>
{classifications.map((item, index) => (
<div key={index} className="p-4 border rounded-lg">
<p className="font-medium">{item.request}</p>
<div className="flex gap-4 mt-2 text-sm">
<span className="bg-blue-100 px-2 py-1 rounded">
{item.category}
</span>
<span className={`px-2 py-1 rounded ${
item.priority === 'urgent' ? 'bg-red-100' :
item.priority === 'high' ? 'bg-orange-100' :
item.priority === 'medium' ? 'bg-yellow-100' : 'bg-green-100'
}`}>
{item.priority}
</span>
<span className={`px-2 py-1 rounded ${
item.sentiment === 'positive' ? 'bg-green-100' :
item.sentiment === 'negative' ? 'bg-red-100' : 'bg-gray-100'
}`}>
{item.sentiment}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}// app/chat/page.tsx
'use client'
import { useChat } from 'ai/react'
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div className="max-w-2xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">AI Assistant</h1>
<div className="space-y-4 mb-6 h-96 overflow-y-auto">
{messages.map(message => (
<div
key={message.id}
className={`p-4 rounded-lg ${
message.role === 'user'
? 'bg-blue-100 ml-auto max-w-[80%]'
: 'bg-gray-100 mr-auto max-w-[80%]'
}`}
>
<div className="font-medium text-sm mb-1">
{message.role === 'user' ? 'You' : 'AI'}
</div>
<div className="whitespace-pre-wrap">{message.content}</div>
</div>
))}
{isLoading && (
<div className="bg-gray-100 p-4 rounded-lg mr-auto max-w-[80%]">
<div className="flex items-center gap-2">
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.1s'}} />
<div className="w-2 h-2 bg-gray-400 rounded-full animate-bounce" style={{animationDelay: '0.2s'}} />
<span className="text-sm text-gray-500 ml-2">AI is thinking...</span>
</div>
</div>
)}
</div>
<form onSubmit={handleSubmit} className="flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
className="flex-1 p-3 border rounded-lg"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading || !input.trim()}
className="bg-blue-600 text-white px-6 py-3 rounded-lg disabled:opacity-50"
>
Send
</button>
</form>
</div>
)
}// app/api/chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o-mini'),
messages,
system: 'You are a helpful assistant. Be concise and friendly.',
})
return result.toDataStreamResponse()
}And that's it! You now have a working AI application with both structured data generation and conversational chat.
This is just the beginning. From here, you can:
The AI SDK makes all of this possible with the same simple, unified interface.
Remember: experiment, iterate, and don't be afraid to try things. The capabilities are improving rapidly, and what might not work this week might work next week.
The future of frontend development includes AI as a core competency, not an add-on. The question isn't whether AI will change how we build applications - it's whether you're ready to be part of that change.
Ready to build? Start with the examples above, then explore the advanced patterns. The AI revolution in frontend development is happening now - and with the AI SDK, you're equipped to be part of it.
"If you know English, you know AI." - Now go build something amazing! 🚀
This comprehensive guide captures the enthusiastic, conversational tone from the transcript while providing all the technical depth and code examples we've covered. It flows naturally from basic concepts to advanced implementations, with plenty of "this is really cool" moments and practical examples that developers can immediately use.
<Actions>
<Action name="Create workshop materials" description="Build hands-on workshop exercises based on this guide" />
<Action name="Generate video script" description="Create a presentation script following this guide's flow" />
<Action name="Build starter templates" description="Create Next.js templates with all the patterns shown" />
<Action name="Create assessment quiz" description="Build a quiz to test understanding of AI SDK concepts" />
<Action name="Generate team onboarding plan" description="Create a structured plan for onboarding teams to AI SDK development" />
</Actions>presentation-script.md
[Point to each part as you speak]
It's almost identical to how we described it in English! generateText, model, prompt - it reads like natural language.
[Now the magic moment]
But here's where it gets really cool. Remember I said we could switch providers with one line? Watch this:
// OpenAI
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Tell me a joke'
})
// Google Gemini - just change this line!
const { text } = await generateText({
model: google('gemini-pro'),
prompt: 'Tell me a joke'
})
// Anthropic Claude - same interface!
const { text } = await generateText({
model: anthropic('claude-3-sonnet-20240229'),
prompt: 'Tell me a joke'
})[Pause to let this sink in]
That's the power of a unified API. But let me show you why this matters so much more than just convenience.
[Get a bit more serious, lean back]
Before the AI SDK, switching AI providers was like switching databases - a massive undertaking that could take weeks. Let me show you what the old world looked like:
// OpenAI - completely different API structure
const openaiResponse = await openai.chat.completions.create({
model: "gpt-4",
messages: [{ role: "user", content: prompt }],
temperature: 0.7,
max_tokens: 1000,
})
const openaiText = openaiResponse.choices[0].message.content
// Anthropic - totally different structure
const anthropicResponse = await anthropic.messages.create({
model: "claude-3-sonnet-20240229",
max_tokens: 1000,
temperature: 0.7,
messages: [{ role: "user", content: prompt }],
})
const anthropicText = anthropicResponse.content[0].text[Shake your head]
Look at that! Completely different APIs, different response structures, different parameter names. If you wanted to A/B test Claude vs GPT-4, you'd literally have to maintain two different codebases.
[Switch tone to excitement]
Now with the AI SDK:
// Same interface everywhere
const openaiResult = await generateText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing',
})
const anthropicResult = await generateText({
model: anthropic('claude-3-sonnet-20240229'),
prompt: 'Explain quantum computing',
})
const xaiResult = await generateText({
model: xai('grok-beta'),
prompt: 'Explain quantum computing',
})[Emphasize this point]
This isn't just convenience - this is strategic flexibility. You can now A/B test different AI models in production without your engineering team having to do a complete rewrite.
[Get excited about this pattern]
But we can take this even further. Let me show you a pattern I absolutely love - building a provider management system that handles everything automatically:
// lib/ai-providers.ts
export const modelConfigs = {
"gpt-4o": {
provider: "openai",
model: "gpt-4o",
supportsVision: true,
supportsTools: true,
costPerToken: 0.00003,
},
"claude-3-sonnet": {
provider: "anthropic",
model: "claude-3-sonnet-20240229",
supportsVision: true,
supportsTools: true,
costPerToken: 0.000015,
},
"grok-beta": {
provider: "xai",
model: "grok-beta",
supportsVision: false,
supportsTools: true,
costPerToken: 0.000002,
},
}
export function getModelInstance(configKey: string) {
const config = modelConfigs[configKey]
switch (config.provider) {
case "openai": return openai(config.model)
case "anthropic": return anthropic(config.model)
case "xai": return xai(config.model)
}
}[Point to the capabilities]
See what we're tracking here? Vision support, tool capabilities, cost per token. Now we can build smart selection logic:
export function selectOptimalModel(requirements: {
needsVision?: boolean
maxCost?: number
priority: "speed" | "cost" | "quality"
}) {
const candidates = Object.entries(modelConfigs).filter(([_, caps]) => {
if (requirements.needsVision && !caps.supportsVision) return false
if (requirements.maxCost && caps.costPerToken > requirements.maxCost) return false
return true
})
// Return the best match based on priority
return candidates[0][0]
}[Get enthusiastic]
This is powerful because now you can optimize different parts of your application with different models without changing your core logic. Need vision for image analysis? Use GPT-4o. Need cost optimization for simple tasks? Use Grok. Need the highest quality reasoning? Use Claude.
[Shift energy - this is about user experience]
Now let's talk about streaming, because this isn't just "nice to have" anymore. Users expect real-time responses. The difference between generateText and streamText is literally just the function name:
// Generate all at once - user waits
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing'
})
// Stream in real-time - user sees progress
const result = streamText({
model: openai('gpt-4o'),
prompt: 'Explain quantum computing'
})
for await (const delta of result.textStream) {
process.stdout.write(delta) // Real-time output!
}[But here's the magic]
But in a React application, you don't even need to handle the streaming manually. The useChat hook does all the heavy lifting:
"use client"
import { useChat } from 'ai/react'
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat()
return (
<div>
{messages.map(message => (
<div key={message.id} className={`message ${message.role}`}>
<strong>{message.role}:</strong> {message.content}
</div>
))}
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={handleInputChange}
placeholder="Type your message..."
disabled={isLoading}
/>
<button type="submit">Send</button>
</form>
</div>
)
}[Emphasize this]
That useChat hook is managing conversation state, handling streaming responses, clearing inputs, error states - all the gnarly stuff you'd otherwise build from scratch every time.
[This is the game-changer moment]
Alright, here's where things get really magical. This is probably my favorite feature - structured object generation.
[Set up the problem]
Traditionally, when you ask an AI to classify something, you get back unstructured text that's hard to work with:
const { text } = await generateText({
model: openai('gpt-4o-mini'),
prompt: `Classify these support requests:
"I can't log in", "The app is slow", "How do I upgrade?"
Categories: billing, technical, sales`
})
console.log(text) // Gets messy, unstructured output like:
// "The first request is technical, the second is technical, the third is sales..."[Build the tension]
The problem? You get back unstructured text that's impossible to work with programmatically.
[Now the magic reveal]
But here's where the AI SDK gets really magical - structured object generation:
import { generateObject } from 'ai'
import { z } from 'zod'
const classificationSchema = z.object({
request: z.string(),
category: z.enum(['billing', 'technical', 'sales', 'general']),
urgency: z.enum(['low', 'medium', 'high']),
language: z.string().describe('The language of the request')
})
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: classificationSchema,
prompt: 'Classify this support request',
output: 'array' // Get an array of classified requests
})
// Now you have fully typed, structured data!
object.forEach(item => {
console.log(`${item.category} - ${item.urgency}`) // Fully typed!
})[Get excited about this]
This is honestly magical. We've moved from unstructured text to fully typed, validated data structures. And that .describe() function on the Zod schema? That's giving the AI more context about exactly what we want for that field.
[Show a real example]
Let me show you how we use this in production. This is a pattern we use at Vercel for the Next.js repo - we get thousands of GitHub issues every week:
const issueSchema = z.object({
title: z.string(),
category: z.enum(['bug', 'feature-request', 'documentation', 'question']),
priority: z.enum(['low', 'medium', 'high', 'critical']),
components: z.array(z.string()).describe('Which Next.js components are affected'),
reproducible: z.boolean().describe('Can this issue be reproduced?'),
estimatedEffort: z.enum(['small', 'medium', 'large']).describe('Development effort required')
})
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: issueSchema,
prompt: `Analyze this GitHub issue: ${issueContent}`
})
// Now we can automatically route, prioritize, and assign issues![Pause for impact]
This represents reliable AI. The output isn't just text anymore - it's structured, typed, validated data that integrates seamlessly with your existing systems.
[Shift to the future]
Now let me show you something that blew my mind when I first built it - a chat interface that handles text, images, and files all in one conversation.
[Show the backend first]
Here's the API route:
// app/api/multi-modal-chat/route.ts
import { streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
export async function POST(req: Request) {
const { messages } = await req.json()
const result = streamText({
model: openai('gpt-4o'), // Vision-capable model
messages: messages.map(message => ({
role: message.role,
content: message.content.map(part => {
if (part.type === "text") {
return { type: "text", text: part.text }
}
if (part.type === "image") {
return { type: "image", image: part.image }
}
return part
}),
})),
})
return result.toDataStreamResponse()
}[Now the frontend]
And on the frontend:
export default function MultiModalChat() {
const [attachments, setAttachments] = useState<Attachment[]>([])
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/multi-modal-chat'
})
const onSubmit = (e: React.FormEvent) => {
e.preventDefault()
const content = []
if (input.trim()) content.push({ type: "text", text: input })
attachments.forEach(attachment => {
if (attachment.type === "image") {
content.push({ type: "image", image: attachment.base64 })
}
})
handleSubmit(e, { data: { content } })
}
return (
<div>
<form onSubmit={onSubmit}>
<input type="file" onChange={handleFileUpload} accept="image/*" />
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
</div>
)
}[Emphasize the breakthrough]
What's incredible here is that text, images, and files are all just content types now. The AI doesn't care if you're sending it a paragraph or a screenshot. That's revolutionary.
[Give a concrete example]
Imagine a user uploads a screenshot of an error and types "What's wrong here?" The AI can see the screenshot, read the error message, understand the context, and provide a solution. That's the future of user interfaces.
[Build excitement - this is where AI becomes powerful]
Alright, here's where things get really exciting - AI that can actually DO things, not just generate text. This is called tool calling, and it's like giving your AI superpowers.
import { tool } from 'ai'
import { z } from 'zod'
export const tools = {
calculator: tool({
description: 'Perform mathematical calculations',
parameters: z.object({
expression: z.string().describe('Mathematical expression to evaluate'),
}),
execute: async ({ expression }) => {
try {
const result = Function(`"use strict"; return (${expression})`)()
return { result: result.toString(), expression }
} catch (error) {
return { error: 'Invalid expression', expression }
}
},
}),
getWeather: tool({
description: 'Get current weather for a location',
parameters: z.object({
location: z.string().describe('City name or location'),
}),
execute: async ({ location }) => {
// In real app, call weather API
return {
location,
temperature: '22°C',
condition: 'sunny',
humidity: '65%'
}
},
}),
scheduleEvent: tool({
description: 'Schedule an event or reminder',
parameters: z.object({
title: z.string(),
date: z.string().describe('Date in YYYY-MM-DD format'),
time: z.string().describe('Time in HH:MM format'),
}),
execute: async ({ title, date, time }) => {
// In real app, integrate with calendar API
return {
eventId: Math.random().toString(36),
message: `Event "${title}" scheduled for ${date} at ${time}`,
status: 'confirmed'
}
},
}),
}[Show how to use them]
Now when you use these tools:
const { text, toolCalls } = await generateText({
model: openai('gpt-4o'),
prompt: 'What\'s 15% of 2,450 plus 300? Also, what\'s the weather in Tokyo?',
tools,
maxToolRoundtrips: 3,
})[Get excited about the implications]
The AI will automatically decide which tools to use, call them with the right parameters, and incorporate the results into its response. Each tool represents a capability extension. You're not just building one AI feature - you're building an ecosystem of AI capabilities that can work together.
[Give real-world examples]
Think about the possibilities:
This is AI that takes action, not just AI that talks.
[Set up the concept]
Now let me show you RAG - Retrieval-Augmented Generation. Think of this as giving the AI perfect memory of your business. Instead of just knowing general information, it knows YOUR data, YOUR documents, YOUR business context.
import { embed, generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
export class SimpleVectorStore {
private documents: Document[] = []
async addDocument(doc: { id: string, content: string, metadata: any }) {
// Generate embedding for the document
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: doc.content,
})
this.documents.push({ ...doc, embedding })
}
async similaritySearch(query: string, limit = 5) {
// Generate embedding for the query
const { embedding: queryEmbedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
})
// Find most similar documents
const similarities = this.documents.map(doc => ({
document: doc,
similarity: this.cosineSimilarity(queryEmbedding, doc.embedding),
}))
return similarities
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit)
.map(item => item.document)
}
async generateAnswer(query: string) {
// Find relevant documents
const relevantDocs = await this.similaritySearch(query, 3)
// Create context from relevant documents
const context = relevantDocs
.map(doc => `Source: ${doc.metadata.title}\n${doc.content}`)
.join('\n\n')
// Generate answer with context
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: `Answer the question based on the provided context.
Context:
${context}
Question: ${query}
Answer:`,
})
return { answer: text, sources: relevantDocs }
}
}[Explain the power]
This is contextual AI. The AI isn't just generating generic responses - it's generating responses based on YOUR data, YOUR documents, YOUR business context.
[Give concrete examples]
Imagine:
This is AI that's truly integrated with your business.
[Get serious about production]
Now, all of this is great for demos, but let's talk about production. When you're building real applications, you need error handling, rate limiting, cost management, and observability.
[Show resilient AI]
Here's a pattern for building resilient AI applications:
class ResilientAI {
private providers = [
{ name: 'openai', model: openai('gpt-4o'), priority: 1 },
{ name: 'anthropic', model: anthropic('claude-3-sonnet-20240229'), priority: 2 },
{ name: 'xai', model: xai('grok-beta'), priority: 3 },
]
async generateWithFallback(prompt: string, options = {}) {
const { maxRetries = 3, timeout = 30000 } = options
const errors = []
for (const provider of this.providers) {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
const result = await generateText({
model: provider.model,
prompt,
abortSignal: controller.signal,
})
clearTimeout(timeoutId)
return { ...result, provider: provider.name, attempt }
} catch (error) {
errors.push({ provider: provider.name, error: error.message })
// Exponential backoff
if (attempt < maxRetries) {
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempt) * 1000)
)
}
}
}
}
throw new Error(`All providers failed: ${JSON.stringify(errors)}`)
}
}[Emphasize reliability]
This gives you automatic failover, retry logic with exponential backoff, and timeout handling. This is production-ready AI.
[Shift to career implications]
Let me talk about what this means for us as developers. Prompt engineering isn't just a buzzword anymore. It's becoming as fundamental as knowing SQL or understanding REST APIs.
[Make it relatable]
The difference is, with SQL you're querying structured data. With prompts, you're querying intelligence itself.
[Show good prompting]
A good prompt has four parts:
const promptTemplates = {
codeReview: `You are a senior software engineer conducting a code review.
Analyze the following code and provide:
1. Potential issues or bugs
2. Performance improvements
3. Best practice recommendations
4. Security considerations
Code to review:
{code}
Provide your feedback in a structured format.`,
userStoryGeneration: `Convert the following feature description into proper user stories using the format:
"As a [user type], I want [functionality] so that [benefit]"
Include:
- Main user story
- 2-3 acceptance criteria
- Edge cases to consider
Feature description:
{feature}`,
}[Explain the pattern]
[Make it actionable]
These templates become reusable intelligence patterns. You can version control your AI interactions the same way you version control your code. That's a paradigm shift.
[Switch to live coding mode]
Alright, let's build something together. I'm going to walk you through creating a support ticket classifier - the same pattern we use in production at Vercel.
[Start with setup]
First, let's set up a new Next.js project:
npm create next-app@latest my-ai-app
cd my-ai-app
npm install ai @ai-sdk/openai zod[Environment setup]
Add your OpenAI API key to .env.local:
OPENAI_API_KEY=your_api_key_here[Create the API route]
Now let's create the classification API:
// app/api/classify/route.ts
import { generateObject } from 'ai'
import { openai } from '@ai-sdk/openai'
import { z } from 'zod'
const classificationSchema = z.object({
request: z.string(),
category: z.enum(['billing', 'technical', 'sales', 'general']),
priority: z.enum(['low', 'medium', 'high', 'urgent']),
sentiment: z.enum(['positive', 'neutral', 'negative']),
})
export async function POST(req: Request) {
const { requests } = await req.json()
try {
const { object } = await generateObject({
model: openai('gpt-4o-mini'),
schema: classificationSchema,
prompt: `Classify these support requests:
Categories: billing, technical, sales, general
Priority: low, medium, high, urgent
Sentiment: positive, neutral, negative
Requests: ${JSON.stringify(requests)}`,
output: 'array',
})
return Response.json({ classifications: object })
} catch (error) {
return Response.json({ error: 'Classification failed' }, { status: 500 })
}
}[Create the frontend]
Now the frontend:
// app/page.tsx
'use client'
import { useState } from 'react'
export default function Home() {
const [requests, setRequests] = useState('')
const [classifications, setClassifications] = useState([])
const [loading, setLoading] = useState(false)
const handleClassify = async () => {
setLoading(true)
const requestList = requests.split('\n').filter(r => r.trim())
const response = await fetch('/api/classify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ requests: requestList }),
})
const data = await response.json()
setClassifications(data.classifications)
setLoading(false)
}
return (
<div className="max-w-4xl mx-auto p-8">
<h1 className="text-3xl font-bold mb-8">AI Support Ticket Classifier</h1>
<div className="space-y-6">
<div>
<label className="block text-sm font-medium mb-2">
Support Requests (one per line):
</label>
<textarea
value={requests}
onChange={(e) => setRequests(e.target.value)}
className="w-full h-32 p-3 border rounded-lg"
placeholder="I can't log into my account The app is running slowly How do I upgrade my plan?"
/>
</div>
<button
onClick={handleClassify}
disabled={loading || !requests.trim()}
className="bg-blue-600 text-white px-6 py-2 rounded-lg disabled:opacity-50"
>
{loading ? 'Classifying...' : 'Classify Requests'}
</button>
{classifications.length > 0 && (
<div className="space-y-4">
<h2 className="text-xl font-semibold">Classifications:</h2>
{classifications.map((item, index) => (
<div key={index} className="p-4 border rounded-lg">
<p className="font-medium">{item.request}</p>
<div className="flex gap-4 mt-2 text-sm">
<span className="bg-blue-100 px-2 py-1 rounded">
{item.category}
</span>
<span className={`px-2 py-1 rounded ${
item.priority === 'urgent' ? 'bg-red-100' :
item.priority === 'high' ? 'bg-orange-100' :
item.priority === 'medium' ? 'bg-yellow-100' : 'bg-green-100'
}`}>
{item.priority}
</span>
<span className={`px-2 py-1 rounded ${
item.sentiment === 'positive' ? 'bg-green-100' :
item.sentiment === 'negative' ? 'bg-red-100' : 'bg-gray-100'
}`}>
{item.sentiment}
</span>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}[Test it live]
Let's test this with some real examples:
[Show the results and get excited]
Look at that! Fully structured, typed data with categories, priorities, and sentiment analysis. This is the power of the AI SDK in action.
[Bring it all together]
So let's recap what we've covered today:
[Make it personal]
The AI SDK makes all of this possible with the same simple, unified interface we started with. Remember: experiment, iterate, and don't be afraid to try things. The capabilities are improving rapidly, and what might not work this week might work next week.
[End with impact]
The future of frontend development includes AI as a core competency, not an add-on. The question isn't whether AI will change how we build applications - it's whether you're ready to be part of that change.
[Final call to action]
So here's what I want you to do: pick one of these patterns, build something small this week, and share it with the team. Start with the basic text generation, then add streaming, then try structured output.
The AI revolution in frontend development is happening now - and with the AI SDK, you're equipped to be part of it.
[End with energy]
Questions? Let's dive in!
Common Questions & Answers:
Q: "What about costs?" A: Great question! The AI SDK gives you cost transparency. You can track usage per request, set up rate limiting, and even switch to cheaper models for simple tasks. I showed the cost tracking in the provider management example.
Q: "How do we handle errors in production?" A: The resilient AI pattern I showed handles this - automatic failover between providers, retry logic, timeout handling. You can also implement circuit breakers and graceful degradation.
Q: "What about data privacy?" A: You control where your data goes. You can use local models, on-premise solutions, or providers with strict data handling policies. The unified API means you can switch providers based on privacy requirements.
Q: "How do we test AI features?" A: Great question! You can mock the AI SDK functions in tests, use deterministic prompts for consistent outputs, and implement A/B testing for different approaches.
Q: "What's the learning curve?" A: If you know TypeScript and React, you already know 90% of what you need. The hardest part is learning to write good prompts, which is more about clear communication than technical skills.
[End of script - you've got this! Remember to pause for questions, engage with the audience, and have fun with the demos]
This script is designed to be read naturally while presenting, with all the code snippets ready to copy/paste to your demo screen. The tone matches the enthusiastic, conversational style from the transcript, and includes timing cues, speaking notes, and Q&A prep to help you deliver a compelling presentation.
[This is your private script - audience won't see this. Code blocks are ready to copy/paste to your demo screen]
[Start with energy, lean forward]
Alright team, let's dive into AI - specifically the Vercel AI SDK. And I want to start with something that might sound like a big claim, but bear with me here.
[Pause for effect]
You know how we always talk about intuitive APIs? Well, the AI SDK has been built with this philosophy: "if you can describe what you want in plain language, you can build it with AI."
[Let that sink in]
I know that might sound too good to be true, but I'm going to demonstrate exactly what I mean. By the end of this session, you'll see how integrating AI can feel as straightforward as calling any other API.
[Transition]
So let me start with the simplest possible example, and then we'll explore why this changes everything for frontend development.
[Switch to code screen]
Let's think about how we'd describe calling an AI model in everyday terms:
Now look at this code:
import { generateText } from 'ai'
import { openai } from '@ai-sdk/openai'
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Write a creative product description for wireless headphones'
})
console.log(text)