The AI SDK 5 Migration Model Context Protocol (MCP) Server provides an automated way to migrate your project using a coding agent. This server has been designed for Cursor, but should work with any coding agent that supports MCP.
To get started, create or edit .cursor/mcp.json in your project:
After saving, open the command palette (Cmd+Shift+P on macOS, Ctrl+Shift+P on Windows/Linux) and search for "View: Open MCP Settings". Verify the new server appears and is toggled on.
Then use this prompt:
Please migrate this project to AI SDK 5 using the ai-sdk-5-migration mcp server. Start by creating a checklist.
Additionally, you need to update the following peer dependencies:
zod package: 4.1.8 or later (recommended to avoid TypeScript performance issues)
An example upgrade command would be:
npm install ai @ai-sdk/react @ai-sdk/openai zod@^4.1.8
If you encounter TypeScript performance issues after upgrading, ensure you're
using Zod 4.1.8 or later. If the issue persists, update your tsconfig.json
to use moduleResolution: "nodenext". See the TypeScript performance
troubleshooting guide for
more details.
The AI SDK provides Codemod transformations to help upgrade your codebase when a
feature is deprecated, removed, or otherwise changed.
Codemods are transformations that run on your codebase automatically. They
allow you to easily apply many changes without having to manually go through
every file.
Codemods are intended as a tool to help you with the upgrade process. They may
not cover all of the changes you need to make. You may need to make additional
changes manually.
You can run all codemods provided as part of the 5.0 upgrade process by running
the following command from the root of your project:
npx @ai-sdk/codemod upgrade
To run only the v5 codemods (v4 → v5 migration):
npx @ai-sdk/codemod v5
Individual codemods can be run by specifying the name of the codemod:
The writeMessageAnnotation and writeData methods from DataStreamWriter have been removed. Instead, use custom data parts with the new UIMessage stream architecture.
Tool call and result properties have been renamed for better consistency with schemas.
AI SDK 4.0
// Tool calls used "args" and "result"for await (const part of result.fullStream) { switch (part.type) { case 'tool-call': console.log('Tool args:', part.args); break; case 'tool-result': console.log('Tool result:', part.result); break; }}
AI SDK 5.0
// Tool calls now use "input" and "output"for await (const part of result.fullStream) { switch (part.type) { case 'tool-call': console.log('Tool input:', part.input); break; case 'tool-result': console.log('Tool output:', part.output); break; }}
The ToolExecutionError class has been removed. Tool execution errors now appear as tool-error content parts in the result steps, enabling automated LLM roundtrips in multi-step scenarios.
// Type-safe tool parts with specific names{ message.parts.map(part => { switch (part.type) { case 'tool-getWeatherInformation': return <div>Getting weather...</div>; case 'tool-askForConfirmation': return <div>Asking for confirmation...</div>; } });}
AI SDK 5.0 introduces dynamic tools for handling tools with unknown types at development time, such as MCP tools without schemas or user-defined functions at runtime.
The new dynamicTool helper function allows you to define tools where the input and output types are not known at compile time.
AI SDK 5.0
import { dynamicTool } from 'ai';import { z } from 'zod';// Define a dynamic toolconst runtimeTool = dynamicTool({ description: 'A tool defined at runtime', inputSchema: z.object({}), execute: async input => { // Input and output are typed as 'unknown' return { result: `Processed: ${input.query}` }; },});
MCP tools that don't provide schemas are now automatically treated as dynamic tools:
AI SDK 5.0
import { MCPClient } from 'ai';const client = new MCPClient({ /* ... */});const tools = await client.getTools();// Tools without schemas are now 'dynamic' type// and won't break type inference when mixed with static tools
When iterating over toolCalls and toolResults, you now need to check the dynamic flag first for proper type narrowing:
AI SDK 4.0
// Direct type checking worked without dynamic flagonStepFinish: step => { for (const toolCall of step.toolCalls) { switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; case 'search': console.log(toolCall.input.query); // typed as string break; } }};
AI SDK 5.0
// Must check dynamic flag first for type narrowingonStepFinish: step => { for (const toolCall of step.toolCalls) { // Check if it's a dynamic tool first if (toolCall.dynamic) { console.log('Dynamic tool:', toolCall.toolName); console.log('Input:', toolCall.input); // typed as unknown continue; } // Now TypeScript knows it's a static tool switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; case 'search': console.log(toolCall.input.query); // typed as string break; } }};
In v4, you typically rendered tool invocations using a catch-all tool-invocation type. In v5, the recommended approach is to handle each tool specifically using its typed part name (e.g., tool-getWeather). However, if you need a catch-all pattern for rendering all tool invocations the same way, you can use the isToolUIPart and getToolName helper functions as a fallback.
The experimental_continueSteps option has been removed from generateText().
AI SDK 4.0
const result = await generateText({ experimental_continueSteps: true, // ...});
AI SDK 5.0
const result = await generateText({ // experimental_continueSteps has been removed // Use newer models with higher output token limits instead // ...});
For core functions like generateText and streamText, the maxSteps parameter has been replaced with stopWhen, which provides more flexible control over multi-step execution. The stopWhen parameter defines conditions for stopping the generation when the last step contains tool results. When multiple conditions are provided as an array, the generation stops if any condition is met.
AI SDK 4.0
// V4: Simple numeric limit
const result = await generateText({
model: "anthropic/claude-sonnet-5.5",
messages,
maxSteps: 5, // Stop after a maximum of 5 steps
});
// useChat with maxSteps
const { messages } = useChat({
maxSteps: 3, // Stop after a maximum of 3 steps
});
AI SDK 5.0
import { stepCountIs, hasToolCall } from 'ai';
// V5: Server-side - flexible stopping conditions with stopWhen
const result = await generateText({
model: "anthropic/claude-sonnet-5.5",
messages,
// Only triggers when last step has tool results
stopWhen: stepCountIs(5), // Stop at step 5 if tools were called
});
// Server-side - stop when specific tool is called
const result = await generateText({
model: "anthropic/claude-sonnet-5.5",
messages,
stopWhen: hasToolCall('finalizeTask'), // Stop when finalizeTask tool is called
});
Common stopping patterns:
AI SDK 5.0
// Stop after N steps (equivalent to old maxSteps)// Note: Only applies when the last step has tool resultsstopWhen: stepCountIs(5);// Stop when specific tool is calledstopWhen: hasToolCall('finalizeTask');// Multiple conditions (stops if ANY condition is met)stopWhen: [ stepCountIs(10), // Maximum 10 steps hasToolCall('submitOrder'), // Or when order is submitted];// Custom condition based on step contentstopWhen: ({ steps }) => { const lastStep = steps[steps.length - 1]; // Custom logic - only triggers if last step has tool results return lastStep?.text?.includes('COMPLETE');};
Important: The stopWhen conditions are only evaluated when the last step contains tool results.
Usage properties now distinguish between single step and total usage.
AI SDK 4.0
// usage contained total token usage across all stepsconsole.log(result.usage);
AI SDK 5.0
// usage contains token usage from the final step onlyconsole.log(result.usage);// totalUsage contains total token usage across all stepsconsole.log(result.totalUsage);
The maxSteps parameter has been removed from useChat. You should now use server-side stopWhen conditions for multi-step tool execution control, and manually submit tool results and trigger new messages for client-side tool calls.
// Server-side: Use stopWhen for multi-step control
import { streamText, convertToModelMessages, stepCountIs } from 'ai';
const result = await streamText({
model: "anthropic/claude-sonnet-5.5",
messages: convertToModelMessages(messages),
stopWhen: stepCountIs(5), // Stop after 5 steps with tool calls
});
// Client-side: Configure automatic submission
import { useChat } from '@ai-sdk/react';
import {
DefaultChatTransport,
lastAssistantMessageIsCompleteWithToolCalls,
} from 'ai';
const { messages, sendMessage, addToolOutput } = useChat({
// Automatically submit when all tool results are available
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
async onToolCall({ toolCall }) {
const result = await executeToolCall(toolCall);
// Important: Don't await addToolOutput inside onToolCall to avoid deadlocks
addToolOutput({
tool: toolCall.toolName,
toolCallId: toolCall.toolCallId,
output: result,
});
},
});
Important: When using sendAutomaticallyWhen, don't use await with
addToolOutput inside onToolCall as it can cause deadlocks. The await is
useful when you're not using automatic submission and need to ensure the
messages are updated before manually calling sendMessage().
This change provides more flexibility for handling tool calls and aligns client behavior with server-side multi-step execution patterns.
In v4, you could share chat state between components by using the same id parameter in multiple useChat hooks.
AI SDK 4.0
// Component Aconst { messages } = useChat({ id: 'shared-chat', api: '/api/chat',});// Component B - would share the same chat stateconst { messages } = useChat({ id: 'shared-chat', api: '/api/chat',});
In v5, you need to explicitly share chat instances by passing a shared Chat instance.
AI SDK 5.0
// e.g. Store Chat instance in React Context and create a custom hook// Component Aconst { chat } = useSharedChat(); // Custom hook that accesses shared Chat from contextconst { messages, sendMessage } = useChat({ chat, // Pass the shared chat instance});// Component B - shares the same chat instanceconst { chat } = useSharedChat(); // Same hook to access shared Chat from contextconst { messages } = useChat({ chat, // Same shared chat instance});
The addToolResult method has been renamed to addToolOutput. Additionally, the result parameter has been renamed to output for consistency with other tool-related APIs.
AI SDK 4.0
const { addToolResult } = useChat();// Add tool result with 'result' parameteraddToolResult({ toolCallId: 'tool-call-123', result: 'Weather: 72°F, sunny',});
AI SDK 5.0
const { addToolOutput } = useChat();// Add tool output with 'output' parameter and 'tool' name for type safetyaddToolOutput({ tool: 'getWeather', toolCallId: 'tool-call-123', output: 'Weather: 72°F, sunny',});
addToolResult is still available but deprecated. It will be removed in
version 6.
The automatic tool result submission behavior has been updated in useChat and the Chat component. You now have more control and flexibility over when tool results are submitted.
onToolCall no longer supports returning values to automatically submit tool results
You must explicitly call addToolOutput to provide tool results
Use sendAutomaticallyWhen with lastAssistantMessageIsCompleteWithToolCalls helper for automatic submission
Important: Don't use await with addToolOutput inside onToolCall to avoid deadlocks
The maxSteps parameter has been removed from the Chat component and useChat hook
For multi-step tool execution, use server-side stopWhen conditions instead (see maxSteps Removal)
AI SDK 4.0
const { messages, sendMessage, addToolResult } = useChat({ maxSteps: 5, // Removed in v5 // Automatic submission by returning a value async onToolCall({ toolCall }) { if (toolCall.toolName === 'getLocation') { const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco']; return cities[Math.floor(Math.random() * cities.length)]; } },});
In v4, the body option in useChat configuration would dynamically update with component state changes. In v5, the body value is only captured at the first render and remains static throughout the component lifecycle.
AI SDK 4.0
const [temperature, setTemperature] = useState(0.7);const { messages } = useChat({ api: '/api/chat', body: { temperature, // This would update dynamically in v4 },});
AI SDK 5.0
const [temperature, setTemperature] = useState(0.7);// Option 1: Use request-level configuration (Recommended)const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat' }),});// Pass dynamic values at request timesendMessage( { text: input }, { body: { temperature, // Current temperature value at request time }, },);// Option 2: Use function configuration with useRefconst temperatureRef = useRef(temperature);temperatureRef.current = temperature;const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', body: () => ({ temperature: temperatureRef.current, }), }),});
For more details on request configuration, see the Chatbot guide.
In v4, usage information was directly accessible through the onFinish callback's options parameter. In v5, usage data is attached as metadata to individual messages using the messageMetadata function in toUIMessageStreamResponse.
import {
convertToModelMessages,
streamText,
UIMessage,
type LanguageModelUsage,
} from 'ai';
// Create a new metadata type (optional for type-safety)
type MyMetadata = {
totalUsage: LanguageModelUsage;
};
// Create a new custom message type with your own metadata
export type MyUIMessage = UIMessage<MyMetadata>;
export async function POST(req: Request) {
const { messages }: { messages: MyUIMessage[] } = await req.json();
const result = streamText({
model: "anthropic/claude-sonnet-5.5",
messages: convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse({
originalMessages: messages,
messageMetadata: ({ part }) => {
// Send total usage when generation is finished
if (part.type === 'finish') {
return { totalUsage: part.totalUsage };
}
},
});
}
Then, on the client, you can access the message-level metadata.
AI SDK 5.0 - Client
'use client';import { useChat } from '@ai-sdk/react';import type { MyUIMessage } from './api/chat/route';import { DefaultChatTransport } from 'ai';export default function Chat() { // Use custom message type defined on the server (optional for type-safety) const { messages } = useChat<MyUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); return ( <div className="flex flex-col w-full max-w-md py-24 mx-auto stretch"> {messages.map(m => ( <div key={m.id} className="whitespace-pre-wrap"> {m.role === 'user' ? 'User: ' : 'AI: '} {m.parts.map(part => { if (part.type === 'text') { return part.text; } })} {/* Render usage via metadata */} {m.metadata?.totalUsage && ( <div>Total usage: {m.metadata?.totalUsage.totalTokens} tokens</div> )} </div> ))} </div> );}
You can also access your metadata from the onFinish callback of useChat:
AI SDK 5.0 - onFinish
'use client';import { useChat } from '@ai-sdk/react';import type { MyUIMessage } from './api/chat/route';import { DefaultChatTransport } from 'ai';export default function Chat() { // Use custom message type defined on the server (optional for type-safety) const { messages } = useChat<MyUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat', }), onFinish: ({ message }) => { // Access message metadata via onFinish callback console.log(message.metadata?.totalUsage); }, });}
The experimental_prepareRequestBody option has been replaced with prepareSendMessagesRequest in the transport configuration.
AI SDK 4.0
import { useChat } from '@ai-sdk/react';const { messages } = useChat({ api: '/api/chat', // Only send the last message to the server: experimental_prepareRequestBody({ messages, id }) { return { message: messages[messages.length - 1], id }; },});
AI SDK 5.0
import { useChat } from '@ai-sdk/react';import { DefaultChatTransport } from 'ai';const { messages } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', // Only send the last message to the server: prepareSendMessagesRequest({ messages, id }) { return { body: { message: messages[messages.length - 1], id } }; }, }),});
The @ai-sdk/ui-utils package has been removed and its exports moved to the main ai package.
AI SDK 4.0
import { getTextFromDataUrl } from '@ai-sdk/ui-utils';
AI SDK 5.0
import { getTextFromDataUrl } from 'ai';
Note: processDataStream was removed entirely in v5.0. Use readUIMessageStream instead for processing UI message streams, or use the more configurable Chat/useChat APIs for most use cases.
import { useChat } from '@ai-sdk/react';import { DefaultChatTransport } from 'ai';function Chat() { const { messages, sendMessage } = useChat({ transport: new DefaultChatTransport({ api: '/api/chat', }), }); // ...}
The useAssistant hook was specific to the OpenAI Assistants API. OpenAI has
deprecated that API in favor of the Responses API. Configure useChat for your
route as shown above, then return a UI message stream from the route:
app/api/chat/route.ts
import { openai } from '@ai-sdk/openai';import { convertToModelMessages, streamText, type UIMessage } from 'ai';export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: openai.responses('gpt-4o-mini'), prompt: convertToModelMessages(messages), }); return result.toUIMessageStreamResponse();}
embedMany now makes parallel requests with a configurable maxParallelCalls option.
AI SDK 5.0
const { embeddings, usage } = await embedMany({ maxParallelCalls: 2, // Limit parallel requests model: 'openai/text-embedding-3-small', values: [ 'sunny day at the beach', 'rainy afternoon in the city', 'snowy night in the mountains', ],});
The streaming architecture has been completely redesigned in v5 to support better content differentiation, concurrent streaming of multiple parts, and improved real-time UX.
The fundamental streaming pattern has changed from single chunks to a three-phase pattern with unique IDs for each content block.
AI SDK 4.0
for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-delta': { process.stdout.write(chunk.textDelta); break; } }}
AI SDK 5.0
for await (const chunk of result.fullStream) { switch (chunk.type) { case 'text-start': { // New: Initialize a text block with unique ID console.log(`Starting text block: ${chunk.id}`); break; } case 'text-delta': { // Changed: Now includes ID and uses 'delta' property process.stdout.write(chunk.delta); // Changed from 'textDelta' break; } case 'text-end': { // New: Finalize the text block console.log(`Completed text block: ${chunk.id}`); break; } }}
Reasoning content now follows the same start/delta/end pattern:
AI SDK 4.0
for await (const chunk of result.fullStream) { switch (chunk.type) { case 'reasoning': { // Single chunk with full reasoning text console.log('Reasoning:', chunk.text); break; } }}
AI SDK 5.0
for await (const chunk of result.fullStream) { switch (chunk.type) { case 'reasoning-start': { console.log(`Starting reasoning block: ${chunk.id}`); break; } case 'reasoning-delta': { process.stdout.write(chunk.delta); break; } case 'reasoning-end': { console.log(`Completed reasoning block: ${chunk.id}`); break; } }}
Stream finish events have been renamed for consistency.
AI SDK 4.0
for await (const part of result.fullStream) { switch (part.type) { case 'step-finish': { console.log('Step finished:', part.finishReason); break; } case 'finish': { console.log('Usage:', part.usage); break; } }}
AI SDK 5.0
for await (const part of result.fullStream) { switch (part.type) { case 'finish-step': { // Renamed from 'step-finish' console.log('Step finished:', part.finishReason); break; } case 'finish': { console.log('Total Usage:', part.totalUsage); // Changed from 'usage' break; } }}
The getErrorMessage option in toDataStreamResponse has been replaced with onError in toUIMessageStreamResponse, providing more control over error forwarding to the client.
By default, error messages are NOT sent to the client to prevent leaking sensitive information. The onError callback allows you to explicitly control what error information is forwarded to the client.
AI SDK 4.0
return result.toDataStreamResponse({ getErrorMessage: error => { // Return sanitized error data to send to client // Only return what you want the client to see! return { errorCode: 'STREAM_ERROR', message: 'An error occurred while processing your request', // In production, avoid sending error.message directly to prevent information leakage }; },});
AI SDK 5.0
return result.toUIMessageStreamResponse({ onError: error => { // Return sanitized error data to send to client // Only return what you want the client to see! return { errorCode: 'STREAM_ERROR', message: 'An error occurred while processing your request', // In production, avoid sending error.message directly to prevent information leakage }; },});
If you have persisted messages in a database, see the Data Migration
Guide for comprehensive
guidance on migrating your stored message data to the v5 format.
In v4, you would typically use helper functions like appendResponseMessages or appendClientMessage to format messages in the onFinish callback of streamText:
AI SDK 4.0
import {
streamText,
convertToModelMessages,
appendClientMessage,
appendResponseMessages,
} from 'ai';
const updatedMessages = appendClientMessage({
messages,
message: lastUserMessage,
});
const result = streamText({
model: "anthropic/claude-sonnet-5.5",
messages: updatedMessages,
experimental_generateMessageId: () => generateId(), // ID generation on streamText
onFinish: async ({ responseMessages, usage }) => {
// Use helper functions to format messages
const finalMessages = appendResponseMessages({
messages: updatedMessages,
responseMessages,
});
// Save formatted messages to database
await saveMessages(finalMessages);
},
});
In v5, message persistence is now handled through the toUIMessageStreamResponse method, which automatically formats response messages in the UIMessage format:
AI SDK 5.0
import { streamText, convertToModelMessages, UIMessage } from 'ai';
const messages: UIMessage[] = [
// Your existing messages in UIMessage format
];
const result = streamText({
model: "anthropic/claude-sonnet-5.5",
messages: convertToModelMessages(messages),
// experimental_generateMessageId removed from here
});
return result.toUIMessageStreamResponse({
originalMessages: messages, // IMPORTANT: Required to prevent duplicate messages
generateMessageId: () => generateId(), // IMPORTANT: Required for proper message ID generation
onFinish: ({ messages, responseMessage }) => {
// messages contains all messages (original + response) in UIMessage format
saveChat({ chatId, messages });
// responseMessage contains just the generated message in UIMessage format
saveMessage({ chatId, message: responseMessage });
},
});
Important: When using toUIMessageStreamResponse, you should always
provide both originalMessages and generateMessageId parameters. Without
these, you may experience duplicate or repeated assistant messages in your UI.
For more details, see Troubleshooting: Repeated Assistant
Messages.
The experimental_generateMessageId option has been moved from streamText configuration to toUIMessageStreamResponse, as it's designed for use with UIMessages rather than ModelMessages.
In AI SDK 5, the default OpenAI provider instance uses the Responses API, while AI SDK 4 used the Chat Completions API. The Chat Completions API remains fully supported and you can use it with openai.chat(...).
AI SDK 4.0
import { openai } from '@ai-sdk/openai';const defaultModel = openai('gpt-4.1-mini'); // Chat Completions API
AI SDK 5.0
import { openai } from '@ai-sdk/openai';const defaultModel = openai('gpt-4.1-mini'); // Responses API// Specify a specific API when needed:const chatCompletionsModel = openai.chat('gpt-4.1-mini');const responsesModel = openai.responses('gpt-4.1-mini');
The Responses and Chat Completions APIs have different behavior and defaults.
If you depend on the Chat Completions API, switch your model instance to
openai.chat(...) and audit your configuration.
In AI SDK 4.0, you could set the strictSchemas option on Responses models (which defaulted to true). This option has been renamed to strictJsonSchema in AI SDK 5.0 and now defaults to false.
AI SDK 4.0
import { z } from 'zod';import { generateObject } from 'ai';import { openai, type OpenAIResponsesProviderOptions } from '@ai-sdk/openai';const result = await generateObject({ model: openai.responses('gpt-4.1'), schema: z.object({ // ... }), providerOptions: { openai: { strictSchemas: true, // default behaviour in AI SDK 4 } satisfies OpenAIResponsesProviderOptions, },});
AI SDK 5.0
import { z } from 'zod';import { generateObject } from 'ai';import { openai, type OpenAIResponsesProviderOptions } from '@ai-sdk/openai';const result = await generateObject({ model: openai('gpt-4.1-2024'), // uses Responses API schema: z.object({ // ... }), providerOptions: { openai: { strictJsonSchema: true, // defaults to false, opt back in to the AI SDK 4 strict behaviour } satisfies OpenAIResponsesProviderOptions, },});
If you call openai.chat(...) to use the Chat Completions API directly, you can type it with OpenAIChatLanguageModelOptions. AI SDK 5 adds the same strictJsonSchema option there as well.
Search Grounding is now called "Google Search" and is now a provider defined tool.
AI SDK 4.0
const { text, providerMetadata } = await generateText({ model: google('gemini-1.5-pro', { useSearchGrounding: true, }), prompt: 'List the top 5 San Francisco news from the past week.',});
AI SDK 5.0
import { google } from '@ai-sdk/google';const { text, sources, providerMetadata } = await generateText({ model: google('gemini-1.5-pro'), prompt: 'List the top 5 San Francisco news from the past week.' tools: { google_search: google.tools.googleSearch({}), },});
If you experience TypeScript server crashes, slow type checking, or errors like "Type instantiation is excessively deep and possibly infinite" when using Zod with AI SDK 5.0:
First, ensure you're using Zod 4.1.8 or later - this version includes a fix for module resolution issues that cause TypeScript performance problems.
If the issue persists, update your tsconfig.json to use moduleResolution: "nodenext":
{ "compilerOptions": { "moduleResolution": "nodenext" // ... other options }}
This resolves the TypeScript performance issues while allowing you to continue using the standard Zod import. If this doesn't resolve the issue, you can try using a version-specific import path as an alternative solution. For detailed troubleshooting steps, see TypeScript performance issues with Zod.
This section documents breaking changes between different beta versions of AI SDK 5.0. If you're upgrading from an earlier v5 beta version to a later one, check this section for any changes that might affect your code.