Backend AI with Vercel AI SDK v5 and v6. **Installation:** ```
npm install ai @ai-sdk/openai @ai-sdk/anthropic @ai-sdk/google zodgenerateObject() and streamObject() are DEPRECATED and will be removed in a future version. Use the new Output API instead.// ❌ DEPRECATED - will be removed import { generateObject } from 'ai'; const result = await generateObject({ model: openai('gpt-5'), schema: z.object({ name: z.string(), age: z.number() }), prompt: 'Generate a person', }); `**After (v6 - USE THIS):**` // ✅ NEW OUTPUT API import { generateText, Output } from 'ai'; const result = await generateText({ model: openai('gpt-5'), output: Output.object({ schema: z.object({ name: z.string(), age: z.number() }) }), prompt: 'Generate a person', }); // Access the typed object console.log(result.object); // { name: "Alice", age: 30 } `### Output Types` import { generateText, Output } from 'ai'; // Object with Zod schema output: Output.object({ schema: myZodSchema }) // Array of typed objects output: Output.array({ schema: personSchema }) // Enum/choice from options output: Output.choice({ choices: ['positive', 'negative', 'neutral'] }) // Plain text (explicit) output: Output.text() // Unstructured JSON (no schema validation) output: Output.json() `### Streaming with Output API` import { streamText, Output } from 'ai'; const result = streamText({ model: openai('gpt-5'), output: Output.object({ schema: personSchema }), prompt: 'Generate a person', }); // Stream partial objects for await (const partialObject of result.objectStream) { console.log(partialObject); // { name: "Ali..." } -> { name: "Alice", age: ... } } // Get final object const finalObject = await result.object;
ToolLoopAgent class:tools: { payment: tool({ // Dynamic approval based on input needsApproval: async ({ amount }) => amount > 1000, inputSchema: z.object({ amount: z.number() }), execute: async ({ amount }) => { /* process payment */ }, }), readFile: tool({ needsApproval: false, // Safe operations don't need approval inputSchema: z.object({ path: z.string() }), execute: async ({ path }) => fs.readFile(path), }), deleteFile: tool({ needsApproval: true, // Destructive operations always need approval inputSchema: z.object({ path: z.string() }), execute: async ({ path }) => fs.unlink(path), }), }
import { rerank } from 'ai'; const result = await rerank({ model: cohere.reranker('rerank-v3.5'), query: 'user question', documents: searchResults, topK: 5, });
import { experimental_createMCPClient } from 'ai'; const mcpClient = await experimental_createMCPClient({ transport: { type: 'stdio', command: 'npx', args: ['-y', '@modelcontextprotocol/server-filesystem'] }, }); const tools = await mcpClient.tools(); const result = await generateText({ model: openai('gpt-5'), tools, prompt: 'List files in the current directory', });
generateText() instead of streamText() for MCP tools.// ❌ RISKY: Dynamic tools change without your control const mcpClient = await experimental_createMCPClient({ /* ... */ }); const tools = await mcpClient.tools(); // Can change anytime! // ✅ SAFE: Generate static, versioned tool definitions // Step 1: Install mcp-to-ai-sdk npm install -g mcp-to-ai-sdk // Step 2: Generate static tools (one-time, version controlled) npx mcp-to-ai-sdk generate stdio 'npx -y @modelcontextprotocol/server-filesystem' // Step 3: Import static tools import { tools } from './generated-mcp-tools'; const result = await generateText({ model: openai('gpt-5'), tools, // Static, reviewed, versioned prompt: 'Use tools', });
import { wrapLanguageModel, extractReasoningMiddleware } from 'ai'; const wrappedModel = wrapLanguageModel({ model: anthropic('claude-sonnet-4-5-20250929'), middleware: extractReasoningMiddleware({ tagName: 'think' }), }); // Reasoning extracted automatically from <think>...</think> tags `**6\. Telemetry (OpenTelemetry)**` const result = await generateText({ model: openai('gpt-5'), prompt: 'Hello', experimental_telemetry: { isEnabled: true, functionId: 'my-chat-function', metadata: { userId: '123' }, recordInputs: true, recordOutputs: true, }, });
import { openai } from '@ai-sdk/openai'; const gpt52 = openai('gpt-5.2'); const gpt51 = openai('gpt-5.1'); const gpt5 = openai('gpt-5'); const o3 = openai('o3'); const o3mini = openai('o3-mini');
import { anthropic } from '@ai-sdk/anthropic'; const sonnet45 = anthropic('claude-sonnet-4-5-20250929'); // Latest const opus41 = anthropic('claude-opus-4-1-20250805'); const haiku45 = anthropic('claude-haiku-4-5-20251015');
import { google } from '@ai-sdk/google'; const pro = google('gemini-2.5-pro'); const flash = google('gemini-2.5-flash'); const lite = google('gemini-2.5-flash-lite');
import { experimental_generateSpeech as generateSpeech } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateSpeech({ model: openai.speech('tts-1-hd'), voice: 'alloy', text: 'Hello, how can I help you today?', }); // result.audio is an ArrayBuffer containing the audio const audioBuffer = result.audio;
import { experimental_transcribe as transcribe } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await transcribe({ model: openai.transcription('whisper-1'), audio: audioFile, // File, Blob, ArrayBuffer, or URL }); console.log(result.text); // Transcribed text console.log(result.segments); // Timestamped segments
import { generateImage } from 'ai'; import { openai } from '@ai-sdk/openai'; const result = await generateImage({ model: openai.image('dall-e-3'), prompt: 'A futuristic city at sunset', size: '1024x1024', n: 1, }); // result.images is an array of generated images const imageUrl = result.images[0].url; const imageBase64 = result.images[0].base64;
import { embed, embedMany, cosineSimilarity } from 'ai'; import { openai } from '@ai-sdk/openai'; // Single embedding const result = await embed({ model: openai.embedding('text-embedding-3-small'), value: 'Hello world', }); console.log(result.embedding); // number[] // Multiple embeddings (parallel processing) const results = await embedMany({ model: openai.embedding('text-embedding-3-small'), values: ['Hello', 'World', 'AI'], maxParallelCalls: 5, // Parallel processing }); // Compare similarity const similarity = cosineSimilarity( results.embeddings[0], results.embeddings[1] ); console.log(`Similarity: ${similarity}`); // 0.0 to 1.0
import { generateText } from 'ai'; import { google } from '@ai-sdk/google'; const result = await generateText({ model: google('gemini-2.5-pro'), messages: [{ role: 'user', content: [ { type: 'text', text: 'Summarize this document' }, { type: 'file', data: pdfBuffer, mimeType: 'application/pdf' }, ], }], }); // Or with images const result = await generateText({ model: openai('gpt-5'), messages: [{ role: 'user', content: [ { type: 'text', text: 'What is in this image?' }, { type: 'image', image: imageBuffer }, ], }], });
toTextStreamResponse()toUIMessageStreamResponse()toUIMessageStreamResponse():const result = streamText({ model: workersai('@cf/qwen/qwen3-30b-a3b-fp8'), messages, system: 'You are helpful.', }); // ✅ For chat UIs - returns SSE with JSON events return result.toUIMessageStreamResponse({ headers: { 'Access-Control-Allow-Origin': '*' }, }); // ❌ For simple text - returns plain text chunks only return result.toTextStreamResponse();
toDataStreamResponse() does NOT exist in AI SDK v5 (common misconception).workers-ai-provider@2.x requires AI SDK v5, NOT v4.# ✅ Correct - AI SDK v5 with workers-ai-provider v2 npm install ai@^5.0.0 workers-ai-provider@^2.0.0 zod@^3.25.0 # ❌ Wrong - AI SDK v4 causes error npm install ai@^4.0.0 workers-ai-provider@^2.0.0 # Error: "AI SDK 4 only supports models that implement specification version v1"
zod@^3.25.0 or later for zod/v3 and zod/v4 exports. Older versions (3.22.x) cause build errors: "Could not resolve zod/v4".// ❌ BAD: Top-level imports cause startup overhead import { createWorkersAI } from 'workers-ai-provider'; const workersai = createWorkersAI({ binding: env.AI }); // ✅ GOOD: Lazy initialization inside handler app.post('/chat', async (c) => { const { createWorkersAI } = await import('workers-ai-provider'); const workersai = createWorkersAI({ binding: c.env.AI }); // ... });
parameters → inputSchema (Zod schema)args → input, result → outputToolExecutionError removed (now tool-error content parts)maxSteps parameter removed → Use stopWhen(stepCountIs(n))maxTokens → maxOutputTokensproviderMetadata → providerOptionsparameters → inputSchemaargs → input, result → outputCoreMessage → ModelMessageMessage → UIMessageconvertToCoreMessages → convertToModelMessagesToolExecutionError class removedtool-error content partsmaxSteps → stopWhenstepCountIs() or hasToolCall()content string → parts arraytoolCallStreaming option removedai/rsc → @ai-sdk/rscai/react → @ai-sdk/reactLangChainAdapter → @ai-sdk/langchainimport { generateText } from 'ai'; const result = await generateText({ model: openai.chat('gpt-4-turbo'), maxTokens: 500, providerMetadata: { openai: { user: 'user-123' } }, tools: { weather: { description: 'Get weather', parameters: z.object({ location: z.string() }), execute: async (args) => { /* args.location */ }, }, }, maxSteps: 5, }); `**After (v5):**` import { generateText, tool, stopWhen, stepCountIs } from 'ai'; const result = await generateText({ model: openai('gpt-4-turbo'), maxOutputTokens: 500, providerOptions: { openai: { user: 'user-123' } }, tools: { weather: tool({ description: 'Get weather', inputSchema: z.object({ location: z.string() }), execute: async ({ location }) => { /* input.location */ }, }), }, stopWhen: stepCountIs(5), });
maxTokens to maxOutputTokensproviderMetadata to providerOptionsparameters to inputSchemaargs → inputmaxSteps with stopWhen(stepCountIs(n))CoreMessage → ModelMessageToolExecutionError handlingai/rsc → @ai-sdk/rsc)npx ai migrateimport { AI_APICallError } from 'ai'; try { const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Hello', }); } catch (error) { if (error instanceof AI_APICallError) { console.error('API call failed:', error.message); console.error('Status code:', error.statusCode); console.error('Response:', error.responseBody); // Check common causes if (error.statusCode === 401) { // Invalid API key } else if (error.statusCode === 429) { // Rate limit - implement backoff } else if (error.statusCode >= 500) { // Provider issue - retry } } }
import { AI_NoObjectGeneratedError } from 'ai'; try { const result = await generateObject({ model: openai('gpt-4-turbo'), schema: z.object({ /* complex schema */ }), prompt: 'Generate data', }); } catch (error) { if (error instanceof AI_NoObjectGeneratedError) { console.error('No valid object generated'); // Solutions: // 1. Simplify schema // 2. Add more context to prompt // 3. Provide examples in prompt // 4. Try different model (gpt-5 or claude-sonnet-4-5 for complex objects) } }
// BAD: Top-level imports cause startup overhead import { createWorkersAI } from 'workers-ai-provider'; import { complexSchema } from './schemas'; const workersai = createWorkersAI({ binding: env.AI }); // GOOD: Lazy initialization inside handler export default { async fetch(request, env) { const { createWorkersAI } = await import('workers-ai-provider'); const workersai = createWorkersAI({ binding: env.AI }); // Use workersai here } }
createDataStreamResponse.// Use the onError callback (added in v4.1.22) const stream = streamText({ model: openai('gpt-4-turbo'), prompt: 'Hello', onError({ error }) { console.error('Stream error:', error); // Custom error logging and handling }, }); // Stream safely for await (const chunk of stream.textStream) { process.stdout.write(chunk); } `**Alternative (Manual try-catch):**` // Fallback if not using onError callback try { const stream = streamText({ model: openai('gpt-4-turbo'), prompt: 'Hello', }); for await (const chunk of stream.textStream) { process.stdout.write(chunk); } } catch (error) { console.error('Stream error:', error); }
onError callback for proper error capture (recommended)import { AI_LoadAPIKeyError } from 'ai'; try { const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Hello', }); } catch (error) { if (error instanceof AI_LoadAPIKeyError) { console.error('API key error:', error.message); // Check: // 1. .env file exists and loaded // 2. Correct env variable name (OPENAI_API_KEY) // 3. Key format is valid (starts with sk-) } }
import { AI_InvalidArgumentError } from 'ai'; try { const result = await generateText({ model: openai('gpt-4-turbo'), maxOutputTokens: -1, // Invalid! prompt: 'Hello', }); } catch (error) { if (error instanceof AI_InvalidArgumentError) { console.error('Invalid argument:', error.message); // Check parameter types and values } }
import { AI_NoContentGeneratedError } from 'ai'; try { const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Some prompt', }); } catch (error) { if (error instanceof AI_NoContentGeneratedError) { console.error('No content generated'); // Possible causes: // 1. Safety filters blocked output // 2. Prompt triggered content policy // 3. Model configuration issue // Handle gracefully: return { text: 'Unable to generate response. Please try different input.' }; } }
import { AI_TypeValidationError } from 'ai'; try { const result = await generateObject({ model: openai('gpt-4-turbo'), schema: z.object({ age: z.number().min(0).max(120), // Strict validation }), prompt: 'Generate person', }); } catch (error) { if (error instanceof AI_TypeValidationError) { console.error('Validation failed:', error.message); // Solutions: // 1. Relax schema constraints // 2. Add more guidance in prompt // 3. Use .optional() for unreliable fields } }
.optional() for fields that may not always be presentimport { AI_RetryError } from 'ai'; try { const result = await generateText({ model: openai('gpt-4-turbo'), prompt: 'Hello', maxRetries: 3, // Default is 2 }); } catch (error) { if (error instanceof AI_RetryError) { console.error('All retries failed'); console.error('Last error:', error.lastError); // Check root cause: // - Persistent network issue // - Provider outage // - Invalid configuration } }
// Implement exponential backoff async function generateWithBackoff(prompt: string, retries = 3) { for (let i = 0; i < retries; i++) { try { return await generateText({ model: openai('gpt-4-turbo'), prompt, }); } catch (error) { if (error instanceof AI_APICallError && error.statusCode === 429) { const delay = Math.pow(2, i) * 1000; // Exponential backoff console.log(`Rate limited, waiting ${delay}ms`); await new Promise(resolve => setTimeout(resolve, delay)); } else { throw error; } } } throw new Error('Rate limit retries exhausted'); }
// Instead of deeply nested schemas at top level: // const complexSchema = z.object({ /* 100+ fields */ }); // Define inside functions or use type assertions: function generateData() { const schema = z.object({ /* complex schema */ }); return generateObject({ model: openai('gpt-4-turbo'), schema, prompt: '...' }); } // Or use z.lazy() for recursive schemas: type Category = { name: string; subcategories?: Category[] }; const CategorySchema: z.ZodType<Category> = z.lazy(() => z.object({ name: z.string(), subcategories: z.array(CategorySchema).optional(), }) );
z.lazy() for recursive types// Use built-in retry and mode selection const result = await generateObject({ model: openai('gpt-4-turbo'), schema: mySchema, prompt: 'Generate data', mode: 'json', // Force JSON mode (supported by GPT-4) maxRetries: 3, // Retry on invalid JSON }); // Or catch and retry manually: try { const result = await generateObject({ model: openai('gpt-4-turbo'), schema: mySchema, prompt: 'Generate data', }); } catch (error) { // Retry with different model const result = await generateObject({ model: openai('gpt-4-turbo'), schema: mySchema, prompt: 'Generate data', }); }
mode: 'json' when available// Conditionally add tools only when needed const needsTools = await analyzePrompt(userInput); const result = await generateText({ model: google('gemini-3-flash'), tools: needsTools ? { weather: weatherTool } : undefined, prompt: userInput, });
SyntaxError: "[object Object]" is not valid JSON Cause: Anthropic provider built-in tools (web_fetch, etc.) return error objects that SDK tries to JSON.parse Source: GitHub Issue #11856try { const result = await generateText({ model: anthropic('claude-sonnet-4-5-20250929'), tools: { web_fetch: { type: 'anthropic_defined', name: 'web_fetch' } }, prompt: userPrompt, }); } catch (error) { if (error.message.includes('is not valid JSON')) { // Tool returned error result, handle gracefully console.error('Tool execution failed - likely blocked URL or permission issue'); // Retry without tool or use custom tool } throw error; }
tool-result in assistant message not allowed Cause: Server-executed tools incorrectly place tool-result parts in assistant messages Source: GitHub Issue #11855execute runs on server, not sent to model), the AI SDK incorrectly includes tool-result parts in the assistant message. Anthropic expects tool-result only in user messages.// Workaround: Filter messages before sending const filteredMessages = messages.map(msg => { if (msg.role === 'assistant') { return { ...msg, content: msg.content.filter(part => part.type !== 'tool-result'), }; } return msg; }); const result = await generateText({ model: anthropic('claude-sonnet-4-5-20250929'), tools: { database: databaseTool }, messages: filteredMessages, prompt: 'Get user data', });
useChat with memoized options (common for performance), the onData and onFinish callbacks have stale closures and don't see updated state variables.const [count, setCount] = useState(0); const chatOptions = useMemo(() => ({ onFinish: (message) => { console.log('Count:', count); // ALWAYS 0, never updates! }, }), []); // Empty deps = stale closure const { messages, append } = useChat(chatOptions); `**Workaround 1 - Don't Memoize Callbacks**:` const { messages, append } = useChat({ onFinish: (message) => { console.log('Count:', count); // Now sees current count }, }); `**Workaround 2 - Use useRef**:` const countRef = useRef(count); useEffect(() => { countRef.current = count; }, [count]); const chatOptions = useMemo(() => ({ onFinish: (message) => { console.log('Count:', countRef.current); // Always current }, }), []);
const { messages, append, reload } = useChat({ api: '/api/chat', onError: (error) => { if (error.message.includes('stream') || error.message.includes('aborted')) { // Attempt to reload last message reload(); } }, }); `**Workaround 2 - Detect Visibility Change**:` useEffect(() => { const handleVisibilityChange = () => { if (document.visibilityState === 'visible') { // Check if stream was interrupted const lastMessage = messages[messages.length - 1]; if (lastMessage?.role === 'assistant' && !lastMessage.content) { reload(); } } }; document.addEventListener('visibilitychange', handleVisibilityChange); return () => document.removeEventListener('visibilitychange', handleVisibilityChange); }, [messages, reload]);
npm view ai version npm view ai dist-tags