
Ai Sdk Core
- 9 installs
- 5 repo stars
- Updated August 5, 2026
- bjornmelin/dev-skills
Ai-sdk-core is a Claude Code skill giving expert guidance for AI SDK Core text generation, structured data, tool calling, embeddings, and MCP integration.
About
Ai-sdk-core is a Claude Code skill for building with AI SDK Core: generating text and structured output, tool calling, embeddings and reranking, MCP integration, middleware, telemetry, and error handling. It provides a function-selection table (generateText, streamText, generateObject, streamObject, embed, rerank) and patterns for typed and dynamic tools, multi-step execution, and provider setup. Developers use it to wire LLM calls, tools, and MCP servers with a consistent API across providers.
- Guidance for AI SDK Core: text, structured data, tool calling, embeddings, reranking
- Covers MCP integration via createMCPClient and stdio/HTTP transports
- Includes middleware, telemetry, provider setup, and error handling
Ai Sdk Core by the numbers
- 9 all-time installs (skills.sh)
- Ranked #12,152 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
ai-sdk-core capabilities & compatibility
Free skill; requires a model provider API key (e.g. OpenAI or Anthropic) to run the generated calls.
- Capabilities
- api development · orchestration
- Works with
- openai · anthropic · vercel
- Use cases
- api development · orchestration
- Pricing
- Bring your own API key
What ai-sdk-core says it does
Use AI SDK Core to generate text/structured output, call tools, and connect to MCP servers with consistent APIs across providers.
Use `createMCPClient()` to load MCP tools, resources, and prompts.
npx skills add https://github.com/bjornmelin/dev-skills --skill ai-sdk-coreAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 9 |
|---|---|
| repo stars | ★ 5 |
| Last updated | August 5, 2026 |
| Repository | bjornmelin/dev-skills ↗ |
What it does
Generate text and structured output, call tools, and connect to MCP servers with the AI SDK Core across LLM providers.
Who is it for?
Wiring text/structured generation, tool calling, and MCP servers with the AI SDK Core across providers
Skip if: Building the client-side chat UI, which is covered by the AI SDK UI hooks instead
When should I use this skill?
Building with generateText/streamText, generateObject/streamObject, tools, embeddings, or MCP tools
What you get
Correct AI SDK Core calls for text, structured data, tools, embeddings, and MCP with proper error handling.
- AI SDK Core text/structured/tool implementations
- MCP client integrations
By the numbers
- 6-row function-selection table
- 10 bundled reference files
Files
AI SDK Core
Use AI SDK Core to generate text/structured output, call tools, and connect to MCP servers with consistent APIs across providers.
Quick Start
pnpm add ai @ai-sdk/openai zod@^4.3.5import { generateText } from 'ai';
const { text } = await generateText({
model: 'openai/gpt-4o',
prompt: 'Explain quantum computing in one paragraph.',
});Function Selection
| Need | Function | Streaming |
|---|---|---|
| Text response | generateText | No |
| Streaming text | streamText | Yes |
| Structured JSON | generateObject | No |
| Streaming JSON | streamObject | Yes |
| Embeddings | embed / embedMany | No |
| Rerank | rerank | No |
Core Patterns
Generate Text
import { generateText } from 'ai';
const { text, usage } = await generateText({
model: 'anthropic/claude-sonnet-4.5',
system: 'You are a helpful assistant.',
prompt: 'What is the capital of France?',
});Stream Text
import { streamText } from 'ai';
const result = streamText({
model: 'openai/gpt-4o',
prompt: 'Write a short story.',
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}Generate Structured Data
import { generateObject } from 'ai';
import { z } from 'zod';
const { object } = await generateObject({
model: 'openai/gpt-4o',
schema: z.object({
recipe: z.object({
name: z.string(),
ingredients: z.array(z.object({ name: z.string(), amount: z.string() })),
steps: z.array(z.string()),
}),
}),
prompt: 'Generate a recipe for chocolate chip cookies.',
});Tool Calling (Typed)
import { generateText, tool } from 'ai';
import { z } from 'zod';
const { text, toolCalls } = await generateText({
model: 'openai/gpt-4o',
tools: {
weather: tool({
description: 'Get weather for a location',
inputSchema: z.object({ location: z.string() }),
execute: async ({ location }) => ({ temperature: 72, condition: 'sunny' }),
}),
},
prompt: 'What is the weather in San Francisco?',
});Dynamic Tools (Runtime Schemas)
import { dynamicTool } from 'ai';
import { z } from 'zod';
const customTool = dynamicTool({
description: 'Execute a custom function',
inputSchema: z.object({}),
execute: async input => ({ ok: true, input }),
});Multi-Step Tool Execution
import { generateText, stepCountIs } from 'ai';
const { steps } = await generateText({
model: 'openai/gpt-4o',
tools: { search, analyze, summarize },
stopWhen: stepCountIs(5),
prompt: 'Research and summarize AI developments.',
});Tooling Checklist
- Use
tool()for typed inputs anddynamicTool()for unknown schemas. - Use
needsApprovalfor sensitive actions (tool-approval-request/response flow). - Use
stopWhenwithstepCountIs/hasToolCallfor multi-step loops. - Use
prepareStepfor per-step controls (model swap, toolChoice, activeTools, prompt compression). - Use
experimental_contextwhen tools need app-specific context. - Use
inputExamplesandstrictto improve tool call reliability.
MCP Integration (Model Context Protocol)
- Use
createMCPClient()to load MCP tools, resources, and prompts. - Prefer HTTP transport for production; use
Experimental_StdioMCPTransportonly for local Node.js servers. - Close MCP clients after use (try/finally or
onFinish).
See references/mcp-integration.md for transports, schema definition, outputSchema typing, and elicitation.
Reference Files
| Reference | When to Use |
|---|---|
references/text-generation.md | generateText/streamText callbacks, streaming, response handling |
references/structured-data.md | generateObject/streamObject, Output API, Zod patterns |
references/tool-calling.md | tool/dynamicTool, approval flow, repair, activeTools, hooks |
references/dynamic-tools.md | dynamicTool patterns, MCP + dynamic tools, large tool sets |
references/embeddings-rag.md | embed/embedMany, rerank, chunking |
references/providers.md | OpenAI/Anthropic/Google setup, registry, AI Gateway |
references/middleware.md | wrapLanguageModel, built-in/custom middleware |
references/mcp-integration.md | MCP client, transports, tools/resources/prompts/elicitation |
references/production.md | Telemetry, error handling, testing, cost control |
references/migration.md | v6 upgrade notes |
Error Handling
import { generateText, AI_APICallError } from 'ai';
try {
await generateText({ model: 'openai/gpt-4o', prompt: 'Hello' });
} catch (error) {
if (error instanceof AI_APICallError) {
console.error('API Error:', error.message);
}
}Provider Setup
import { openai } from '@ai-sdk/openai';
const { text } = await generateText({
model: openai('gpt-4o'),
prompt: 'Hello!',
});Version Guidance
- Use AI SDK v6+ with matching provider packages.
- Pin major versions in
package.jsonto avoid breaking changes.
Dynamic Tools Reference (AI SDK Core v6+)
Use this guide for building dynamic tools with unknown schemas at compile time, integrating MCP tools, and scaling to large tool sets. Assumes Zod v4.3.5.
---
1) When to Use dynamicTool()
Use dynamicTool() when the input/output types are unknown at build time:
- MCP tools without schemas
- User-defined functions loaded at runtime
- Tools sourced from external databases or registries
- Dynamic tool generation based on user input
Prefer tool() for stable, type-safe tools with known schemas.
---
2) Dynamic Tool Basics
dynamicTool() still requires a schema, but the types are unknown. Use z.unknown() or a permissive shape and validate inside execute. The resulting tool is flagged as dynamic in tool calls for type narrowing.
import { dynamicTool } from 'ai';
import { z } from 'zod';
export const customTool = dynamicTool({
description: 'Execute a custom user-defined function',
inputSchema: z.object({
action: z.string(),
payload: z.unknown(),
}),
execute: async input => {
const { action, payload } = input as { action: string; payload: unknown };
return { ok: true, action, payload };
},
});Notes
- Use Zod v4.3.5 for schema typing consistency.
- Use
z.any()orz.unknown()for fully dynamic payloads, then validate at runtime. - Consider per-action schema maps to validate
payloadbefore use.
---
3) Type-Safe Handling with the dynamic Flag
When mixing static and dynamic tools, narrow by the dynamic flag:
const result = await generateText({
model,
tools: {
weather: weatherTool,
custom: customTool,
},
onStepFinish: ({ toolCalls }) => {
for (const call of toolCalls) {
if (call.dynamic) {
// input/output are unknown
continue;
}
if (call.toolName === 'weather') {
call.input.location; // typed
}
}
},
});---
4) Dynamic Tools + Large Tool Sets
- Use
dynamicTool()when schemas are unknown at compile time. - Use
activeToolsto avoid sending large tool lists every step. - Prefer schema definition over discovery for type safety and control.
Pattern: phase-based tooling
prepareStep: async ({ stepNumber }) => {
if (stepNumber <= 2) return { activeTools: ['search'], toolChoice: 'required' };
if (stepNumber <= 5) return { activeTools: ['analyze'] };
return { activeTools: ['summarize'], toolChoice: 'required' };
}---
5) Dynamic Tools + MCP Client
MCP tool discovery often results in unknown types. You can either:
1) Use schema discovery (all tools, unknown inputs), or 2) Use schema definition (typed + selective), or 3) Combine MCP tools with local dynamicTool() wrappers.
Local MCP server (stdio)
import { createMCPClient } from '@ai-sdk/mcp';
import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
import { generateText } from 'ai';
let client;
try {
client = await createMCPClient({
transport: new Experimental_StdioMCPTransport({
command: 'node',
args: ['src/stdio/dist/server.js'],
}),
});
const mcpTools = await client.tools(); // schema discovery
const result = await generateText({
model,
tools: {
...mcpTools,
custom: customTool,
},
stopWhen: stepCountIs(5),
prompt: 'Use available tools to answer.',
});
} finally {
await client?.close();
}Typed MCP tools (schema definition)
import { z } from 'zod';
const mcpTools = await client.tools({
schemas: {
'get-weather': {
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({
temperature: z.number(),
conditions: z.string(),
}),
},
},
});---
6) ToolLoopAgent + Dynamic Tools
import { ToolLoopAgent } from 'ai';
const agent = new ToolLoopAgent({
model,
tools: {
weather: weatherTool,
custom: customTool,
},
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return { activeTools: ['custom'], toolChoice: 'required' };
}
return {};
},
});---
7) Runtime Validation and Safety
Best practices for dynamic tools:
- Validate
unknowninputs with per-action schemas. - Use
needsApprovalfor sensitive actions. - Enforce timeouts via
abortSignal. - Log tool calls/results per step.
- Use
inputExamplesto guide models when possible. - Pass request metadata via
experimental_contextif your tools need per-call context.
---
8) Research Tooling Example (Exa + Context7)
Below is a pattern for a dynamic research tool that routes to different backends. Replace the placeholders with your own HTTP clients.
import { dynamicTool } from 'ai';
import { z } from 'zod';
export const researchTool = dynamicTool({
description: 'Run research actions (search, crawl, docs).',
inputSchema: z.object({
action: z.enum(['exaDeepSearch', 'exaCrawl', 'context7Query']),
query: z.string().optional(),
url: z.url().optional(),
}),
execute: async input => {
const { action, query, url } = input as {
action: 'exaDeepSearch' | 'exaCrawl' | 'context7Query';
query?: string;
url?: string;
};
switch (action) {
case 'exaDeepSearch':
return exaDeepSearch({ query });
case 'exaCrawl':
return exaCrawl({ url });
case 'context7Query':
return context7Query({ query });
default:
return { error: 'Unknown action' };
}
},
});Tip: If you’re in an agent runtime that exposes exa.deep_search_exa, exa.crawling_exa, or Context7 tools directly, you can call them in execute. Otherwise, call their HTTP APIs.
---
9) UI Handling
When using useChat, dynamic tools appear as dynamic-tool parts. Render them explicitly in your UI for visibility and debugging.
AI SDK Core: Embeddings & RAG Reference
Comprehensive guide to embedding generation, similarity search, reranking, and RAG patterns with the Vercel AI SDK Core.
---
1. Embedding Generation
Single Value Embedding
Use embed() for individual text values (queries, single documents):
import { embed } from 'ai';
import { openai } from '@ai-sdk/openai';
const { embedding, usage } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: 'sunny day at the beach',
});
// embedding: number[] (e.g., 1536 dimensions for text-embedding-3-small)
// usage: { tokens: 10 }
console.log(embedding.length); // 1536Batch Embedding with embedMany()
Use embedMany() for bulk operations (preparing vector databases, batch indexing):
import { embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
// embeddings is an array of vectors, same order as input
const { embeddings, usage } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: [
'sunny day at the beach',
'rainy afternoon in the city',
'snowy night in the mountains',
],
});
console.log(embeddings.length); // 3
console.log(usage); // { tokens: 30 }Token Usage Tracking
Both embed() and embedMany() return usage information for cost tracking:
const { embedding, usage } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: 'The quick brown fox jumps over the lazy dog',
});
console.log(usage.tokens); // e.g., 9 tokens
// Use for cost calculation: tokens * price_per_tokenParallel Requests
Control concurrency for embedMany() with maxParallelCalls:
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
maxParallelCalls: 2, // Process 2 batches concurrently
values: Array.from({ length: 100 }, (_, i) => `Document ${i}`),
});
// Optimizes throughput while respecting rate limits
// Default: all requests in parallel (use with caution for large batches)Reduced Dimensions
Lower dimensional embeddings for faster similarity search and smaller storage:
import { embed } from 'ai';
import { openai } from '@ai-sdk/openai';
// text-embedding-3-small default: 1536 dimensions
// Reduce to 512 for storage/speed optimization
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: 'sunny day at the beach',
providerOptions: {
openai: {
dimensions: 512, // 1/3 the size, slight accuracy tradeoff
},
},
});
console.log(embedding.length); // 512Dimension tradeoffs:
- Lower dimensions: faster search, less storage, slightly lower accuracy
- Higher dimensions: better semantic accuracy, slower search, more storage
- Common reductions: 1536 → 512, 3072 → 1024
---
2. Embedding Models
OpenAI
import { openai } from '@ai-sdk/openai';
// text-embedding-3-small: 1536 dims, best cost/performance balance
const smallModel = openai.embedding('text-embedding-3-small');
// text-embedding-3-large: 3072 dims, highest accuracy
const largeModel = openai.embedding('text-embedding-3-large');
// text-embedding-ada-002: 1536 dims, legacy model
const adaModel = openai.embedding('text-embedding-ada-002');Usage example:
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-large'),
value: 'Complex technical query requiring high semantic accuracy',
});import { google } from '@ai-sdk/google';
// text-embedding-004: 768 dims, multilingual support
const googleModel = google.embedding('text-embedding-004');
// With provider-specific options:
const { embedding } = await embed({
model: googleModel,
value: 'document text',
providerOptions: {
google: {
outputDimensionality: 256, // Reduce from 768
taskType: 'RETRIEVAL_DOCUMENT', // or 'RETRIEVAL_QUERY', 'CLASSIFICATION'
},
},
});Cohere
import { cohere } from '@ai-sdk/cohere';
// embed-english-v3.0: 1024 dims, optimized for English
const englishModel = cohere.embedding('embed-english-v3.0');
// embed-multilingual-v3.0: 1024 dims, 100+ languages
const multilingualModel = cohere.embedding('embed-multilingual-v3.0');
// Light models for faster inference:
const lightModel = cohere.embedding('embed-english-light-v3.0'); // 384 dimsModel Selection Guidelines:
- OpenAI text-embedding-3-small: Default choice, excellent cost/performance
- OpenAI text-embedding-3-large: High accuracy needed (research, legal)
- Google text-embedding-004: Multilingual support, Google ecosystem
- Cohere embed-multilingual-v3.0: Best for multilingual RAG
---
3. Similarity Search
cosineSimilarity() Function
Measure semantic similarity between embeddings (-1 to 1, where 1 = identical):
import { cosineSimilarity, embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: [
'sunny day at the beach',
'rainy afternoon in the city',
'beach vacation in summer',
],
});
const sim1 = cosineSimilarity(embeddings[0], embeddings[1]);
const sim2 = cosineSimilarity(embeddings[0], embeddings[2]);
console.log(sim1); // ~0.3 (less similar)
console.log(sim2); // ~0.8 (very similar)Finding Similar Documents
Full similarity search implementation:
import { embed, embedMany, cosineSimilarity } from 'ai';
import { openai } from '@ai-sdk/openai';
const documents = [
'The quick brown fox jumps over the lazy dog',
'A fast auburn fox leaps above a sleepy canine',
'Machine learning is a subset of artificial intelligence',
'Deep neural networks power modern AI systems',
];
// Index documents (one-time operation)
const { embeddings: docEmbeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: documents,
});
// Search query
const query = 'animals jumping';
const { embedding: queryEmbedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
// Calculate similarities
const results = documents.map((doc, index) => ({
document: doc,
similarity: cosineSimilarity(queryEmbedding, docEmbeddings[index]),
}));
// Sort by similarity (highest first)
results.sort((a, b) => b.similarity - a.similarity);
console.log(results);
// [
// { document: 'The quick brown fox...', similarity: 0.85 },
// { document: 'A fast auburn fox...', similarity: 0.82 },
// { document: 'Machine learning...', similarity: 0.12 },
// { document: 'Deep neural networks...', similarity: 0.08 },
// ]Threshold-Based Filtering
Filter out low-relevance results:
const MIN_SIMILARITY = 0.7; // Tune based on your domain
const relevantResults = results.filter(
(result) => result.similarity >= MIN_SIMILARITY
);
console.log(relevantResults);
// Only returns documents with similarity >= 0.7Threshold guidelines:
- 0.9+: Near-duplicates
- 0.7-0.9: Highly relevant
- 0.5-0.7: Moderately relevant
- <0.5: Likely irrelevant (domain-dependent)
---
4. Reranking
Basic Reranking with Cohere
Rerank documents for more accurate relevance scoring than embedding similarity:
import { rerank } from 'ai';
import { cohere } from '@ai-sdk/cohere';
const documents = [
'sunny day at the beach',
'rainy afternoon in the city',
'snowy night in the mountains',
];
const { ranking, rerankedDocuments } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents,
query: 'talk about rain',
topN: 2, // Return top 2 most relevant
});
console.log(ranking);
// [
// { originalIndex: 1, score: 0.95, document: 'rainy afternoon in the city' },
// { originalIndex: 0, score: 0.15, document: 'sunny day at the beach' }
// ]
console.log(rerankedDocuments);
// ['rainy afternoon in the city', 'sunny day at the beach']Structured Object Reranking
Rerank complex objects (emails, database records, JSON documents):
import { rerank } from 'ai';
import { cohere } from '@ai-sdk/cohere';
interface Email {
from: string;
subject: string;
text: string;
timestamp: string;
}
const emails: Email[] = [
{
from: 'Paul Doe',
subject: 'Follow-up: Discount Offer',
text: 'We are happy to give you a discount of 20% on your next order.',
timestamp: '2024-01-15',
},
{
from: 'John McGill',
subject: 'Oracle Pricing Information',
text: 'Sorry for the delay. Here is the pricing from Oracle: $5000/month for the enterprise plan.',
timestamp: '2024-01-16',
},
{
from: 'Sarah Chen',
subject: 'Meeting Notes',
text: 'Following up on our discussion about database vendors and their pricing models.',
timestamp: '2024-01-14',
},
];
const { ranking, rerankedDocuments } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents: emails,
query: 'What pricing did we get from Oracle?',
topN: 1,
});
console.log(rerankedDocuments[0]);
// { from: 'John McGill', subject: 'Oracle Pricing...', text: '...', ... }
console.log(ranking[0].score); // 0.98 (very confident)topN Results Limiting
Efficient retrieval by limiting results:
// Retrieve only top 3 most relevant from large candidate set
const { ranking } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents: largeDocumentSet, // e.g., 1000 documents
query: 'specific technical question',
topN: 3, // Only process and return top 3
});
// Reduces latency and cost vs returning all ranked resultsScoring and Interpretation
Understanding reranking scores:
const { ranking } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents: ['doc1', 'doc2', 'doc3'],
query: 'query',
});
ranking.forEach((result) => {
const { originalIndex, score, document } = result;
// Score interpretation (Cohere models):
if (score > 0.9) {
console.log(`Highly relevant: ${document}`);
} else if (score > 0.5) {
console.log(`Moderately relevant: ${document}`);
} else {
console.log(`Low relevance: ${document}`);
}
});Score characteristics:
- Range: typically 0-1 (model-dependent)
- Relative: scores are comparable within a single rerank call
- Non-calibrated: 0.8 from one query ≠ 0.8 from another query
- Use thresholds empirically based on your data
---
5. Two-Stage Retrieval Pattern
Combine embedding similarity (fast, broad) with reranking (accurate, focused) for optimal RAG:
Full Pipeline Implementation
import { embed, embedMany, cosineSimilarity, rerank } from 'ai';
import { openai } from '@ai-sdk/openai';
import { cohere } from '@ai-sdk/cohere';
interface Document {
id: string;
content: string;
}
async function twoStageRetrieval(
query: string,
documents: Document[],
candidateCount: number = 20,
finalCount: number = 5
): Promise<Document[]> {
// Stage 1: Embedding similarity (fast, broad retrieval)
const { embedding: queryEmbedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
const { embeddings: docEmbeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: documents.map((doc) => doc.content),
maxParallelCalls: 5,
});
// Calculate similarities
const similarities = documents.map((doc, idx) => ({
document: doc,
similarity: cosineSimilarity(queryEmbedding, docEmbeddings[idx]),
}));
// Get top candidates
similarities.sort((a, b) => b.similarity - a.similarity);
const candidates = similarities.slice(0, candidateCount).map((s) => s.document);
// Stage 2: Rerank candidates (accurate, focused)
const { rerankedDocuments } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents: candidates,
query,
topN: finalCount,
});
return rerankedDocuments;
}
// Usage
const documents: Document[] = [
{ id: '1', content: 'Machine learning fundamentals...' },
{ id: '2', content: 'Neural network architectures...' },
// ... 1000s of documents
];
const results = await twoStageRetrieval(
'Explain backpropagation in neural networks',
documents,
20, // Retrieve 20 candidates with embeddings
5 // Rerank to 5 final results
);Performance Characteristics
Stage 1 (Embedding): O(n) similarity calculations
- Fast: cosine similarity is computationally cheap
- Broad: reduces 10,000 docs → 20 candidates
- Recall-focused: captures all potentially relevant docs
Stage 2 (Reranking): O(k) where k << n
- Slower: transformer-based cross-attention
- Focused: 20 candidates → 5 results
- Precision-focused: accurate relevance scoring
Combined: best of both worlds
- Total time: ~100ms embedding + ~200ms reranking
- vs embedding-only: ~100ms but lower accuracy
- vs reranking-only: ~5000ms (too slow for 10k docs)
---
6. RAG Patterns
Chunking Strategies
Split documents into searchable chunks:
// Fixed-size chunking
function chunkText(text: string, chunkSize: number = 500): string[] {
const words = text.split(/\s+/);
const chunks: string[] = [];
for (let i = 0; i < words.length; i += chunkSize) {
chunks.push(words.slice(i, i + chunkSize).join(' '));
}
return chunks;
}
// Overlapping chunks (better context preservation)
function chunkTextWithOverlap(
text: string,
chunkSize: number = 500,
overlap: number = 100
): string[] {
const words = text.split(/\s+/);
const chunks: string[] = [];
const step = chunkSize - overlap;
for (let i = 0; i < words.length; i += step) {
chunks.push(words.slice(i, i + chunkSize).join(' '));
}
return chunks;
}
// Usage
const document = '...very long document...';
const chunks = chunkTextWithOverlap(document, 500, 100);
// Embed each chunk
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: chunks,
});Vector Database Integration Pattern
Generic pattern for vector DB operations:
import { embedMany } from 'ai';
import { openai } from '@ai-sdk/openai';
interface VectorDBClient {
upsert(vectors: { id: string; values: number[]; metadata: any }[]): Promise<void>;
query(vector: number[], topK: number): Promise<any[]>;
}
async function indexDocuments(
documents: { id: string; content: string; metadata?: any }[],
vectorDB: VectorDBClient
): Promise<void> {
const BATCH_SIZE = 100;
for (let i = 0; i < documents.length; i += BATCH_SIZE) {
const batch = documents.slice(i, i + BATCH_SIZE);
// Generate embeddings
const { embeddings } = await embedMany({
model: openai.embedding('text-embedding-3-small'),
values: batch.map((doc) => doc.content),
maxParallelCalls: 5,
});
// Upsert to vector DB
await vectorDB.upsert(
batch.map((doc, idx) => ({
id: doc.id,
values: embeddings[idx],
metadata: { content: doc.content, ...doc.metadata },
}))
);
}
}
async function searchDocuments(
query: string,
vectorDB: VectorDBClient,
topK: number = 5
): Promise<any[]> {
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-small'),
value: query,
});
return await vectorDB.query(embedding, topK);
}Context Injection into Prompts
Complete RAG pipeline with prompt engineering:
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
async function answerQuestion(
question: string,
vectorDB: VectorDBClient
): Promise<string> {
// Retrieve relevant context
const relevantDocs = await searchDocuments(question, vectorDB, 5);
// Format context
const context = relevantDocs
.map((doc, idx) => `[${idx + 1}] ${doc.metadata.content}`)
.join('\n\n');
// Generate answer with context
const { text } = await generateText({
model: anthropic('claude-3-5-sonnet-20241022'),
prompt: `Answer the following question using only the provided context. If the context doesn't contain enough information, say so.
Context:
${context}
Question: ${question}
Answer:`,
});
return text;
}
// Usage
const answer = await answerQuestion(
'What are the key benefits of RAG?',
vectorDB
);Advanced RAG: Hybrid Search + Reranking
Combine lexical (BM25) and semantic (embeddings) search with reranking:
async function hybridSearchWithReranking(
query: string,
documents: Document[],
vectorDB: VectorDBClient
): Promise<Document[]> {
// 1. Lexical search (BM25, full-text search)
const lexicalResults = await lexicalSearch(query, documents, 30);
// 2. Semantic search (embeddings)
const semanticResults = await searchDocuments(query, vectorDB, 30);
// 3. Merge and deduplicate
const mergedCandidates = deduplicateDocuments([
...lexicalResults,
...semanticResults,
]);
// 4. Rerank merged results
const { rerankedDocuments } = await rerank({
model: cohere.reranking('rerank-v3.5'),
documents: mergedCandidates,
query,
topN: 5,
});
return rerankedDocuments;
}---
Summary
Embedding workflow: 1. Use embedMany() for batch indexing with maxParallelCalls 2. Use embed() for query-time single embeddings 3. Reduce dimensions for storage/speed optimization
Similarity search:
cosineSimilarity()for vector comparison- Threshold filtering for relevance control
- Sort by similarity descending
Reranking:
- Use for top-k candidate refinement (20 → 5)
- Supports strings and structured objects
- Higher accuracy than embeddings alone
Two-stage retrieval:
- Stage 1: Embeddings (fast, broad, high recall)
- Stage 2: Reranking (slow, focused, high precision)
- Best performance/accuracy tradeoff
RAG pipeline: 1. Chunk documents with overlap 2. Embed and index in vector DB 3. Retrieve with hybrid search 4. Rerank candidates 5. Inject context into LLM prompts
MCP Integration Reference
Model Context Protocol (MCP) integration in AI SDK Core v6+ enables dynamic tool discovery, resource access, prompts, and elicitation handling.
---
1) Create an MCP Client
Use HTTP transport for production; use stdio for local servers only.
import { createMCPClient } from '@ai-sdk/mcp';
const mcpClient = await createMCPClient({
transport: {
type: 'http',
url: 'https://your-server.com/mcp',
headers: { Authorization: 'Bearer my-api-key' },
authProvider: myOAuthClientProvider,
},
});Transports
HTTP (recommended)
const mcpClient = await createMCPClient({
transport: { type: 'http', url: 'https://api.example.com/mcp' },
});SSE (alternative HTTP)
const mcpClient = await createMCPClient({
transport: { type: 'sse', url: 'https://api.example.com/sse' },
});Stdio (local only, Node.js)
import { Experimental_StdioMCPTransport } from '@ai-sdk/mcp/mcp-stdio';
const mcpClient = await createMCPClient({
transport: new Experimental_StdioMCPTransport({
command: 'node',
args: ['src/stdio/dist/server.js'],
}),
});You can also use the MCP SDK transports (StdioClientTransport, StreamableHTTPClientTransport, SSEClientTransport) if preferred.
Close the client
let client;
try {
client = await createMCPClient({ transport: { type: 'http', url } });
const tools = await client.tools();
await generateText({ model, tools, prompt });
} finally {
await client?.close();
}For streaming, close in onFinish.
---
2) Load MCP Tools
Schema discovery (load all tools)
const tools = await mcpClient.tools();Schema definition (typed + selective)
import { z } from 'zod';
const tools = await mcpClient.tools({
schemas: {
'get-weather': {
inputSchema: z.object({ location: z.string() }),
outputSchema: z.object({ temperature: z.number(), conditions: z.string() }),
},
'tool-with-no-args': { inputSchema: z.object({}) },
},
});Tip: Use schema definition to load only what you need and keep the tool list small. Use activeTools to further limit tools per request/step.
Typed output with outputSchema
When MCP tools return structuredContent, outputSchema validates and types the result. If structuredContent is missing, the client tries to parse JSON from the text content.
---
3) Use MCP Tools in AI SDK Calls
const tools = await mcpClient.tools();
const result = await generateText({
model,
tools,
stopWhen: stepCountIs(5),
prompt: 'What is the weather in Paris?'
});To combine MCP tools with local tools:
const tools = {
localTool,
...(await mcpClient.tools()),
};---
4) Dynamic Tools + Large Tool Sets
- Use
dynamicTool()when schemas are unknown at compile time. - Use
activeToolsto avoid sending large tool lists every step. - Prefer schema definition over discovery for type safety and control.
---
5) Resources
const resources = await mcpClient.listResources();
const resource = await mcpClient.readResource({ uri: 'file:///path/to/doc.txt' });
const templates = await mcpClient.listResourceTemplates();Resources are app-driven; decide when to fetch and how to pass them as context.
---
6) Prompts (Experimental)
const prompts = await mcpClient.experimental_listPrompts();
const prompt = await mcpClient.experimental_getPrompt({
name: 'code_review',
arguments: { code: 'function add(a,b){return a+b;}' },
});
const result = await generateText({ model, messages: prompt.messages });---
7) Elicitation
Enable elicitation and register a handler:
import { ElicitationRequestSchema } from '@ai-sdk/mcp';
const client = await createMCPClient({
transport: { type: 'sse', url: 'https://server.com/sse' },
capabilities: { elicitation: {} },
});
client.onElicitationRequest(ElicitationRequestSchema, async request => {
const { message, requestedSchema } = request.params;
const userInput = await getInputFromUser(message, requestedSchema);
return { action: 'accept', content: userInput };
});Actions: accept (with content), decline, or cancel.
---
8) Notes and Caveats
- The MCP client is lightweight and does not support notifications or resumable streams.
- Stdio transport is local-only and not suitable for production.
AI SDK Core: Language Model Middleware Reference
Language model middleware intercepts and modifies calls to language models, enabling features like guardrails, RAG, caching, and logging in a model-agnostic way. Middleware can be developed and distributed independently from the models they enhance.
1. Middleware Architecture
wrapLanguageModel()
Wraps a language model with one or more middleware functions:
import { wrapLanguageModel } from 'ai';
const wrappedModel = wrapLanguageModel({
model: yourModel,
middleware: yourMiddleware,
});
// Use wrapped model like any other model
const result = await streamText({
model: wrappedModel,
prompt: 'What cities are in the United States?',
});Core Middleware Interface
Middleware implements one or more of these methods:
import type { LanguageModelMiddleware } from 'ai';
const middleware: LanguageModelMiddleware = {
// Pre-process parameters before doGenerate/doStream
transformParams: async ({ params }) => {
// Modify params.prompt, params.temperature, etc.
return modifiedParams;
},
// Wrap doGenerate (non-streaming)
wrapGenerate: async ({ doGenerate, params }) => {
// Pre-process
const result = await doGenerate();
// Post-process result.text, result.toolCalls, etc.
return result;
},
// Wrap doStream (streaming)
wrapStream: async ({ doStream, params }) => {
const { stream, ...rest } = await doStream();
// Transform stream chunks
return { stream: transformedStream, ...rest };
},
};transformParams: Pre-processing
Modifies parameters before they reach the model (applies to both generate and stream):
const ragMiddleware: LanguageModelMiddleware = {
transformParams: async ({ params }) => {
const lastUserMessage = getLastUserMessageText({ prompt: params.prompt });
if (!lastUserMessage) return params;
const context = await findSources({ text: lastUserMessage });
const instruction =
'Use the following information to answer:\n' +
context.map(chunk => JSON.stringify(chunk)).join('\n');
return addToLastUserMessage({ params, text: instruction });
},
};wrapGenerate: Request/Response Modification
Intercepts non-streaming doGenerate calls:
const guardrailMiddleware: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate, params }) => {
console.log('Request:', params.prompt);
const { text, ...rest } = await doGenerate();
// Filter sensitive information
const cleanedText = text?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN-REDACTED]');
console.log('Response:', cleanedText);
return { text: cleanedText, ...rest };
},
};wrapStream: Streaming Transformation
Intercepts streaming doStream calls with TransformStream:
import type { LanguageModelStreamPart } from 'ai';
const streamLogMiddleware: LanguageModelMiddleware = {
wrapStream: async ({ doStream, params }) => {
const { stream, ...rest } = await doStream();
let generatedText = '';
const textBlocks = new Map<string, string>();
const transformStream = new TransformStream<
LanguageModelStreamPart,
LanguageModelStreamPart
>({
transform(chunk, controller) {
switch (chunk.type) {
case 'text-start':
textBlocks.set(chunk.id, '');
break;
case 'text-delta':
const existing = textBlocks.get(chunk.id) || '';
textBlocks.set(chunk.id, existing + chunk.delta);
generatedText += chunk.delta;
break;
case 'text-end':
console.log(`Block ${chunk.id}:`, textBlocks.get(chunk.id));
break;
}
controller.enqueue(chunk);
},
flush() {
console.log('Total generated:', generatedText);
},
});
return { stream: stream.pipeThrough(transformStream), ...rest };
},
};2. Built-in Middleware
extractReasoningMiddleware
Extracts reasoning from special tags (e.g., <think>...</think> for DeepSeek R1, Claude extended thinking) and exposes as reasoning property:
import { wrapLanguageModel, extractReasoningMiddleware } from 'ai';
const model = wrapLanguageModel({
model: yourModel,
middleware: extractReasoningMiddleware({ tagName: 'think' }),
});
const result = await generateText({
model,
prompt: 'Solve this complex problem...',
});
// Access extracted reasoning
console.log(result.reasoning); // Content inside <think> tags
console.log(result.text); // Final answer (tags removed)Options:
tagName(required): Tag name to extract (e.g., 'think', 'reasoning')startWithReasoning(optional): Prepend reasoning tag to response if model doesn't include it
// For models that don't start with reasoning tag
const deepseekModel = wrapLanguageModel({
model: deepseek('deepseek-reasoner'),
middleware: extractReasoningMiddleware({
tagName: 'think',
startWithReasoning: true, // Adds <think> at start
}),
});simulateStreamingMiddleware
Converts non-streaming responses to streaming format for consistent interface:
import { wrapLanguageModel, simulateStreamingMiddleware } from 'ai';
const model = wrapLanguageModel({
model: nonStreamingModel,
middleware: simulateStreamingMiddleware(),
});
// Now works with streamText even if model doesn't support streaming
for await (const chunk of streamText({ model, prompt: '...' })) {
console.log(chunk);
}defaultSettingsMiddleware
Applies default configuration to all model calls:
import { wrapLanguageModel, defaultSettingsMiddleware } from 'ai';
const model = wrapLanguageModel({
model: yourModel,
middleware: defaultSettingsMiddleware({
settings: {
temperature: 0.5,
maxOutputTokens: 800,
topP: 0.9,
frequencyPenalty: 0.5,
providerOptions: {
openai: { store: false },
},
},
}),
});
// All calls inherit these defaults (can be overridden per-call)
const result = await generateText({
model,
prompt: '...',
// temperature: 0.8, // Overrides default
});addToolInputExamplesMiddleware
Serializes inputExamples into tool descriptions for providers that don't support them natively:
import { wrapLanguageModel, addToolInputExamplesMiddleware, tool } from 'ai';
import { z } from 'zod';
const model = wrapLanguageModel({
model: yourModel,
middleware: addToolInputExamplesMiddleware({
examplesPrefix: 'Input Examples:',
}),
});
const result = await generateText({
model,
tools: {
weather: tool({
description: 'Get the weather in a location',
inputSchema: z.object({
location: z.string(),
}),
inputExamples: [
{ input: { location: 'San Francisco' } },
{ input: { location: 'London' } },
],
}),
},
prompt: 'What is the weather in Tokyo?',
});Transformed description:
Get the weather in a location
Input Examples:
{"location":"San Francisco"}
{"location":"London"}Options:
addToolInputExamplesMiddleware({
examplesPrefix: 'Examples:',
formatExample: (example, index) => `${index + 1}. ${JSON.stringify(example.input)}`,
removeInputExamples: true, // Remove from tool after adding to description
})3. Custom Middleware Patterns
Logging Middleware
Track requests, responses, and performance:
const loggingMiddleware: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate, params }) => {
const start = Date.now();
console.log('Generate request:', {
model: params.modelId,
prompt: params.prompt,
temperature: params.temperature,
});
const result = await doGenerate();
const duration = Date.now() - start;
console.log('Generate response:', {
text: result.text,
usage: result.usage,
duration,
});
return result;
},
wrapStream: async ({ doStream, params }) => {
console.log('Stream request:', params);
const { stream, ...rest } = await doStream();
let generatedText = '';
const transformStream = new TransformStream({
transform(chunk, controller) {
if (chunk.type === 'text-delta') {
generatedText += chunk.delta;
}
controller.enqueue(chunk);
},
flush() {
console.log('Stream complete:', generatedText);
},
});
return { stream: stream.pipeThrough(transformStream), ...rest };
},
};Caching Middleware
Simple in-memory cache for identical requests:
const cache = new Map<string, any>();
const cachingMiddleware: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate, params }) => {
const cacheKey = JSON.stringify(params);
if (cache.has(cacheKey)) {
console.log('Cache hit');
return cache.get(cacheKey);
}
const result = await doGenerate();
cache.set(cacheKey, result);
return result;
},
// Streaming cache implementation (store full response)
wrapStream: async ({ doStream, params }) => {
const cacheKey = JSON.stringify(params);
if (cache.has(cacheKey)) {
// Return cached stream
const cachedResponse = cache.get(cacheKey);
return {
stream: new ReadableStream({
start(controller) {
for (const chunk of cachedResponse.chunks) {
controller.enqueue(chunk);
}
controller.close();
},
}),
...cachedResponse.metadata,
};
}
const { stream, ...rest } = await doStream();
const chunks: any[] = [];
const transformStream = new TransformStream({
transform(chunk, controller) {
chunks.push(chunk);
controller.enqueue(chunk);
},
flush() {
cache.set(cacheKey, { chunks, metadata: rest });
},
});
return { stream: stream.pipeThrough(transformStream), ...rest };
},
};RAG Context Injection
Add retrieved context to user messages:
const ragMiddleware: LanguageModelMiddleware = {
transformParams: async ({ params }) => {
const lastUserMessageText = getLastUserMessageText({
prompt: params.prompt,
});
if (!lastUserMessageText) {
return params; // No user message to augment
}
// Retrieve relevant documents (pseudo-code)
const sources = await findSources({ text: lastUserMessageText });
const instruction =
'Use the following information to answer the question:\n' +
sources.map(chunk => JSON.stringify(chunk)).join('\n');
return addToLastUserMessage({ params, text: instruction });
},
};
// Helper functions (not part of AI SDK)
function getLastUserMessageText({ prompt }) {
if (typeof prompt === 'string') return prompt;
const userMessages = prompt.filter(m => m.role === 'user');
return userMessages[userMessages.length - 1]?.content;
}
function addToLastUserMessage({ params, text }) {
if (typeof params.prompt === 'string') {
return { ...params, prompt: `${text}\n\n${params.prompt}` };
}
const messages = [...params.prompt];
const lastUserIndex = messages.findLastIndex(m => m.role === 'user');
messages[lastUserIndex] = {
...messages[lastUserIndex],
content: `${text}\n\n${messages[lastUserIndex].content}`,
};
return { ...params, prompt: messages };
}Guardrails Middleware
Filter or validate generated content:
const guardrailMiddleware: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate }) => {
const { text, ...rest } = await doGenerate();
// PII filtering
const cleanedText = text
?.replace(/\b\d{3}-\d{2}-\d{4}\b/g, '[SSN-REDACTED]')
?.replace(/\b[\w.%+-]+@[\w.-]+\.[A-Z]{2,}\b/gi, '[EMAIL-REDACTED]');
return { text: cleanedText, ...rest };
},
// Note: Streaming guardrails are difficult because you don't know
// the full content until the stream finishes
wrapStream: async ({ doStream }) => {
const { stream, ...rest } = await doStream();
let fullText = '';
const transformStream = new TransformStream({
transform(chunk, controller) {
if (chunk.type === 'text-delta') {
fullText += chunk.delta;
}
controller.enqueue(chunk);
},
flush(controller) {
// Can only validate after stream completes
if (containsProhibitedContent(fullText)) {
controller.error(new Error('Content policy violation'));
}
},
});
return { stream: stream.pipeThrough(transformStream), ...rest };
},
};4. Composition
Chaining Multiple Middleware
Apply multiple middleware in sequence:
const wrappedModel = wrapLanguageModel({
model: yourModel,
middleware: [ragMiddleware, loggingMiddleware, cachingMiddleware],
});
// Applied as: ragMiddleware(loggingMiddleware(cachingMiddleware(yourModel)))
// Execution order:
// 1. RAG adds context
// 2. Logging wraps the call
// 3. Caching checks/stores resultOrder of Execution
Middleware executes in reverse order (last-to-first for wrapping):
const middleware1: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate }) => {
console.log('Middleware 1: before');
const result = await doGenerate();
console.log('Middleware 1: after');
return result;
},
};
const middleware2: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate }) => {
console.log('Middleware 2: before');
const result = await doGenerate();
console.log('Middleware 2: after');
return result;
},
};
wrapLanguageModel({ model, middleware: [middleware1, middleware2] });
// Output:
// Middleware 1: before
// Middleware 2: before
// [actual model call]
// Middleware 2: after
// Middleware 1: afterMiddleware + Custom Providers
Middleware works with both official and custom providers:
import { createOpenAI } from '@ai-sdk/openai';
const customOpenAI = createOpenAI({
baseURL: 'https://custom-endpoint.example.com/v1',
apiKey: process.env.CUSTOM_API_KEY,
});
const wrappedModel = wrapLanguageModel({
model: customOpenAI('gpt-4'),
middleware: [loggingMiddleware, cachingMiddleware],
});Custom Metadata in Middleware
Pass request-specific context via providerOptions:
const metadataLoggingMiddleware: LanguageModelMiddleware = {
wrapGenerate: async ({ doGenerate, params }) => {
const metadata = params?.providerMetadata?.yourLogMiddleware;
console.log('User ID:', metadata?.userId);
console.log('Session:', metadata?.sessionId);
const result = await doGenerate();
return result;
},
};
const { text } = await generateText({
model: wrapLanguageModel({
model: yourModel,
middleware: metadataLoggingMiddleware,
}),
prompt: 'Invent a new holiday',
providerOptions: {
yourLogMiddleware: {
userId: '12345',
sessionId: 'abc-def',
timestamp: Date.now(),
},
},
});5. Embedding Middleware
wrapEmbeddingModel()
Similar pattern for embedding models (inferred from language model pattern):
import type { EmbeddingModelV1Middleware } from '@ai-sdk/provider';
const embeddingMiddleware: EmbeddingModelV1Middleware = {
transformParams: async ({ params }) => {
// Pre-process embedding parameters
return params;
},
wrapEmbed: async ({ doEmbed, params }) => {
const result = await doEmbed();
// Post-process embeddings
return result;
},
};
const wrappedEmbedding = wrapEmbeddingModel({
model: yourEmbeddingModel,
middleware: embeddingMiddleware,
});Custom Embedding Transformations
Normalize or transform embedding vectors:
const normalizeEmbeddingMiddleware: EmbeddingModelV1Middleware = {
wrapEmbed: async ({ doEmbed }) => {
const result = await doEmbed();
// Normalize embeddings to unit vectors
const normalizedEmbeddings = result.embeddings.map(embedding => {
const magnitude = Math.sqrt(
embedding.reduce((sum, val) => sum + val * val, 0)
);
return embedding.map(val => val / magnitude);
});
return { ...result, embeddings: normalizedEmbeddings };
},
};Community Middleware
Custom Tool Call Parser
@ai-sdk-tool/parser enables function calling for models without native support:
import { wrapLanguageModel } from 'ai';
import { gemmaToolMiddleware } from '@ai-sdk-tool/parser';
const model = wrapLanguageModel({
model: openrouter('google/gemma-3-27b-it'),
middleware: gemmaToolMiddleware,
});
// Now Gemma supports tool calls
const result = await generateText({
model,
tools: {
weather: tool({ /* ... */ }),
},
prompt: 'What is the weather in SF?',
});Available variants:
createToolMiddleware: Custom tool call formatshermesToolMiddleware: Hermes/Qwen formatgemmaToolMiddleware: Gemma 3 format
Best Practices
1. Keep middleware focused: Each middleware should have a single responsibility 2. Handle both generate and stream: Implement both unless your use case is specific 3. Preserve original behavior: Don't break the model contract; add, don't replace 4. Use TransformStream for streaming: Native stream transformation prevents buffering 5. Cache carefully: Consider memory limits and cache invalidation 6. Log to stderr: stdout is reserved for MCP JSON-RPC in server contexts 7. Test middleware independently: Unit test each middleware before composition 8. Document side effects: Make it clear if middleware performs I/O or mutations
References
AI SDK v6 Migration Guide
Comprehensive guide for migrating from AI SDK v5 to v6 stable.
Package Versions
pnpm add ai@^6.0.3 @ai-sdk/openai@^3.0.1 @ai-sdk/anthropic@^3.0.1 @ai-sdk/google@^3.0.1 @ai-sdk/react@^3.0.3---
Automated Migration
Step 1: Run Codemod
npx @ai-sdk/codemod v6The codemod automatically handles:
textEmbeddingModel()→embeddingModel()MockLanguageModelV2→MockLanguageModelMockEmbeddingModelV2→MockEmbeddingModelToolCallOptions→ToolExecutionOptionsCoreMessage→ModelMessageconvertToCoreMessages→convertToModelMessagesLanguageModelV2Middleware→LanguageModelMiddlewareLanguageModelV2StreamPart→LanguageModelStreamPart
Step 2: Update Packages
pnpm add ai@^6.0.3 @ai-sdk/openai@^3.0.1 @ai-sdk/anthropic@^3.0.1 @ai-sdk/google@^3.0.1 @ai-sdk/react@^3.0.3Step 3: Manual Fixes
Async convertToModelMessages
The function is now async and must be awaited:
// Before (v5)
const messages = convertToCoreMessages(uiMessages);
// After (v6)
const messages = await convertToModelMessages(uiMessages);Output API (Recommended for Structured Data)
The Output API provides a unified interface for structured outputs with generateText/streamText:
// Before (deprecated but still works)
import { generateObject } from 'ai';
import { z } from 'zod';
const { object } = await generateObject({
model: openai('gpt-4o'),
schema: z.object({ name: z.string() }),
prompt: 'Generate a name',
});
// After (v6 - preferred)
import { generateText, Output } from 'ai';
import { z } from 'zod';
const { output } = await generateText({
model: openai('gpt-4o'),
output: Output.object({
schema: z.object({ name: z.string() }),
}),
prompt: 'Generate a name',
});Output API variants:
Output.object({ schema })- Single typed objectOutput.array({ element })- Array of objectsOutput.choice({ options })- Enum classificationOutput.json()- Untyped JSONOutput.text()- Plain text (default)
Embedding Model Method
// Before (v5)
const model = openai.textEmbeddingModel('text-embedding-3-small');
// or
const model = openai.embeddingModel('text-embedding-3-small');
// After (v6)
const model = openai.embedding('text-embedding-3-small');MCP Client (Now Stable)
// Before (v5)
import { experimental_createMCPClient as createMCPClient } from '@ai-sdk/mcp';
// After (v6)
import { createMCPClient } from '@ai-sdk/mcp';Step 4: Type Check
pnpm type-check---
Breaking Changes Reference
| Before (v5) | After (v6) |
|---|---|
CoreMessage | ModelMessage |
convertToCoreMessages | await convertToModelMessages |
ToolCallOptions | ToolExecutionOptions |
textEmbeddingModel() | embedding() |
embeddingModel() | embedding() |
MockLanguageModelV2 | MockLanguageModel |
MockEmbeddingModelV2 | MockEmbeddingModel |
LanguageModelV2Middleware | LanguageModelMiddleware |
LanguageModelV2StreamPart | LanguageModelStreamPart |
Finish reason 'unknown' | 'other' |
experimental_createMCPClient | createMCPClient |
---
New Features in v6
Output API
Unified structured output with text generation:
import { generateText, Output } from 'ai';
import { z } from 'zod';
// Object output
const { output } = await generateText({
model: openai('gpt-4o'),
output: Output.object({
schema: z.object({
name: z.string(),
age: z.number(),
}),
}),
prompt: 'Generate a person',
});
// Array output
const { output: people } = await generateText({
model: openai('gpt-4o'),
output: Output.array({
element: z.object({ name: z.string() }),
}),
prompt: 'Generate 3 names',
});
// Choice output (enum)
const { output: category } = await generateText({
model: openai('gpt-4o'),
output: Output.choice({
options: ['tech', 'business', 'sports'],
}),
prompt: 'Classify this article',
});Tool Enhancements
needsApproval
Request human approval before tool execution:
import { tool } from 'ai';
import { z } from 'zod';
const deleteTool = tool({
description: 'Delete a file',
inputSchema: z.object({ path: z.string() }),
needsApproval: true, // Requires approval before execution
execute: async ({ path }) => {
await deleteFile(path);
return { success: true };
},
});inputExamples
Provide example inputs for better model guidance:
const searchTool = tool({
description: 'Search for information',
inputSchema: z.object({
query: z.string(),
filters: z.object({
date: z.string().optional(),
}).optional(),
}),
inputExamples: [
{ input: { query: 'AI news', filters: { date: '2024-01-15' } } },
{ input: { query: 'weather forecast' } },
],
execute: async ({ query, filters }) => {
// Implementation
},
});toModelOutput
Transform tool results for model consumption:
const apiTool = tool({
description: 'Call an API',
inputSchema: z.object({ endpoint: z.string() }),
execute: async ({ endpoint }) => {
const response = await fetch(endpoint);
return await response.json();
},
toModelOutput: (result) => {
// Return simplified version for model context
return JSON.stringify(result, null, 2).slice(0, 1000);
},
});DevTools
Visual debugging and inspection:
import { devtools } from '@ai-sdk/devtools';
// Wrap your model for debugging
const debugModel = devtools(openai('gpt-4o'));
const { text } = await generateText({
model: debugModel,
prompt: 'Hello',
});Reranking with Bedrock
AWS Bedrock reranking support:
import { rerank } from 'ai';
import { bedrock } from '@ai-sdk/amazon-bedrock';
const { ranking, rerankedDocuments } = await rerank({
model: bedrock.reranking('amazon.rerank-v1:0'),
documents: ['doc1', 'doc2', 'doc3'],
query: 'search query',
topN: 2,
});---
Provider Updates
OpenAI
- Responses API is now default (use
openai.chat()for Chat API) - o3/o4 models with
reasoningEffortandreasoningSummary - strictJsonSchema defaults to
true - Built-in tools:
webSearch,fileSearch,imageGeneration,codeInterpreter,mcp - Prompt caching with
promptCacheKeyandpromptCacheRetention
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
const { text, reasoning } = await generateText({
model: openai('gpt-5'),
prompt: 'Complex reasoning task',
providerOptions: {
openai: {
reasoningEffort: 'high',
reasoningSummary: 'detailed',
},
},
});Anthropic
- Claude 4 models:
claude-opus-4,claude-sonnet-4,claude-haiku-4 - Agent skills:
pptx,docx,xlsx,pdf - structuredOutputMode option
- MCP connectors for server integration
- Web search and code execution tools
import { anthropic } from '@ai-sdk/anthropic';
import { generateText } from 'ai';
const { text, reasoning } = await generateText({
model: anthropic('claude-opus-4-20250514'),
prompt: 'Complex task',
providerOptions: {
anthropic: {
thinking: { type: 'enabled', budgetTokens: 12000 },
},
},
});- Gemini 3 models with
thinkingLevelconfiguration - Gemini 2.5 with
thinkingBudget - Built-in tools:
googleSearch,codeExecution,fileSearch,urlContext - Implicit caching with 75% discount
import { google } from '@ai-sdk/google';
import { generateText } from 'ai';
const { text, reasoning } = await generateText({
model: google('gemini-3-pro-preview'),
prompt: 'Math problem',
providerOptions: {
google: {
thinkingConfig: {
thinkingLevel: 'high',
includeThoughts: true,
},
},
},
});AI Gateway
- OIDC authentication for Vercel deployments
- BYOK (Bring Your Own Key) support
- Dynamic model discovery with
gateway.getAvailableModels() - Credit tracking with
gateway.getCredits()
import { gateway } from 'ai';
// OIDC auth is automatic on Vercel
const { text } = await generateText({
model: 'openai/gpt-5',
prompt: 'Hello',
});
// Model discovery
const models = await gateway.getAvailableModels();---
Testing Migration
Update test mocks:
// Before (v5)
import { MockLanguageModelV2 } from 'ai/test';
const mockModel = new MockLanguageModelV2({ /* ... */ });
// After (v6)
import { MockLanguageModel } from 'ai/test';
const mockModel = new MockLanguageModel({ /* ... */ });---
Troubleshooting
"convertToCoreMessages is not a function"
The function was renamed. Update to:
import { convertToModelMessages } from 'ai';
const messages = await convertToModelMessages(uiMessages);"Property 'textEmbeddingModel' does not exist"
Use the new method name:
const model = openai.embedding('text-embedding-3-small');"MockLanguageModelV2 is not exported"
Update the import:
import { MockLanguageModel } from 'ai/test';Finish reason 'unknown' not in union
Update type checks:
// Before
if (result.finishReason === 'unknown') { /* ... */ }
// After
if (result.finishReason === 'other') { /* ... */ }AI SDK Core - Production Patterns Reference
Comprehensive guide to production-ready patterns for AI SDK Core v6, covering telemetry, error handling, testing, prompt engineering, and media APIs.
Table of Contents
- Telemetry (OpenTelemetry)
- DevTools
- Error Handling
- Testing Patterns
- Prompt Engineering
- Media APIs (Experimental)
---
Telemetry (OpenTelemetry)
Status: Experimental (may change)
AI SDK uses OpenTelemetry for standardized observability instrumentation.
Enabling Telemetry
For Next.js apps, follow the Next.js OpenTelemetry guide first.
Enable telemetry per function call using experimental_telemetry:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Write a short story about a cat.',
experimental_telemetry: { isEnabled: true },
});Recording Controls
Control recording of inputs and outputs (default: both enabled). Disable for privacy, data transfer, or performance reasons:
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Sensitive user data here...',
experimental_telemetry: {
isEnabled: true,
recordInputs: false, // Don't record sensitive prompts
recordOutputs: true, // Record outputs only
},
});Telemetry Metadata
Add custom identifiers and metadata for tracking:
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Write a short story about a cat.',
experimental_telemetry: {
isEnabled: true,
functionId: 'story-generator-v2',
metadata: {
userId: 'user-123',
feature: 'creative-writing',
environment: 'production',
},
},
});Custom Tracer Provider
Provide a custom TracerProvider instead of using the @opentelemetry/api singleton:
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const tracerProvider = new NodeTracerProvider();
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Write a short story about a cat.',
experimental_telemetry: {
isEnabled: true,
tracer: tracerProvider.getTracer('ai'),
},
});Span Types
generateText Spans
`ai.generateText` (span): Full function call, contains 1+ ai.generateText.doGenerate spans
Attributes:
operation.name:ai.generateText+ functionIdai.operationId:"ai.generateText"ai.prompt: Input promptai.response.text: Generated textai.response.toolCalls: Tool calls (stringified JSON)ai.response.finishReason: Completion reasonai.settings.maxOutputTokens: Max output tokens
`ai.generateText.doGenerate` (span): Individual provider call, can contain ai.toolCall spans
Attributes:
operation.name:ai.generateText.doGenerate+ functionIdai.operationId:"ai.generateText.doGenerate"ai.prompt.messages: Messages passed to providerai.prompt.tools: Tool definitions array (stringified)ai.prompt.toolChoice: Tool choice setting (stringified JSON)ai.response.text: Generated textai.response.toolCalls: Tool calls (stringified JSON)ai.response.finishReason: Completion reason
`ai.toolCall` (span): Individual tool execution
Attributes:
operation.name:"ai.toolCall"ai.operationId:"ai.toolCall"ai.toolCall.name: Tool nameai.toolCall.id: Tool call IDai.toolCall.args: Input parametersai.toolCall.result: Output result (if serializable and successful)
streamText Spans & Events
`ai.streamText` (span): Full streaming call, contains ai.streamText.doStream
Attributes: Same as ai.generateText
`ai.streamText.doStream` (span): Provider streaming call
Additional attributes:
ai.response.msToFirstChunk: Time to first chunk (ms)ai.response.msToFinish: Time to finish part (ms)ai.response.avgCompletionTokensPerSecond: Average tokens/second
`ai.stream.firstChunk` (event): Emitted on first chunk
ai.response.msToFirstChunk: Time to first chunk
`ai.stream.finish` (event): Emitted on stream completion
generateObject Spans
`ai.generateObject` (span): Full function call
Additional attributes:
ai.schema: JSON schema (stringified)ai.schema.name: Schema nameai.schema.description: Schema descriptionai.response.object: Generated object (stringified JSON)ai.settings.output: Output type (object,no-schema)
`ai.generateObject.doGenerate` (span): Provider call
streamObject Spans
`ai.streamObject` (span): Full streaming call
`ai.streamObject.doStream` (span): Provider streaming call
Additional attributes:
ai.response.msToFirstChunk: Time to first chunk
embed Spans
`ai.embed` (span): Full embedding call
Attributes:
ai.value: Input valueai.embedding: JSON-stringified embedding
`ai.embed.doEmbed` (span): Provider call
Attributes:
ai.values: Input values arrayai.embeddings: Embeddings array (stringified)
embedMany Spans
`ai.embedMany` (span): Batch embedding call
`ai.embedMany.doEmbed` (span): Provider batch call
Basic LLM Span Information
All LLM spans include:
resource.name: functionIdai.model.id: Model IDai.model.provider: Provider nameai.request.headers.*: Request headersai.response.providerMetadata: Provider-specific metadataai.settings.maxRetries: Max retriesai.telemetry.functionId: Function IDai.telemetry.metadata.*: Custom metadataai.usage.completionTokens: Completion tokensai.usage.promptTokens: Prompt tokens
Call LLM Span Information
Individual LLM call spans add:
ai.response.model: Actual model used (may differ from requested)ai.response.id: Response IDai.response.timestamp: Response timestamp- Semantic Conventions for GenAI:
gen_ai.system: Provider namegen_ai.request.model: Requested modelgen_ai.request.temperature: Temperature settinggen_ai.request.max_tokens: Max tokensgen_ai.request.frequency_penalty: Frequency penaltygen_ai.request.presence_penalty: Presence penaltygen_ai.request.top_k: Top Kgen_ai.request.top_p: Top Pgen_ai.request.stop_sequences: Stop sequencesgen_ai.response.finish_reasons: Finish reasonsgen_ai.usage.input_tokens: Input tokensgen_ai.usage.output_tokens: Output tokens
---
DevTools
Debug AI SDK applications with the official DevTools during development.
Installation & Usage
# Install
npm install @ai-sdk/devtools
# Run DevTools server
npx @ai-sdk/devtoolsDevTools provides a local web interface at http://localhost:3001 for debugging AI SDK calls.
Features
- Request/Response Inspection: Real-time view of all AI SDK calls
- Token Usage Visualization: See input/output token counts per call
- Latency Breakdown: Per-provider timing metrics
- Tool Call Tracing: Step-by-step tool execution visibility
- Stream Debugging: Inspect streaming responses chunk by chunk
- Message History: Full conversation context for multi-turn sessions
Integration with Telemetry
DevTools complements OpenTelemetry. Use both for comprehensive observability:
| Tool | Use Case | Environment |
|---|---|---|
| DevTools | Interactive debugging UI | Development |
| OpenTelemetry | Traces, metrics, alerting | Production |
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Write a short story.',
// DevTools captures this automatically when running
experimental_telemetry: {
isEnabled: true,
functionId: 'story-generator',
},
});DevTools automatically captures all AI SDK calls in the same Node.js process. No code changes required—just start the DevTools server alongside your application.
---
Error Handling
AI SDK Core provides typed errors for robust error handling in production.
Error Types
AI_APICallError: API call failuresAI_NoContentGeneratedError: No content in responseAI_InvalidPromptError: Invalid prompt formatAI_InvalidModelError: Invalid model configurationAI_NoImageGeneratedError: Image generation failedAI_NoSpeechGeneratedError: Speech generation failedAI_NoTranscriptGeneratedError: Transcription failed
Synchronous Error Handling
Use try/catch for regular errors:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const { text } = await generateText({
model: openai('gpt-4'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
console.log(text);
} catch (error) {
// Handle API errors, invalid prompts, etc.
console.error('Generation failed:', error);
}Streaming Error Handling (Simple Streams)
For streams without error chunk support, errors throw as regular errors:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const { textStream } = streamText({
model: openai('gpt-4'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
for await (const textPart of textStream) {
process.stdout.write(textPart);
}
} catch (error) {
console.error('Stream failed:', error);
}Streaming Error Handling (Full Streams)
Full streams support error parts for in-stream error handling:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const { fullStream } = streamText({
model: openai('gpt-4'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
for await (const part of fullStream) {
switch (part.type) {
case 'text-delta':
process.stdout.write(part.textDelta);
break;
case 'error': {
const error = part.error;
console.error('Stream error:', error);
// Handle error (log, retry, notify user)
break;
}
case 'abort': {
console.log('Stream aborted by user');
// Handle stream abort
break;
}
case 'tool-error': {
const error = part.error;
console.error('Tool execution error:', error);
// Handle tool-specific errors
break;
}
case 'finish':
console.log('\nStream completed');
break;
}
}
} catch (error) {
// Handle errors outside streaming (setup, network)
console.error('Stream initialization failed:', error);
}onError and onAbort Callbacks
Use callbacks for cleanup and state updates:
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { textStream } = streamText({
model: openai('gpt-4'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
onAbort: ({ steps }) => {
// Called when stream aborted via AbortSignal (onFinish NOT called)
console.log('Stream aborted after', steps.length, 'steps');
// Update UI state, save partial results, etc.
},
onFinish: ({ steps, totalUsage, text }) => {
// Called on normal completion (NOT called on abort)
console.log('Stream completed normally');
console.log('Total tokens:', totalUsage.totalTokens);
},
});
for await (const textPart of textStream) {
process.stdout.write(textPart);
}onAbort receives:
steps: Array of completed steps before abort
Handling Abort Events Directly
import { streamText } from 'ai';
import { openai } from '@ai-sdk/openai';
const { fullStream } = streamText({
model: openai('gpt-4'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
});
for await (const chunk of fullStream) {
switch (chunk.type) {
case 'abort': {
console.log('Stream was aborted');
// Perform cleanup immediately
break;
}
case 'text-delta':
process.stdout.write(chunk.textDelta);
break;
}
}Tool-Specific Error Extraction
Extract and handle tool errors separately:
import { generateText, stepCountIs, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const result = await generateText({
model: openai('gpt-4'),
tools: {
weather: tool({
description: 'Get weather for a location',
inputSchema: z.object({
location: z.string(),
}),
execute: async ({ location }) => {
// Simulate tool error
throw new Error(`Weather API unavailable for ${location}`);
},
}),
},
prompt: 'What is the weather in Paris?',
stopWhen: stepCountIs(3),
});
// Check for tool errors in response
const toolErrors = result.steps.flatMap(step =>
step.content.filter(part => part.type === 'tool-error')
);
for (const toolError of toolErrors) {
console.error('Tool error:', toolError.toolName, toolError.error);
}---
Testing Patterns
AI SDK Core provides mock providers and helpers for deterministic, fast, cost-free testing.
Mock Providers
Import from ai/test:
MockLanguageModel: Mock language model (v6)MockEmbeddingModel: Mock embedding model (v6)mockId: Incrementing integer ID generatormockValues: Iterator over values arraysimulateReadableStream: Simulates streams with delays
Note: In v6, MockLanguageModelV2 was renamed to MockLanguageModel and MockEmbeddingModelV2 to MockEmbeddingModel.
Testing generateText
import { generateText } from 'ai';
import { MockLanguageModel } from 'ai/test';
const result = await generateText({
model: new MockLanguageModel({
doGenerate: async () => ({
finishReason: 'stop',
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
content: [{ type: 'text', text: 'Hello, world!' }],
warnings: [],
}),
}),
prompt: 'Hello, test!',
});
expect(result.text).toBe('Hello, world!');
expect(result.usage.totalTokens).toBe(30);Testing streamText
import { streamText } from 'ai';
import { MockLanguageModel, simulateReadableStream } from 'ai/test';
const result = streamText({
model: new MockLanguageModel({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-start', id: 'text-1' },
{ type: 'text-delta', id: 'text-1', delta: 'Hello' },
{ type: 'text-delta', id: 'text-1', delta: ', ' },
{ type: 'text-delta', id: 'text-1', delta: 'world!' },
{ type: 'text-end', id: 'text-1' },
{
type: 'finish',
finishReason: 'stop',
logprobs: undefined,
usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13 },
},
],
}),
}),
}),
prompt: 'Hello, test!',
});
const parts: string[] = [];
for await (const part of result.textStream) {
parts.push(part);
}
expect(parts.join('')).toBe('Hello, world!');Testing generateObject
import { generateObject } from 'ai';
import { MockLanguageModel } from 'ai/test';
import { z } from 'zod';
const result = await generateObject({
model: new MockLanguageModel({
doGenerate: async () => ({
finishReason: 'stop',
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
content: [{ type: 'text', text: '{"content":"Hello, world!"}' }],
warnings: [],
}),
}),
schema: z.object({ content: z.string() }),
prompt: 'Hello, test!',
});
expect(result.object).toEqual({ content: 'Hello, world!' });Testing streamObject
import { streamObject } from 'ai';
import { MockLanguageModel, simulateReadableStream } from 'ai/test';
import { z } from 'zod';
const result = streamObject({
model: new MockLanguageModel({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-start', id: 'text-1' },
{ type: 'text-delta', id: 'text-1', delta: '{ ' },
{ type: 'text-delta', id: 'text-1', delta: '"content": ' },
{ type: 'text-delta', id: 'text-1', delta: '"Hello, ' },
{ type: 'text-delta', id: 'text-1', delta: 'world' },
{ type: 'text-delta', id: 'text-1', delta: '!"' },
{ type: 'text-delta', id: 'text-1', delta: ' }' },
{ type: 'text-end', id: 'text-1' },
{
type: 'finish',
finishReason: 'stop',
logprobs: undefined,
usage: { inputTokens: 3, outputTokens: 10, totalTokens: 13 },
},
],
}),
}),
}),
schema: z.object({ content: z.string() }),
prompt: 'Hello, test!',
});
const finalObject = await result.object;
expect(finalObject).toEqual({ content: 'Hello, world!' });Simulating UI Message Streams
Test or debug UI message streams with controlled delays:
import { simulateReadableStream } from 'ai';
// Next.js route example
export async function POST(req: Request) {
return new Response(
simulateReadableStream({
initialDelayInMs: 1000, // Delay before first chunk
chunkDelayInMs: 300, // Delay between chunks
chunks: [
'data: {"type":"start","messageId":"msg-123"}\n\n',
'data: {"type":"text-start","id":"text-1"}\n\n',
'data: {"type":"text-delta","id":"text-1","delta":"This"}\n\n',
'data: {"type":"text-delta","id":"text-1","delta":" is an"}\n\n',
'data: {"type":"text-delta","id":"text-1","delta":" example."}\n\n',
'data: {"type":"text-end","id":"text-1"}\n\n',
'data: {"type":"finish"}\n\n',
'data: [DONE]\n\n',
],
}).pipeThrough(new TextEncoderStream()),
{
status: 200,
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
'x-vercel-ai-ui-message-stream': 'v1',
},
},
);
}---
Prompt Engineering
Best practices for effective prompts, especially for tools and structured data.
Prompts for Tools
As tool count/complexity increases, results degrade. Follow these tips:
1. Use strong models: gpt-5, gpt-4.1 excel at tool calling. Weaker models struggle. 2. Limit tool count: Keep to 5 or fewer tools. 3. Simplify schemas: Avoid deep nesting, excessive optional fields, complex unions. 4. Use semantic names: Meaningful tool names, parameters, and properties help models understand intent. 5. Add descriptions: Use .describe("...") on Zod schema properties. 6. Document tool outputs: If tool output is unclear or tools depend on each other, describe the output in the tool's description. 7. Include examples: Provide example input/output JSON in prompts for complex tools.
Example with descriptions:
import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const result = await generateText({
model: openai('gpt-4'),
tools: {
weather: tool({
description: 'Get current weather for a location. Returns temperature in Celsius and conditions.',
inputSchema: z.object({
location: z.string().describe('City name, e.g. "San Francisco" or "London"'),
units: z.enum(['celsius', 'fahrenheit']).describe('Temperature units'),
}),
execute: async ({ location, units }) => {
return { temperature: 22, conditions: 'Sunny', units };
},
}),
},
prompt: 'What is the weather in Paris?',
});Tool & Structured Data Schemas
Zod Dates
Models return dates as strings, not Date objects. Use transformations:
import { generateObject } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const result = await generateObject({
model: openai('gpt-4'),
schema: z.object({
events: z.array(
z.object({
event: z.string(),
date: z
.iso.date() // Validates ISO 8601 date string
.transform(value => new Date(value)), // Converts to Date
}),
),
}),
prompt: 'List 5 important events from the year 2000.',
});
console.log(result.object.events[0].date instanceof Date); // trueOptional Parameters (Strict Schemas)
For strict schema validation (e.g., OpenAI structured outputs), use .nullable() instead of .optional():
import { tool } from 'ai';
import { z } from 'zod';
// ❌ This may fail with strict schema validation
const failingTool = tool({
description: 'Execute a command',
inputSchema: z.object({
command: z.string(),
workdir: z.string().optional(), // May cause errors
timeout: z.string().optional(),
}),
});
// ✅ This works with strict schema validation
const workingTool = tool({
description: 'Execute a command',
inputSchema: z.object({
command: z.string(),
workdir: z.string().nullable(), // Use nullable
timeout: z.string().nullable(),
}),
});Temperature Settings
For deterministic tool calls and object generation, use temperature: 0:
import { generateText, tool } from 'ai';
import { openai } from '@ai-sdk/openai';
import { z } from 'zod';
const result = await generateText({
model: openai('gpt-4'),
temperature: 0, // Deterministic tool calls
tools: {
executeCommand: tool({
description: 'Execute a shell command',
inputSchema: z.object({
command: z.string(),
}),
execute: async ({ command }) => {
// Execute command
return { output: 'Command executed' };
},
}),
},
prompt: 'Execute the ls command',
});Lower temperatures reduce randomness, crucial for:
- Structured data generation
- Precise tool calls with correct parameters
- Consistent schema adherence
Debugging
Inspecting Warnings
Check call warnings to ensure your prompt/tools are handled correctly:
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Hello, world!',
temperature: 2.5, // May trigger warning if unsupported
});
console.log(result.warnings);
// Check for unsupported parameters, deprecations, etc.HTTP Request Bodies
Inspect raw HTTP request bodies for debugging (provider-specific):
import { generateText } from 'ai';
import { openai } from '@ai-sdk/openai';
const result = await generateText({
model: openai('gpt-4'),
prompt: 'Hello, world!',
});
console.log(result.request.body);
// View exact payload sent to provider---
Media APIs (Experimental)
AI SDK Core provides experimental APIs for image generation, speech synthesis, and audio transcription.
generateImage
Generate images from text prompts using image models.
import { experimental_generateImage as generateImage } from 'ai';
import { openai } from '@ai-sdk/openai';
const { image } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
});
const base64 = image.base64; // Base64 image data
const uint8Array = image.uint8Array; // Uint8Array binary dataSize and Aspect Ratio
Specify size (format: {width}x{height}) or aspect ratio (format: {width}:{height}). Supported values vary by model.
// Size (OpenAI)
const { image } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
size: '1024x1024', // 1024x1024, 1792x1024, 1024x1792
});
// Aspect Ratio (Google Vertex)
import { vertex } from '@ai-sdk/google-vertex';
const { image: vertexImage } = await generateImage({
model: vertex.image('imagen-3.0-generate-002'),
prompt: 'Santa Claus driving a Cadillac',
aspectRatio: '16:9', // 1:1, 3:4, 4:3, 9:16, 16:9
});Generating Multiple Images
const { images } = await generateImage({
model: openai.image('dall-e-2'),
prompt: 'Santa Claus driving a Cadillac',
n: 4, // Generate 4 images
});
// SDK batches requests automatically based on model limits
// Override with maxImagesPerCall:
const { images: batchedImages } = await generateImage({
model: openai.image('dall-e-2'),
prompt: 'Santa Claus driving a Cadillac',
maxImagesPerCall: 5, // Custom batch size
n: 10, // 2 calls of 5 images each
});Seed for Reproducibility
const { image } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
seed: 1234567890, // Same seed = same image (if supported)
});Provider-Specific Options
const { image } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
size: '1024x1024',
providerOptions: {
openai: {
style: 'vivid', // 'vivid' or 'natural'
quality: 'hd', // 'hd' or 'standard'
},
},
});Error Handling
import {
experimental_generateImage as generateImage,
NoImageGeneratedError,
} from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const { image } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
});
} catch (error) {
if (NoImageGeneratedError.isInstance(error)) {
console.log('NoImageGeneratedError');
console.log('Cause:', error.cause);
console.log('Responses:', error.responses);
}
}generateSpeech
Generate speech audio from text using text-to-speech models.
import { experimental_generateSpeech as generateSpeech } from 'ai';
import { openai } from '@ai-sdk/openai';
const audio = await generateSpeech({
model: openai.speech('tts-1'),
text: 'Hello, world!',
voice: 'alloy', // OpenAI voices: alloy, echo, fable, onyx, nova, shimmer
});
const audioData = audio.audioData; // Uint8ArrayLanguage Setting
import { experimental_generateSpeech as generateSpeech } from 'ai';
import { lmnt } from '@ai-sdk/lmnt';
const audio = await generateSpeech({
model: lmnt.speech('aurora'),
text: 'Hola, mundo!',
language: 'es', // Spanish (provider support varies)
});Provider-Specific Options
const audio = await generateSpeech({
model: openai.speech('tts-1'),
text: 'Hello, world!',
voice: 'alloy',
providerOptions: {
openai: {
speed: 1.25, // 0.25 to 4.0
},
},
});Error Handling
import {
experimental_generateSpeech as generateSpeech,
NoSpeechGeneratedError,
} from 'ai';
import { openai } from '@ai-sdk/openai';
try {
const audio = await generateSpeech({
model: openai.speech('tts-1'),
text: 'Hello, world!',
voice: 'alloy',
});
} catch (error) {
if (NoSpeechGeneratedError.isInstance(error)) {
console.log('NoSpeechGeneratedError');
console.log('Cause:', error.cause);
console.log('Responses:', error.responses);
}
}transcribe
Transcribe audio to text using transcription models.
import { experimental_transcribe as transcribe } from 'ai';
import { openai } from '@ai-sdk/openai';
import { readFile } from 'fs/promises';
const transcript = await transcribe({
model: openai.transcription('whisper-1'),
audio: await readFile('audio.mp3'), // Uint8Array, ArrayBuffer, Buffer, base64 string, or URL
});
const text = transcript.text; // "Hello, world!"
const segments = transcript.segments; // Segments with timestamps (if available)
const language = transcript.language; // "en" (if available)
const durationInSeconds = transcript.durationInSeconds; // Duration (if available)Provider-Specific Options
const transcript = await transcribe({
model: openai.transcription('whisper-1'),
audio: await readFile('audio.mp3'),
providerOptions: {
openai: {
timestampGranularities: ['word'], // Get word-level timestamps
},
},
});
// Access word-level segments
for (const segment of transcript.segments ?? []) {
console.log(`[${segment.start}s - ${segment.end}s]: ${segment.text}`);
}Error Handling
import {
experimental_transcribe as transcribe,
NoTranscriptGeneratedError,
} from 'ai';
import { openai } from '@ai-sdk/openai';
import { readFile } from 'fs/promises';
try {
const transcript = await transcribe({
model: openai.transcription('whisper-1'),
audio: await readFile('audio.mp3'),
});
} catch (error) {
if (NoTranscriptGeneratedError.isInstance(error)) {
console.log('NoTranscriptGeneratedError');
console.log('Cause:', error.cause);
console.log('Responses:', error.responses);
}
}Common Media API Settings
All media APIs support:
Abort Signals and Timeouts
const result = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
abortSignal: AbortSignal.timeout(5000), // 5 second timeout
});Custom Headers
const result = await generateSpeech({
model: openai.speech('tts-1'),
text: 'Hello, world!',
voice: 'alloy',
headers: { 'X-Custom-Header': 'custom-value' },
});Warnings
const { image, warnings } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
});
console.log(warnings); // Check for unsupported parametersProvider Metadata
const { image, providerMetadata } = await generateImage({
model: openai.image('dall-e-3'),
prompt: 'Santa Claus driving a Cadillac',
});
// OpenAI returns revised prompts
const revisedPrompt = providerMetadata.openai.images[0]?.revisedPrompt;
console.log('Original:', 'Santa Claus driving a Cadillac');
console.log('Revised:', revisedPrompt);AI SDK Core: Provider Configuration Reference
Comprehensive reference for configuring and using AI providers with Vercel AI SDK v6.
OpenAI Provider
Setup
import { openai } from '@ai-sdk/openai';
import { createOpenAI } from '@ai-sdk/openai';
// Default instance (uses OPENAI_API_KEY env var)
const model = openai('gpt-5');
// Custom instance with settings
const custom = createOpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: 'https://api.openai.com/v1',
organization: 'org-123',
project: 'proj-456',
headers: { 'Custom-Header': 'value' },
fetch: customFetchImpl,
});Responses API vs Chat API
OpenAI provider defaults to the Responses API (since AI SDK 5). Explicitly select APIs:
// Responses API (default)
const model = openai('gpt-5');
const model2 = openai.responses('gpt-5');
// Chat API
const chatModel = openai.chat('gpt-5');
// Completion API (legacy)
const completionModel = openai.completion('gpt-3.5-turbo-instruct');Responses API Provider Options
import { openai, OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
import { generateText } from 'ai';
const result = await generateText({
model: openai('gpt-5'),
prompt: 'Explain quantum computing',
providerOptions: {
openai: {
parallelToolCalls: false,
store: false,
user: 'user_123',
maxToolCalls: 10,
metadata: { session: 'abc123' },
// Conversation continuity
conversation: 'conv_id_from_api',
previousResponseId: 'resp_xyz',
instructions: 'Updated system instructions',
// Reasoning models (o3, o4, gpt-5)
reasoningEffort: 'high', // 'none' | 'minimal' | 'low' | 'medium' | 'high'
reasoningSummary: 'detailed', // 'auto' | 'detailed'
// Structured outputs
strictJsonSchema: true, // enabled by default
// Service tier
serviceTier: 'flex', // 'auto' | 'flex' | 'priority' | 'default'
// Verbosity control
textVerbosity: 'medium', // 'low' | 'medium' | 'high'
// Advanced options
include: ['file_search_call.results', 'message.output_text.logprobs'],
truncation: 'auto', // or 'disabled'
// Prompt caching
promptCacheKey: 'my-custom-cache-key-123',
promptCacheRetention: '24h', // 'in_memory' | '24h' (GPT-5.1 only)
safetyIdentifier: 'user-stable-id',
} satisfies OpenAIResponsesProviderOptions,
},
});
// Access response metadata
const { responseId, cachedPromptTokens, reasoningTokens } =
result.providerMetadata?.openai;Reasoning Output
For reasoning models (o3, o4-mini, gpt-5), enable reasoning summaries:
// Streaming reasoning
const result = streamText({
model: openai('gpt-5'),
prompt: 'Explain the Mission burrito debate in San Francisco.',
providerOptions: {
openai: {
reasoningSummary: 'detailed', // 'auto' or 'detailed'
},
},
});
for await (const part of result.fullStream) {
if (part.type === 'reasoning') {
console.log(`Reasoning: ${part.textDelta}`);
} else if (part.type === 'text-delta') {
process.stdout.write(part.textDelta);
}
}
// Non-streaming reasoning
const { text, reasoning } = await generateText({
model: openai('gpt-5'),
prompt: 'Complex reasoning task',
providerOptions: {
openai: { reasoningSummary: 'auto' },
},
});
console.log('Reasoning:', reasoning);Built-in Tools (Responses API)
// Web Search Tool
const result = await generateText({
model: openai('gpt-5'),
prompt: 'What happened in San Francisco last week?',
tools: {
web_search: openai.tools.webSearch({
externalWebAccess: true,
searchContextSize: 'high',
userLocation: {
type: 'approximate',
city: 'San Francisco',
region: 'California',
},
}),
},
toolChoice: { type: 'tool', toolName: 'web_search' }, // force usage
});
// File Search Tool
const result = await generateText({
model: openai('gpt-5'),
prompt: 'What does the document say about authentication?',
tools: {
file_search: openai.tools.fileSearch({
vectorStoreIds: ['vs_123'],
maxNumResults: 5,
filters: { key: 'author', type: 'eq', value: 'Jane Smith' },
ranking: { ranker: 'auto', scoreThreshold: 0.5 },
}),
},
providerOptions: {
openai: { include: ['file_search_call.results'] },
},
});
// Image Generation Tool (gpt-5 variants)
const result = await generateText({
model: openai('gpt-5'),
prompt: 'Generate an image of an echidna swimming.',
tools: {
image_generation: openai.tools.imageGeneration({
outputFormat: 'webp',
quality: 'low',
}),
},
});
for (const toolResult of result.staticToolResults) {
if (toolResult.toolName === 'image_generation') {
const base64Image = toolResult.output.result;
}
}
// Code Interpreter Tool
const result = await generateText({
model: openai('gpt-5'),
prompt: 'Calculate the factorial of 10',
tools: {
code_interpreter: openai.tools.codeInterpreter({
container: { fileIds: ['file-123', 'file-456'] },
}),
},
});
// MCP Tool
const result = await generateText({
model: openai('gpt-5'),
prompt: 'Search for AI developments',
tools: {
mcp: openai.tools.mcp({
serverLabel: 'web-search',
serverUrl: 'https://mcp.exa.ai/mcp',
serverDescription: 'A web-search API for AI agents',
allowedTools: ['search', 'summarize'],
authorization: 'Bearer token',
headers: { 'X-Custom': 'value' },
}),
},
});Chat API Provider Options
import { openai, OpenAIChatLanguageModelOptions } from '@ai-sdk/openai';
await generateText({
model: openai.chat('gpt-5'),
prompt: 'Hello',
providerOptions: {
openai: {
logitBias: { '50256': -100 },
logprobs: true, // or number for top N
parallelToolCalls: true,
user: 'test-user',
reasoningEffort: 'medium',
maxCompletionTokens: 2048,
store: true,
metadata: { custom: 'value' },
serviceTier: 'priority',
strictJsonSchema: true,
textVerbosity: 'low',
promptCacheKey: 'cache-key',
promptCacheRetention: '24h',
safetyIdentifier: 'user-id',
// Predicted outputs (gpt-4o, gpt-4o-mini)
prediction: {
type: 'content',
content: existingCode,
},
} satisfies OpenAIChatLanguageModelOptions,
},
});
// Access logprobs
const { providerMetadata } = await generateText({ /* ... */ });
const logprobs = providerMetadata?.openai?.logprobs;Prompt Caching
OpenAI automatically caches prompts ≥1024 tokens (5-10 min TTL, up to 1 hour off-peak):
const { text, usage, providerMetadata } = await generateText({
model: openai.chat('gpt-4o-mini'),
prompt: 'A 1024-token or longer prompt...',
});
console.log('Cache hits:', providerMetadata?.openai?.cachedPromptTokens);
// Manual cache control
const result = await generateText({
model: openai.chat('gpt-5'),
prompt: 'Long prompt...',
providerOptions: {
openai: { promptCacheKey: 'my-custom-cache-key-123' },
},
});
// Extended caching (GPT-5.1, 24h TTL)
const result = await generateText({
model: openai.chat('gpt-5.1'),
prompt: 'Long prompt...',
providerOptions: {
openai: {
promptCacheKey: 'cache-key',
promptCacheRetention: '24h',
},
},
});Image & PDF Inputs
// Image input (Chat & Responses API)
const result = await generateText({
model: openai('gpt-5'),
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'Describe the image.' },
{ type: 'image', image: fs.readFileSync('./image.png') },
// Or URL: { type: 'image', image: 'https://example.com/img.png' }
// Or file ID: { type: 'image', image: 'file-8EFBcWHsQxZV7YGezBC1fq' }
// Image detail control (Chat API only)
{
type: 'image',
image: 'https://example.com/img.png',
providerOptions: {
openai: { imageDetail: 'low' }, // 'low' | 'high' | 'auto'
},
},
],
}],
});
// PDF input
const result = await generateText({
model: openai('gpt-5'),
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'What is an embedding model?' },
{
type: 'file',
data: fs.readFileSync('./ai.pdf'),
mediaType: 'application/pdf',
filename: 'ai.pdf', // optional
},
// Or URL: { type: 'file', data: 'https://example.com/doc.pdf', mediaType: 'application/pdf' }
// Or file ID: { type: 'file', data: 'file-8EFBcWHsQxZV7YGezBC1fq', mediaType: 'application/pdf' }
],
}],
});Embeddings
import { openai } from '@ai-sdk/openai';
import { embed } from 'ai';
const { embedding } = await embed({
model: openai.embedding('text-embedding-3-large'),
value: 'sunny day at the beach',
providerOptions: {
openai: {
dimensions: 512, // custom dimensions (text-embedding-3 only)
user: 'test-user',
},
},
});Anthropic Provider
Setup
import { anthropic } from '@ai-sdk/anthropic';
import { createAnthropic } from '@ai-sdk/anthropic';
// Default instance (uses ANTHROPIC_API_KEY env var)
const model = anthropic('claude-opus-4-20250514');
// Custom instance
const custom = createAnthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
baseURL: 'https://api.anthropic.com/v1',
headers: { 'Custom-Header': 'value' },
fetch: customFetchImpl,
});Claude Model Lineup
// Opus: Most capable, complex tasks
anthropic('claude-opus-4-5');
anthropic('claude-opus-4-20250514');
// Sonnet: Balanced performance and speed
anthropic('claude-sonnet-4-5');
anthropic('claude-sonnet-4-20250514');
anthropic('claude-3-7-sonnet-20250219');
// Haiku: Fastest, simple tasks
anthropic('claude-haiku-4-5');
anthropic('claude-3-5-haiku-latest');Provider Options
import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic';
const result = await generateText({
model: anthropic('claude-opus-4-20250514'),
prompt: 'Explain quantum computing',
providerOptions: {
anthropic: {
// Disable parallel tool calls
disableParallelToolUse: false,
// Include reasoning content in requests
sendReasoning: true,
// Effort level (claude-opus-4-5)
effort: 'high', // 'low' | 'medium' | 'high'
// Thinking/reasoning
thinking: {
type: 'enabled',
budgetTokens: 12000,
},
// Tool streaming
toolStreaming: true,
// Structured output mode
structuredOutputMode: 'auto', // 'outputFormat' | 'jsonTool' | 'auto'
} satisfies AnthropicProviderOptions,
},
});Thinking/Reasoning (Claude Opus 4, Sonnet 4, 3.7)
const { text, reasoning, reasoningDetails } = await generateText({
model: anthropic('claude-opus-4-20250514'),
prompt: 'How many people will live in the world in 2040?',
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 12000, // thinking token budget
},
} satisfies AnthropicProviderOptions,
},
});
console.log('Reasoning:', reasoning); // reasoning text
console.log('Reasoning details:', reasoningDetails); // includes redacted reasoning
console.log('Response:', text);Prompt Caching (Ephemeral, 24h retention)
Cache content using providerOptions on messages or message parts:
const errorMessage = '... long error message ...';
const result = await generateText({
model: anthropic('claude-3-5-sonnet-20240620'),
messages: [{
role: 'user',
content: [
{ type: 'text', text: 'You are a JavaScript expert.' },
{
type: 'text',
text: `Error message: ${errorMessage}`,
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
},
{ type: 'text', text: 'Explain the error message.' },
],
}],
});
// Check cached token usage
console.log(result.providerMetadata?.anthropic);
// e.g. { cacheCreationInputTokens: 2118 }
// Cache control on system messages
const result = await generateText({
model: anthropic('claude-3-5-sonnet-20240620'),
messages: [
{
role: 'system',
content: 'Cached system message part',
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
},
{
role: 'system',
content: 'Uncached system message part',
},
{ role: 'user', content: 'User prompt' },
],
});
// Longer cache TTL (1 hour)
{
type: 'text',
text: 'Long cached message',
providerOptions: {
anthropic: {
cacheControl: { type: 'ephemeral', ttl: '1h' },
},
},
}
// Cache control on tools
const result = await generateText({
model: anthropic('claude-3-5-haiku-latest'),
tools: {
cityAttractions: tool({
inputSchema: z.object({ city: z.string() }),
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral' } },
},
}),
},
messages: [{ role: 'user', content: 'User prompt' }],
});Minimum cacheable lengths:
- Claude Opus 4.5: 4096 tokens
- Claude Opus 4.1, 4, Sonnet 4.5, 4, 3.7, Opus 3: 1024 tokens
- Claude Haiku 4.5: 4096 tokens
- Claude Haiku 3.5, 3: 2048 tokens
Built-in Tools
// Web Search Tool (claude-opus-4, claude-sonnet-4)
const result = await generateText({
model: anthropic('claude-opus-4-20250514'),
prompt: 'Latest AI developments',
tools: {
web_search: anthropic.tools.webSearch_20250305({
maxUses: 5,
allowedDomains: ['techcrunch.com', 'wired.com'],
blockedDomains: ['example-spam-site.com'],
userLocation: {
type: 'approximate',
country: 'US',
region: 'California',
city: 'San Francisco',
timezone: 'America/Los_Angeles',
},
}),
},
});
// Web Fetch Tool
const result = await generateText({
model: anthropic('claude-sonnet-4-0'),
prompt: 'What is this page about? https://en.wikipedia.org/wiki/Maglemosian_culture',
tools: {
web_fetch: anthropic.tools.webFetch_20250910({
maxUses: 1,
allowedDomains: ['wikipedia.org'],
blockedDomains: ['ads.example.com'],
citations: { enabled: true },
maxContentTokens: 10000,
}),
},
});
// Code Execution Tool
const result = await generateText({
model: anthropic('claude-opus-4-20250514'),
prompt: 'Calculate the mean and standard deviation of [1, 2, 3, 4, 5]',
tools: {
code_execution: anthropic.tools.codeExecution_20250825(),
},
});
// Tool Search (BM25 or Regex)
const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'What is the weather in San Francisco?',
tools: {
toolSearch: anthropic.tools.toolSearchBm25_20251119(),
// or: toolSearchRegex_20251119()
get_weather: tool({
description: 'Get the current weather at a location',
inputSchema: z.object({
location: z.string().describe('The city and state'),
}),
execute: async ({ location }) => ({ /* ... */ }),
providerOptions: {
anthropic: { deferLoading: true },
},
}),
},
});MCP Connectors
const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: 'Call the echo tool with "hello world".',
providerOptions: {
anthropic: {
mcpServers: [{
type: 'url',
name: 'echo',
url: 'https://echo.mcp.inevitable.fyi/mcp',
authorizationToken: mcpAuthToken,
toolConfiguration: {
enabled: true,
allowedTools: ['echo'],
},
}],
} satisfies AnthropicProviderOptions,
},
});Agent Skills
const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
tools: {
code_execution: anthropic.tools.codeExecution_20250825(),
},
prompt: 'Create a presentation about renewable energy with 5 slides',
providerOptions: {
anthropic: {
container: {
skills: [
{
type: 'anthropic', // or 'custom'
skillId: 'pptx', // Built-in: 'pptx', 'docx', 'pdf', 'xlsx'
version: 'latest', // optional
},
],
},
} satisfies AnthropicProviderOptions,
},
});Google Generative AI Provider
Setup
import { google } from '@ai-sdk/google';
import { createGoogleGenerativeAI } from '@ai-sdk/google';
// Default instance (uses GOOGLE_GENERATIVE_AI_API_KEY env var)
const model = google('gemini-2.5-flash');
// Custom instance
const custom = createGoogleGenerativeAI({
apiKey: process.env.GOOGLE_GENERATIVE_AI_API_KEY,
baseURL: 'https://generativelanguage.googleapis.com/v1beta',
headers: { 'Custom-Header': 'value' },
fetch: customFetchImpl,
});Gemini Models
// Gemini 3 (newest, with thinking levels)
google('gemini-3-pro-preview');
// Gemini 2.5 (production-ready)
google('gemini-2.5-pro');
google('gemini-2.5-flash');
google('gemini-2.5-flash-lite');
// Gemini 2.0
google('gemini-2.0-flash');
// Gemini 1.5
google('gemini-1.5-pro');
google('gemini-1.5-flash');
google('gemini-1.5-flash-8b');Provider Options
import { google, GoogleGenerativeAIProviderOptions } from '@ai-sdk/google';
const result = await generateText({
model: google('gemini-2.5-flash'),
prompt: 'Explain quantum computing',
providerOptions: {
google: {
// Cached content
cachedContent: 'cachedContents/{cachedContent}',
// Structured outputs
structuredOutputs: true, // disable to avoid schema limitations
// Safety settings
safetySettings: [{
category: 'HARM_CATEGORY_HATE_SPEECH',
threshold: 'BLOCK_LOW_AND_ABOVE',
}],
// Response modalities
responseModalities: ['TEXT', 'IMAGE'],
// Image generation config
imageConfig: {
aspectRatio: '16:9', // 1:1, 2:3, 3:2, 3:4, 4:3, 4:5, 5:4, 9:16, 16:9, 21:9
},
} satisfies GoogleGenerativeAIProviderOptions,
},
});
// Access safety ratings
const metadata = result.providerMetadata?.google;
console.log(metadata?.safetyRatings);
console.log(metadata?.usageMetadata);Thinking Configuration
// Gemini 3: thinkingLevel
const { text, reasoning } = await generateText({
model: google('gemini-3-pro-preview'),
prompt: 'What is the sum of the first 10 prime numbers?',
providerOptions: {
google: {
thinkingConfig: {
thinkingLevel: 'high', // 'low' | 'high'
includeThoughts: true,
},
} satisfies GoogleGenerativeAIProviderOptions,
},
});
console.log('Reasoning:', reasoning);
// Gemini 2.5: thinkingBudget
const { text, reasoning } = await generateText({
model: google('gemini-2.5-flash'),
prompt: 'Complex reasoning task',
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 8192, // thinking token budget
includeThoughts: true,
},
} satisfies GoogleGenerativeAIProviderOptions,
},
});Built-in Tools
// Google Search
const { text, sources, providerMetadata } = await generateText({
model: google('gemini-2.5-flash'),
tools: {
google_search: google.tools.googleSearch({}),
},
prompt: 'List the top 5 San Francisco news from the past week.',
});
const metadata = providerMetadata?.google;
console.log(metadata?.groundingMetadata);
// Code Execution
const { text, toolCalls, toolResults } = await generateText({
model: google('gemini-2.5-pro'),
tools: {
code_execution: google.tools.codeExecution({}),
},
prompt: 'Use python to calculate the 20th fibonacci number.',
});
// File Search (Gemini 2.5)
const { text, sources } = await generateText({
model: google('gemini-2.5-pro'),
tools: {
file_search: google.tools.fileSearch({
fileSearchStoreNames: [
'projects/my-project/locations/us/fileSearchStores/my-store',
],
metadataFilter: 'author = "Robert Graves"',
topK: 8,
}),
},
prompt: "Summarise the key themes of 'I, Claudius'.",
});
// URL Context (Gemini 2.0+)
const { text, sources, providerMetadata } = await generateText({
model: google('gemini-2.5-flash'),
prompt: 'Based on the document: https://ai.google.dev/gemini-api/docs/url-context. Answer: How many links per request?',
tools: {
url_context: google.tools.urlContext({}),
},
});
const urlMetadata = providerMetadata?.google?.urlContextMetadata;Prompt Caching
Gemini 2.5 has implicit caching (automatic 75% discount on cached content):
// Implicit caching (automatic)
const baseContext = 'Long context (1024+ tokens for Flash, 2048+ for Pro)...';
const { text } = await generateText({
model: google('gemini-2.5-pro'),
prompt: `${baseContext}\n\nQuestion 1`,
});
const { text: text2, providerMetadata } = await generateText({
model: google('gemini-2.5-pro'),
prompt: `${baseContext}\n\nQuestion 2`, // Cache hit!
});
console.log(providerMetadata?.google?.usageMetadata?.cachedContentTokenCount);
// Explicit caching (Gemini 2.5, 2.0)
import { GoogleAICacheManager } from '@google/generative-ai/server';
const cacheManager = new GoogleAICacheManager(process.env.GOOGLE_GENERATIVE_AI_API_KEY);
const { name: cachedContent } = await cacheManager.create({
model: 'gemini-2.5-pro',
contents: [{ role: 'user', parts: [{ text: '1000 Lasagna Recipes...' }] }],
ttlSeconds: 60 * 5,
});
const { text } = await generateText({
model: google('gemini-2.5-pro'),
prompt: 'Write a vegetarian lasagna recipe for 4 people.',
providerOptions: {
google: { cachedContent },
},
});AI Gateway Provider
Setup
import { gateway } from 'ai';
import { createGateway } from 'ai';
// Default instance (uses AI_GATEWAY_API_KEY env var or OIDC)
const { text } = await generateText({
model: 'openai/gpt-5',
prompt: 'Hello world',
});
// Or use gateway provider instance
const { text } = await generateText({
model: gateway('openai/gpt-5'),
prompt: 'Hello world',
});
// Custom instance
const custom = createGateway({
apiKey: process.env.AI_GATEWAY_API_KEY,
baseURL: 'https://ai-gateway.vercel.sh/v1/ai',
headers: { 'Custom-Header': 'value' },
fetch: customFetchImpl,
metadataCacheRefreshMillis: 300000, // 5 minutes
});OIDC Authentication (Vercel Deployments)
Automatic OIDC authentication on Vercel:
# Production/Preview: automatic
# Local development:
vercel env pull # Download OIDC token
vercel dev # Auto-refresh tokensBYOK (Bring Your Own Key)
Add provider credentials in Vercel team's AI Gateway settings. No code changes needed.
Provider Options
import type { GatewayProviderOptions } from '@ai-sdk/gateway';
const { text } = await generateText({
model: 'anthropic/claude-sonnet-4',
prompt: 'Explain quantum computing',
providerOptions: {
gateway: {
// Provider routing
order: ['vertex', 'anthropic'], // Try Vertex first
only: ['vertex', 'anthropic'], // Limit to these providers
// Model fallbacks
models: ['openai/gpt-5-nano', 'gemini-2.0-flash'],
// Usage tracking
user: 'user-abc-123', // Track by end-user
tags: ['document-summary', 'premium-feature'], // Categorize usage
} satisfies GatewayProviderOptions,
},
});Provider-Specific Options Through Gateway
Use actual provider names for provider-specific options:
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import type { GatewayProviderOptions } from '@ai-sdk/gateway';
const { text } = await generateText({
model: 'anthropic/claude-sonnet-4',
prompt: 'Explain quantum computing',
providerOptions: {
gateway: {
order: ['vertex', 'anthropic'],
} satisfies GatewayProviderOptions,
anthropic: {
thinking: { type: 'enabled', budgetTokens: 12000 },
} satisfies AnthropicProviderOptions,
},
});Dynamic Model Discovery
import { gateway } from 'ai';
const availableModels = await gateway.getAvailableModels();
availableModels.models.forEach(model => {
console.log(`${model.id}: ${model.name}`);
if (model.pricing) {
console.log(` Input: $${model.pricing.input}/token`);
console.log(` Output: $${model.pricing.output}/token`);
if (model.pricing.cachedInputTokens) {
console.log(` Cached read: $${model.pricing.cachedInputTokens}/token`);
}
}
});
// Use discovered model
const { text } = await generateText({
model: availableModels.models[0].id,
prompt: 'Hello world',
});Credit Usage Tracking
import { gateway } from 'ai';
const credits = await gateway.getCredits();
console.log(`Team balance: ${credits.balance} credits`);
console.log(`Team total used: ${credits.total_used} credits`);Provider Management (v6)
Custom Provider (Pre-configured Models)
import { customProvider, gateway, wrapLanguageModel, defaultSettingsMiddleware } from 'ai';
// Model aliases with custom settings
export const openai = customProvider({
languageModels: {
// Replacement model with custom options
'gpt-5.1': wrapLanguageModel({
model: gateway('openai/gpt-5.1'),
middleware: defaultSettingsMiddleware({
settings: {
providerOptions: {
openai: { reasoningEffort: 'high' },
},
},
}),
}),
// Alias with custom options
'gpt-5.1-high-reasoning': wrapLanguageModel({
model: gateway('openai/gpt-5.1'),
middleware: defaultSettingsMiddleware({
settings: {
providerOptions: {
openai: { reasoningEffort: 'high' },
},
},
}),
}),
},
fallbackProvider: gateway,
});
// Simple model aliases
export const anthropic = customProvider({
languageModels: {
opus: gateway('anthropic/claude-opus-4.1'),
sonnet: gateway('anthropic/claude-sonnet-4.5'),
haiku: gateway('anthropic/claude-haiku-4.5'),
},
fallbackProvider: gateway,
});
// Limit available models (no fallback)
export const myProvider = customProvider({
languageModels: {
'text-medium': gateway('anthropic/claude-3-5-sonnet-20240620'),
'text-small': gateway('openai/gpt-5-mini'),
},
embeddingModels: {
embedding: gateway.embeddingModel('openai/text-embedding-3-small'),
},
// no fallback provider
});Provider Registry (Multi-provider Apps)
import { anthropic } from '@ai-sdk/anthropic';
import { openai } from '@ai-sdk/openai';
import { createProviderRegistry, gateway } from 'ai';
// Create registry
export const registry = createProviderRegistry({
gateway,
anthropic,
openai,
});
// Custom separator
export const customRegistry = createProviderRegistry(
{ gateway, anthropic, openai },
{ separator: ' > ' }
);
// Use language models
const { text } = await generateText({
model: registry.languageModel('openai:gpt-5.1'),
// or: customRegistry.languageModel('openai > gpt-5.1')
prompt: 'Invent a new holiday.',
});
// Use embeddings
const { embedding } = await embed({
model: registry.embeddingModel('openai:text-embedding-3-small'),
value: 'sunny day',
});
// Use image models
const { image } = await generateImage({
model: registry.imageModel('openai:dall-e-3'),
prompt: 'A sunset over the ocean',
});Combined Example (Custom + Registry + Middleware)
import { anthropic, AnthropicProviderOptions } from '@ai-sdk/anthropic';
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
import { xai } from '@ai-sdk/xai';
import { groq } from '@ai-sdk/groq';
import {
createProviderRegistry,
customProvider,
defaultSettingsMiddleware,
gateway,
wrapLanguageModel,
} from 'ai';
export const registry = createProviderRegistry(
{
// Pass through gateway
gateway,
// Pass through full provider
xai,
// Custom OpenAI-compatible provider
custom: createOpenAICompatible({
name: 'provider-name',
apiKey: process.env.CUSTOM_API_KEY,
baseURL: 'https://api.custom.com/v1',
}),
// Model aliases with custom settings
anthropic: customProvider({
languageModels: {
fast: anthropic('claude-haiku-4-5'),
writing: anthropic('claude-sonnet-4-5'),
reasoning: wrapLanguageModel({
model: anthropic('claude-sonnet-4-5'),
middleware: defaultSettingsMiddleware({
settings: {
maxOutputTokens: 100000,
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 32000,
},
} satisfies AnthropicProviderOptions,
},
},
}),
}),
},
fallbackProvider: anthropic,
}),
// Limited models without fallback
groq: customProvider({
languageModels: {
'gemma2-9b-it': groq('gemma2-9b-it'),
'qwen-qwq-32b': groq('qwen-qwq-32b'),
},
}),
},
{ separator: ' > ' }
);
// Usage
const model = registry.languageModel('anthropic > reasoning');Global Provider Configuration
// setup.ts (initialize once during startup)
import { openai } from '@ai-sdk/openai';
globalThis.AI_SDK_DEFAULT_PROVIDER = openai;
// app.ts (use without prefix)
import { streamText } from 'ai';
const result = await streamText({
model: 'gpt-5.1', // Uses global provider (OpenAI)
prompt: 'Invent a new holiday.',
});Common Provider Option Patterns
providerOptions Syntax
import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai';
import type { AnthropicProviderOptions } from '@ai-sdk/anthropic';
import type { GoogleGenerativeAIProviderOptions } from '@ai-sdk/google';
import type { GatewayProviderOptions } from '@ai-sdk/gateway';
const result = await generateText({
model: 'openai/gpt-5',
prompt: 'Hello',
providerOptions: {
openai: {
reasoningEffort: 'high',
} satisfies OpenAIResponsesProviderOptions,
anthropic: {
thinking: { type: 'enabled', budgetTokens: 12000 },
} satisfies AnthropicProviderOptions,
google: {
thinkingConfig: { thinkingLevel: 'high' },
} satisfies GoogleGenerativeAIProviderOptions,
gateway: {
order: ['vertex', 'anthropic'],
} satisfies GatewayProviderOptions,
},
});Reasoning/Thinking Across Providers
// OpenAI reasoning
providerOptions: {
openai: {
reasoningEffort: 'high', // 'none' | 'minimal' | 'low' | 'medium' | 'high'
reasoningSummary: 'detailed', // 'auto' | 'detailed'
},
}
// Anthropic thinking
providerOptions: {
anthropic: {
thinking: {
type: 'enabled',
budgetTokens: 12000,
},
},
}
// Google thinking (Gemini 3)
providerOptions: {
google: {
thinkingConfig: {
thinkingLevel: 'high', // 'low' | 'high'
includeThoughts: true,
},
},
}
// Google thinking (Gemini 2.5)
providerOptions: {
google: {
thinkingConfig: {
thinkingBudget: 8192,
includeThoughts: true,
},
},
}Caching Across Providers
// OpenAI prompt caching
providerOptions: {
openai: {
promptCacheKey: 'cache-key',
promptCacheRetention: '24h', // GPT-5.1 only
},
}
// Anthropic prompt caching
messages: [{
role: 'user',
content: [{
type: 'text',
text: 'Cached content',
providerOptions: {
anthropic: { cacheControl: { type: 'ephemeral', ttl: '1h' } },
},
}],
}]
// Google caching
providerOptions: {
google: {
cachedContent: 'cachedContents/abc123',
},
}This reference covers the major provider configuration patterns in AI SDK v6. For specific edge cases and advanced features, consult the individual provider documentation pages.
Related skills
FAQ
Which function should I use for structured JSON?
Use generateObject for non-streaming structured JSON and streamObject for streaming it, both with a Zod schema.
How does AI SDK Core integrate with MCP?
Use createMCPClient() to load MCP tools, resources, and prompts, preferring HTTP transport for production and Experimental_StdioMCPTransport only for local Node.js servers, and close clients after use.