AI SDK Integration Patterns
Overview
Standards for integrating AI/LLM capabilities into Next.js applications using Vercel AI SDK, OpenAI API, and related tools. Covers streaming chat, generative UI, RAG, and structured output patterns.
Vercel AI SDK Setup
Installation
bash
pnpm add ai @ai-sdk/openai @ai-sdk/anthropic
Provider Configuration
File: src/lib/ai.ts
typescript
import { createOpenAI } from "@ai-sdk/openai"
import { createAnthropic } from "@ai-sdk/anthropic"
export const openai = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
})
export const anthropic = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
Chat Patterns
Basic Streaming Chat (API Route)
File: src/app/api/chat/route.ts
typescript
import { streamText } from "ai"
import { openai } from "@/lib/ai"
export async function POST(request: Request) {
const { messages } = await request.json()
const result = streamText({
model: openai("gpt-4o"),
system: "You are a helpful assistant.",
messages,
})
return result.toDataStreamResponse()
}
Chat UI Component
typescript
"use client"
import { useChat } from "@ai-sdk/react"
export const Chat = () => {
const { messages, input, handleInputChange, handleSubmit, isLoading } = useChat({
api: "/api/chat",
})
return (
<div className="flex flex-col h-full">
<div className="flex-1 overflow-y-auto space-y-4 p-4">
{messages.map((message) => (
<div
key={message.id}
className={cn(
"rounded-lg px-4 py-2 max-w-[80%]",
message.role === "user"
? "ml-auto bg-primary text-primary-foreground"
: "bg-muted"
)}
>
{message.content}
</div>
))}
</div>
<form onSubmit={handleSubmit} className="border-t p-4 flex gap-2">
<input
value={input}
onChange={handleInputChange}
placeholder="Type a message..."
className="flex-1 rounded-md border px-3 py-2"
disabled={isLoading}
/>
<button
type="submit"
disabled={isLoading}
className="rounded-md bg-primary px-4 py-2 text-primary-foreground"
>
Send
</button>
</form>
</div>
)
}
Structured Output (generateObject)
typescript
import { generateObject } from "ai"
import { openai } from "@/lib/ai"
import { z } from "zod"
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().describe("in minutes"),
})
export async function generateRecipe(prompt: string) {
const { object } = await generateObject({
model: openai("gpt-4o"),
schema: recipeSchema,
prompt,
})
return object // Fully typed as z.infer<typeof recipeSchema>
}
Generative UI Pattern
Server-side UI Generation
typescript
import { streamUI } from "ai/rsc"
import { openai } from "@/lib/ai"
import { z } from "zod"
export async function generateUI(prompt: string) {
const result = await streamUI({
model: openai("gpt-4o"),
prompt,
tools: {
showWeather: {
description: "Show weather for a location",
parameters: z.object({
location: z.string(),
temperature: z.number(),
}),
generate: async function* ({ location, temperature }) {
yield <LoadingSkeleton />
return <WeatherCard location={location} temperature={temperature} />
},
},
showStock: {
description: "Show stock price",
parameters: z.object({
symbol: z.string(),
price: z.number(),
}),
generate: async function* ({ symbol, price }) {
yield <LoadingSkeleton />
return <StockCard symbol={symbol} price={price} />
},
},
},
})
return result.value
}
RAG (Retrieval-Augmented Generation)
With Supabase pgvector
typescript
import { embed } from "ai"
import { openai } from "@/lib/ai"
import { createClient } from "@/lib/supabase/server"
// Generate embedding
async function generateEmbedding(text: string) {
const { embedding } = await embed({
model: openai.embedding("text-embedding-3-small"),
value: text,
})
return embedding
}
// Search similar documents
async function searchDocuments(query: string, limit = 5) {
const supabase = await createClient()
const queryEmbedding = await generateEmbedding(query)
const { data } = await supabase.rpc("match_documents", {
query_embedding: queryEmbedding,
match_threshold: 0.7,
match_count: limit,
})
return data
}
// RAG Chat
export async function POST(request: Request) {
const { messages } = await request.json()
const lastMessage = messages[messages.length - 1].content
// Retrieve relevant context
const context = await searchDocuments(lastMessage)
const contextText = context.map((d: any) => d.content).join("\n\n")
const result = streamText({
model: openai("gpt-4o"),
system: `Answer based on the following context:\n\n${contextText}`,
messages,
})
return result.toDataStreamResponse()
}
Supabase pgvector SQL Setup
sql
-- Enable vector extension
CREATE EXTENSION IF NOT EXISTS vector;
-- Create documents table
CREATE TABLE documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536),
metadata JSONB DEFAULT '{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Create similarity search function
CREATE OR REPLACE FUNCTION match_documents(
query_embedding VECTOR(1536),
match_threshold FLOAT,
match_count INT
)
RETURNS TABLE (id UUID, content TEXT, similarity FLOAT)
LANGUAGE SQL STABLE
AS $$
SELECT id, content, 1 - (embedding <=> query_embedding) AS similarity
FROM documents
WHERE 1 - (embedding <=> query_embedding) > match_threshold
ORDER BY embedding <=> query_embedding
LIMIT match_count;
$$;
Tool Calling Pattern
typescript
import { streamText, tool } from "ai"
import { openai } from "@/lib/ai"
import { z } from "zod"
const result = streamText({
model: openai("gpt-4o"),
messages,
tools: {
getWeather: tool({
description: "Get current weather for a location",
parameters: z.object({
location: z.string().describe("City name"),
}),
execute: async ({ location }) => {
// API call
return { temperature: 22, condition: "sunny" }
},
}),
searchProducts: tool({
description: "Search for products in the catalog",
parameters: z.object({
query: z.string(),
maxPrice: z.number().optional(),
}),
execute: async ({ query, maxPrice }) => {
// DB query
return []
},
}),
},
maxSteps: 5, // Allow multi-step tool calls
})
useCompletion (Non-Chat Streaming)
typescript
"use client"
import { useCompletion } from "@ai-sdk/react"
export const TextGenerator = () => {
const { completion, input, handleInputChange, handleSubmit, isLoading } = useCompletion({
api: "/api/generate",
})
return (
<div>
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit" disabled={isLoading}>Generate</button>
</form>
<div className="prose">{completion}</div>
</div>
)
}
Anti-Patterns
- •Never expose API keys to client - always use server-side routes
- •Never skip error handling on AI calls - models can fail or timeout
- •Never send unlimited context - truncate/summarize conversation history
- •Never use blocking (non-streaming) calls for long AI responses in UI
- •Never trust AI output without validation - always validate with Zod for structured output
- •Never hardcode model names - use configuration for easy switching
Additional Resources
- •references/prompt-engineering.md - Effective prompt design patterns
- •references/cost-optimization.md - Token usage optimization strategies