ai
Version:
AI SDK by Vercel - build apps like ChatGPT, Claude, Gemini, and more with a single interface for any model using the Vercel AI Gateway or go direct to OpenAI, Anthropic, Google, or any other model provider.
1,939 lines (1,563 loc) • 92.4 kB
text/mdx
---
title: Migrate AI SDK 4.x to 5.0
description: Learn how to upgrade AI SDK 4.x to 5.0.
---
# Migrate AI SDK 4.x to 5.0
## Recommended Migration Process
1. Backup your project. If you use a versioning control system, make sure all previous versions are committed.
1. Upgrade to AI SDK 5.0.
1. Automatically migrate your code using one of these approaches:
- Use the [AI SDK 5 Migration MCP Server](#ai-sdk-5-migration-mcp-server) for AI-assisted migration in Cursor or other MCP-compatible coding agents
- Use [codemods](#codemods) to automatically transform your code
1. Follow the breaking changes guide below.
1. Verify your project is working as expected.
1. Commit your changes.
## AI SDK 5 Migration MCP Server
The [AI SDK 5 Migration Model Context Protocol (MCP) Server](https://github.com/vercel-labs/ai-sdk-5-migration-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:
```json
{
"mcpServers": {
"ai-sdk-5-migration": {
"url": "https://ai-sdk-5-migration-mcp-server.vercel.app/api/mcp"
}
}
}
```
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.
```
For more information, see the [AI SDK 5 Migration MCP Server repository](https://github.com/vercel-labs/ai-sdk-5-migration-mcp-server).
## AI SDK 5.0 Package Versions
You need to update the following packages to the following versions in your `package.json` file(s):
- `ai` package: `5.0.0`
- `@ai-sdk/provider` package: `2.0.0`
- `@ai-sdk/provider-utils` package: `3.0.0`
- `@ai-sdk/*` packages: `2.0.0` (other `@ai-sdk` packages)
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
```
<Note>
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](/docs/troubleshooting/typescript-performance-zod) for
more details.
</Note>
## Codemods
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.
<Note>
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.
</Note>
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:
```sh
npx @ai-sdk/codemod upgrade
```
To run only the v5 codemods (v4 → v5 migration):
```sh
npx @ai-sdk/codemod v5
```
Individual codemods can be run by specifying the name of the codemod:
```sh
npx @ai-sdk/codemod <codemod-name> <path>
```
For example, to run a specific v5 codemod:
```sh
npx @ai-sdk/codemod v5/rename-format-stream-part src/
```
See also the [table of codemods](#codemod-table). In addition, the latest set of
codemods can be found in the
[`@ai-sdk/codemod`](https://github.com/vercel/ai/tree/main/packages/codemod/src/codemods)
repository.
## AI SDK Core Changes
### generateText and streamText Changes
#### Maximum Output Tokens
The `maxTokens` parameter has been renamed to `maxOutputTokens` for clarity.
```tsx filename="AI SDK 4.0"
const result = await generateText({
model: __MODEL__,
maxTokens: 1024,
prompt: 'Hello, world!',
});
```
```tsx filename="AI SDK 5.0"
const result = await generateText({
model: __MODEL__,
maxOutputTokens: 1024,
prompt: 'Hello, world!',
});
```
### Message and Type System Changes
#### Core Type Renames
##### `CoreMessage` → `ModelMessage`
```tsx filename="AI SDK 4.0"
import { CoreMessage } from 'ai';
```
```tsx filename="AI SDK 5.0"
import { ModelMessage } from 'ai';
```
##### `Message` → `UIMessage`
```tsx filename="AI SDK 4.0"
import { Message, CreateMessage } from 'ai';
```
```tsx filename="AI SDK 5.0"
import { UIMessage, CreateUIMessage } from 'ai';
```
##### `convertToCoreMessages` → `convertToModelMessages`
```tsx filename="AI SDK 4.0"
import { convertToCoreMessages, streamText } from 'ai';
const result = await streamText({
model: __MODEL__,
messages: convertToCoreMessages(messages),
});
```
```tsx filename="AI SDK 5.0"
import { convertToModelMessages, streamText } from 'ai';
const result = await streamText({
model: __MODEL__,
messages: convertToModelMessages(messages),
});
```
<Note>
For more information about model messages, see the [Model Message
reference](/docs/reference/ai-sdk-core/model-message).
</Note>
### UIMessage Changes
#### Content → Parts Array
For `UIMessage`s (previously called `Message`), the `.content` property has been replaced with a `parts` array structure.
```tsx filename="AI SDK 4.0"
import { type Message } from 'ai'; // v4 Message type
// Messages (useChat) - had content property
const message: Message = {
id: '1',
role: 'user',
content: 'Bonjour!',
};
```
```tsx filename="AI SDK 5.0"
import { type UIMessage, type ModelMessage } from 'ai';
// UIMessages (useChat) - now use parts array
const uiMessage: UIMessage = {
id: '1',
role: 'user',
parts: [{ type: 'text', text: 'Bonjour!' }],
};
```
#### Data Role Removed
The `data` role has been removed from UI messages.
```tsx filename="AI SDK 4.0"
const message = {
role: 'data',
content: 'Some content',
data: { customField: 'value' },
};
```
```tsx filename="AI SDK 5.0"
// V5: Use UI message streams with custom data parts
const stream = createUIMessageStream({
execute({ writer }) {
// Write custom data instead of message annotations
writer.write({
type: 'data-custom',
id: 'custom-1',
data: { customField: 'value' },
});
},
});
```
#### UIMessage Reasoning Structure
The reasoning property on UI messages has been moved to parts.
```tsx filename="AI SDK 4.0"
const message: Message = {
role: 'assistant',
content: 'Hello',
reasoning: 'I will greet the user',
};
```
```tsx filename="AI SDK 5.0"
const message: UIMessage = {
role: 'assistant',
parts: [
{
type: 'reasoning',
text: 'I will greet the user',
},
{
type: 'text',
text: 'Hello',
},
],
};
```
#### Reasoning Part Property Rename
The `reasoning` property on reasoning UI parts has been renamed to `text`.
```tsx filename="AI SDK 4.0"
{
message.parts.map((part, index) => {
if (part.type === 'reasoning') {
return (
<div key={index} className="reasoning-display">
{part.reasoning}
</div>
);
}
});
}
```
```tsx filename="AI SDK 5.0"
{
message.parts.map((part, index) => {
if (part.type === 'reasoning') {
return (
<div key={index} className="reasoning-display">
{part.text}
</div>
);
}
});
}
```
### File Part Changes
File parts now use `.url` instead of `.data` and `.mimeType`.
```tsx filename="AI SDK 4.0"
{
messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <div key={index}>{part.text}</div>;
} else if (part.type === 'file' && part.mimeType.startsWith('image/')) {
return (
<img
key={index}
src={`data:${part.mimeType};base64,${part.data}`}
/>
);
}
})}
</div>
));
}
```
```tsx filename="AI SDK 5.0"
{
messages.map(message => (
<div key={message.id}>
{message.parts.map((part, index) => {
if (part.type === 'text') {
return <div key={index}>{part.text}</div>;
} else if (
part.type === 'file' &&
part.mediaType.startsWith('image/')
) {
return <img key={index} src={part.url} />;
}
})}
</div>
));
}
```
### Stream Data Removal
The `StreamData` class has been completely removed and replaced with UI message streams for custom data.
```tsx filename="AI SDK 4.0"
import { StreamData } from 'ai';
const streamData = new StreamData();
streamData.append('custom-data');
streamData.close();
```
```tsx filename="AI SDK 5.0"
import { createUIMessageStream, createUIMessageStreamResponse } from 'ai';
const stream = createUIMessageStream({
execute({ writer }) {
// Write custom data parts
writer.write({
type: 'data-custom',
id: 'custom-1',
data: 'custom-data',
});
// Can merge with LLM streams
const result = streamText({
model: __MODEL__,
messages,
});
writer.merge(result.toUIMessageStream());
},
});
return createUIMessageStreamResponse({ stream });
```
### Custom Data Streaming: writeMessageAnnotation/writeData Removed
The `writeMessageAnnotation` and `writeData` methods from `DataStreamWriter` have been removed. Instead, use custom data parts with the new `UIMessage` stream architecture.
```tsx filename="AI SDK 4.0"
import { createDataStreamResponse, streamText } from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
return createDataStreamResponse({
execute: dataStream => {
// Write general data
dataStream.writeData('call started');
const result = streamText({
model: __MODEL__,
messages,
onChunk() {
// Write message annotations
dataStream.writeMessageAnnotation({
status: 'streaming',
timestamp: Date.now(),
});
},
onFinish() {
// Write final annotations
dataStream.writeMessageAnnotation({
id: generateId(),
completed: true,
});
dataStream.writeData('call completed');
},
});
result.mergeIntoDataStream(dataStream);
},
});
}
```
```tsx filename="AI SDK 5.0"
import {
createUIMessageStream,
createUIMessageStreamResponse,
streamText,
generateId,
} from 'ai';
export async function POST(req: Request) {
const { messages } = await req.json();
const stream = createUIMessageStream({
execute: ({ writer }) => {
const statusId = generateId();
// Write general data (transient - not added to message history)
writer.write({
type: 'data-status',
id: statusId,
data: { status: 'call started' },
});
const result = streamText({
model: __MODEL__,
messages,
onChunk() {
// Write data parts that update during streaming
writer.write({
type: 'data-status',
id: statusId,
data: {
status: 'streaming',
timestamp: Date.now(),
},
});
},
onFinish() {
// Write final data parts
writer.write({
type: 'data-status',
id: statusId,
data: {
status: 'completed',
},
});
},
});
writer.merge(result.toUIMessageStream());
},
});
return createUIMessageStreamResponse({ stream });
}
```
<Note>
For more detailed information about streaming custom data in v5, see the
[Streaming Data guide](/docs/ai-sdk-ui/streaming-data).
</Note>
##### Provider Metadata → Provider Options
The `providerMetadata` input parameter has been renamed to `providerOptions`. Note that the returned metadata in results is still called `providerMetadata`.
```tsx filename="AI SDK 4.0"
const result = await generateText({
model: 'openai/gpt-5',
prompt: 'Hello',
providerMetadata: {
openai: { store: false },
},
});
```
```tsx filename="AI SDK 5.0"
const result = await generateText({
model: 'openai/gpt-5',
prompt: 'Hello',
providerOptions: {
// Input parameter renamed
openai: { store: false },
},
});
// Returned metadata still uses providerMetadata:
console.log(result.providerMetadata?.openai);
```
#### Tool Definition Changes (parameters → inputSchema)
Tool definitions have been updated to use `inputSchema` instead of `parameters` and error classes have been renamed.
```tsx filename="AI SDK 4.0"
import { tool } from 'ai';
const weatherTool = tool({
description: 'Get the weather for a city',
parameters: z.object({
city: z.string(),
}),
execute: async ({ city }) => {
return `Weather in ${city}`;
},
});
```
```tsx filename="AI SDK 5.0"
import { tool } from 'ai';
const weatherTool = tool({
description: 'Get the weather for a city',
inputSchema: z.object({
city: z.string(),
}),
execute: async ({ city }) => {
return `Weather in ${city}`;
},
});
```
#### Tool Result Content: experimental_toToolResultContent → toModelOutput
The `experimental_toToolResultContent` option has been renamed to `toModelOutput` and is no longer experimental.
```tsx filename="AI SDK 4.0"
const screenshotTool = tool({
description: 'Take a screenshot',
parameters: z.object({}),
execute: async () => {
const imageData = await takeScreenshot();
return imageData; // base64 string
},
experimental_toToolResultContent: result => [{ type: 'image', data: result }],
});
```
```tsx filename="AI SDK 5.0"
const screenshotTool = tool({
description: 'Take a screenshot',
inputSchema: z.object({}),
execute: async () => {
const imageData = await takeScreenshot();
return imageData;
},
toModelOutput: result => ({
type: 'content',
value: [{ type: 'media', mediaType: 'image/png', data: result }],
}),
});
```
### Tool Property Changes (args/result → input/output)
Tool call and result properties have been renamed for better consistency with schemas.
```tsx filename="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;
}
}
```
```tsx filename="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;
}
}
```
### Tool Execution Error Handling
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.
```tsx filename="AI SDK 4.0"
import { ToolExecutionError } from 'ai';
try {
const result = await generateText({
// ...
});
} catch (error) {
if (error instanceof ToolExecutionError) {
console.log('Tool execution failed:', error.message);
console.log('Tool name:', error.toolName);
console.log('Tool input:', error.toolInput);
}
}
```
```tsx filename="AI SDK 5.0"
// Tool execution errors now appear in result steps
const { steps } = await generateText({
// ...
});
// check for tool errors in the steps
const toolErrors = steps.flatMap(step =>
step.content.filter(part => part.type === 'tool-error'),
);
toolErrors.forEach(toolError => {
console.log('Tool error:', toolError.error);
console.log('Tool name:', toolError.toolName);
console.log('Tool input:', toolError.input);
});
```
For streaming scenarios, tool execution errors appear as `tool-error` parts in the stream, while other errors appear as `error` parts.
### Tool Call Streaming Now Default (toolCallStreaming Removed)
The `toolCallStreaming` option has been removed in AI SDK 5.0. Tool call streaming is now always enabled by default.
```tsx filename="AI SDK 4.0"
const result = streamText({
model: __MODEL__,
messages,
toolCallStreaming: true, // Optional parameter to enable streaming
tools: {
weatherTool,
searchTool,
},
});
```
```tsx filename="AI SDK 5.0"
const result = streamText({
model: __MODEL__,
messages: convertToModelMessages(messages),
// toolCallStreaming removed - streaming is always enabled
tools: {
weatherTool,
searchTool,
},
});
```
### Tool Part Type Changes (UIMessage)
In v5, UI tool parts use typed naming: `tool-${toolName}` instead of generic types.
```tsx filename="AI SDK 4.0"
// Generic tool-invocation type
{
message.parts.map(part => {
if (part.type === 'tool-invocation') {
return <div>{part.toolInvocation.toolName}</div>;
}
});
}
```
```tsx filename="AI SDK 5.0"
// 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>;
}
});
}
```
### Dynamic Tools Support
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.
#### New dynamicTool Helper
The new `dynamicTool` helper function allows you to define tools where the input and output types are not known at compile time.
```tsx filename="AI SDK 5.0"
import { dynamicTool } from 'ai';
import { z } from 'zod';
// Define a dynamic tool
const 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 Without Schemas
MCP tools that don't provide schemas are now automatically treated as dynamic tools:
```tsx filename="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
```
#### Type-Safe Handling with Mixed Tools
When using both static and dynamic tools together, use the `dynamic` flag for type narrowing:
```tsx filename="AI SDK 5.0"
const result = await generateText({
model: __MODEL__,
tools: {
// Static tool with known types
weather: weatherTool,
// Dynamic tool with unknown types
customDynamicTool: dynamicTool({
/* ... */
}),
},
onStepFinish: step => {
// Handle tool calls with type safety
for (const toolCall of step.toolCalls) {
if (toolCall.dynamic) {
// Dynamic tool: input/output are 'unknown'
console.log('Dynamic tool called:', toolCall.toolName);
continue;
}
// Static tools have full type inference
switch (toolCall.toolName) {
case 'weather':
// TypeScript knows the exact types
console.log(toolCall.input.location); // string
break;
}
}
},
});
```
#### New dynamic-tool UI Part
UI messages now include a `dynamic-tool` part type for rendering dynamic tool invocations:
```tsx filename="AI SDK 5.0"
{
message.parts.map((part, index) => {
switch (part.type) {
// Static tools use specific types
case 'tool-weather':
return <div>Weather: {part.input.city}</div>;
// Dynamic tools use the generic dynamic-tool type
case 'dynamic-tool':
return (
<div>
Dynamic tool: {part.toolName}
<pre>{JSON.stringify(part.input, null, 2)}</pre>
</div>
);
}
});
}
```
#### Breaking Change: Type Narrowing Required for Tool Calls and Results
When iterating over `toolCalls` and `toolResults`, you now need to check the `dynamic` flag first for proper type narrowing:
```tsx filename="AI SDK 4.0"
// Direct type checking worked without dynamic flag
onStepFinish: 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;
}
}
};
```
```tsx filename="AI SDK 5.0"
// Must check dynamic flag first for type narrowing
onStepFinish: 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;
}
}
};
```
### Tool UI Part State Changes
Tool UI parts now use more granular states that better represent the streaming lifecycle and error handling.
```tsx filename="AI SDK 4.0"
// Old states
{
message.parts.map(part => {
if (part.type === 'tool-invocation') {
switch (part.toolInvocation.state) {
case 'partial-call':
return <div>Loading...</div>;
case 'call':
return (
<div>
Tool called with {JSON.stringify(part.toolInvocation.args)}
</div>
);
case 'result':
return <div>Result: {part.toolInvocation.result}</div>;
}
}
});
}
```
```tsx filename="AI SDK 5.0"
// New granular states
{
message.parts.map(part => {
switch (part.type) {
case 'tool-getWeatherInformation':
switch (part.state) {
case 'input-streaming':
return <pre>{JSON.stringify(part.input, null, 2)}</pre>;
case 'input-available':
return <div>Getting weather for {part.input.city}...</div>;
case 'output-available':
return <div>Weather: {part.output}</div>;
case 'output-error':
return <div>Error: {part.errorText}</div>;
}
}
});
}
```
**State Changes:**
- `partial-call` → `input-streaming` (tool input being streamed)
- `call` → `input-available` (tool input complete, ready to execute)
- `result` → `output-available` (tool execution successful)
- New: `output-error` (tool execution failed)
#### Rendering Tool Invocations (Catch-All Pattern)
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.
```tsx filename="AI SDK 4.0"
{
message.parts.map((part, index) => {
switch (part.type) {
case 'text':
return <div key={index}>{part.text}</div>;
case 'tool-invocation':
const { toolInvocation } = part;
return (
<details key={`tool-${toolInvocation.toolCallId}`}>
<summary>
<span>{toolInvocation.toolName}</span>
{toolInvocation.state === 'result' ? (
<span>Click to expand</span>
) : (
<span>calling...</span>
)}
</summary>
{toolInvocation.state === 'result' ? (
<div>
<pre>{JSON.stringify(toolInvocation.result, null, 2)}</pre>
</div>
) : null}
</details>
);
}
});
}
```
```tsx filename="AI SDK 5.0"
import { isToolUIPart, getToolName } from 'ai';
{
message.parts.map((part, index) => {
switch (part.type) {
case 'text':
return <div key={index}>{part.text}</div>;
default:
if (isToolUIPart(part)) {
const toolInvocation = part;
return (
<details key={`tool-${toolInvocation.toolCallId}`}>
<summary>
<span>{getToolName(toolInvocation)}</span>
{toolInvocation.state === 'output-available' ? (
<span>Click to expand</span>
) : (
<span>calling...</span>
)}
</summary>
{toolInvocation.state === 'output-available' ? (
<div>
<pre>{JSON.stringify(toolInvocation.output, null, 2)}</pre>
</div>
) : null}
</details>
);
}
}
});
}
```
#### Media Type Standardization
`mimeType` has been renamed to `mediaType` for consistency. Both image and file types are supported in model messages.
```tsx filename="AI SDK 4.0"
const result = await generateText({
model: someModel,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'What do you see?' },
{
type: 'image',
image: new Uint8Array([0, 1, 2, 3]),
mimeType: 'image/png',
},
{
type: 'file',
data: contents,
mimeType: 'application/pdf',
},
],
},
],
});
```
```tsx filename="AI SDK 5.0"
const result = await generateText({
model: someModel,
messages: [
{
role: 'user',
content: [
{ type: 'text', text: 'What do you see?' },
{
type: 'image',
image: new Uint8Array([0, 1, 2, 3]),
mediaType: 'image/png',
},
{
type: 'file',
data: contents,
mediaType: 'application/pdf',
},
],
},
],
});
```
### Reasoning Support
#### Reasoning Text Property Rename
The `.reasoning` property has been renamed to `.reasoningText` for multi-step generations.
```tsx filename="AI SDK 4.0"
for (const step of steps) {
console.log(step.reasoning);
}
```
```tsx filename="AI SDK 5.0"
for (const step of steps) {
console.log(step.reasoningText);
}
```
#### Generate Text Reasoning Property Changes
In `generateText()` and `streamText()` results, reasoning properties have been renamed.
```tsx filename="AI SDK 4.0"
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
prompt: 'Explain your reasoning',
});
console.log(result.reasoning); // String reasoning text
console.log(result.reasoningDetails); // Array of reasoning details
```
```tsx filename="AI SDK 5.0"
const result = await generateText({
model: anthropic('claude-sonnet-4-20250514'),
prompt: 'Explain your reasoning',
});
console.log(result.reasoningText); // String reasoning text
console.log(result.reasoning); // Array of reasoning details
```
### Continuation Steps Removal
The `experimental_continueSteps` option has been removed from `generateText()`.
```tsx filename="AI SDK 4.0"
const result = await generateText({
experimental_continueSteps: true,
// ...
});
```
```tsx filename="AI SDK 5.0"
const result = await generateText({
// experimental_continueSteps has been removed
// Use newer models with higher output token limits instead
// ...
});
```
### Image Generation Changes
Image model settings have been moved to `providerOptions`.
```tsx filename="AI SDK 4.0"
await generateImage({
model: luma.image('photon-flash-1', {
maxImagesPerCall: 5,
pollIntervalMillis: 500,
}),
prompt,
n: 10,
});
```
```tsx filename="AI SDK 5.0"
await generateImage({
model: luma.image('photon-flash-1'),
prompt,
n: 10,
maxImagesPerCall: 5,
providerOptions: {
luma: { pollIntervalMillis: 500 },
},
});
```
### Step Result Changes
#### Step Type Removal
The `stepType` property has been removed from step results.
```tsx filename="AI SDK 4.0"
steps.forEach(step => {
switch (step.stepType) {
case 'initial':
console.log('Initial step');
break;
case 'tool-result':
console.log('Tool result step');
break;
case 'done':
console.log('Final step');
break;
}
});
```
```tsx filename="AI SDK 5.0"
steps.forEach((step, index) => {
if (index === 0) {
console.log('Initial step');
} else if (step.toolResults.length > 0) {
console.log('Tool result step');
} else {
console.log('Final step');
}
});
```
### Step Control: maxSteps → stopWhen
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.
```tsx filename="AI SDK 4.0"
// V4: Simple numeric limit
const result = await generateText({
model: __MODEL__,
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
});
```
```tsx filename="AI SDK 5.0"
import { stepCountIs, hasToolCall } from 'ai';
// V5: Server-side - flexible stopping conditions with stopWhen
const result = await generateText({
model: __MODEL__,
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: __MODEL__,
messages,
stopWhen: hasToolCall('finalizeTask'), // Stop when finalizeTask tool is called
});
```
**Common stopping patterns:**
```tsx filename="AI SDK 5.0"
// Stop after N steps (equivalent to old maxSteps)
// Note: Only applies when the last step has tool results
stopWhen: stepCountIs(5);
// Stop when specific tool is called
stopWhen: 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 content
stopWhen: ({ 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 vs Total Usage
Usage properties now distinguish between single step and total usage.
```tsx filename="AI SDK 4.0"
// usage contained total token usage across all steps
console.log(result.usage);
```
```tsx filename="AI SDK 5.0"
// usage contains token usage from the final step only
console.log(result.usage);
// totalUsage contains total token usage across all steps
console.log(result.totalUsage);
```
## AI SDK UI Changes
### Package Structure Changes
### `@ai-sdk/rsc` Package Extraction
The `ai/rsc` export has been extracted to a separate package `@ai-sdk/rsc`.
```tsx filename="AI SDK 4.0"
import { createStreamableValue } from 'ai/rsc';
```
```tsx filename="AI SDK 5.0"
import { createStreamableValue } from '@ai-sdk/rsc';
```
<Note>Don't forget to install the new package: `npm install @ai-sdk/rsc`</Note>
### React UI Hooks Moved to `@ai-sdk/react`
The deprecated `ai/react` export has been removed in favor of `@ai-sdk/react`.
```tsx filename="AI SDK 4.0"
import { useChat } from 'ai/react';
```
```tsx filename="AI SDK 5.0"
import { useChat } from '@ai-sdk/react';
```
<Note>
Don't forget to install the new package: `npm install @ai-sdk/react`
</Note>
### useChat Changes
The `useChat` hook has undergone significant changes in v5, with new transport architecture, removal of managed input state, and more.
#### maxSteps Removal
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.
```tsx filename="AI SDK 4.0"
const { messages, sendMessage } = useChat({
maxSteps: 5, // Automatic tool result submission
});
```
```tsx filename="AI SDK 5.0"
// Server-side: Use stopWhen for multi-step control
import { streamText, convertToModelMessages, stepCountIs } from 'ai';
__PROVIDER_IMPORT__;
const result = await streamText({
model: __MODEL__,
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,
});
},
});
```
<Note>
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()`.
</Note>
This change provides more flexibility for handling tool calls and aligns client behavior with server-side multi-step execution patterns.
For more details on the new tool submission approach, see the [Tool Result Submission Changes](#tool-result-submission-changes) section below.
#### Initial Messages Renamed
The `initialMessages` option has been renamed to `messages`.
```tsx filename="AI SDK 4.0"
import { useChat, type Message } from '@ai-sdk/react';
function ChatComponent({ initialMessages }: { initialMessages: Message[] }) {
const { messages } = useChat({
initialMessages: initialMessages,
// ...
});
// your component
}
```
```tsx filename="AI SDK 5.0"
import { useChat, type UIMessage } from '@ai-sdk/react';
function ChatComponent({ initialMessages }: { initialMessages: UIMessage[] }) {
const { messages } = useChat({
messages: initialMessages,
// ...
});
// your component
}
```
#### Sharing Chat Instances
In v4, you could share chat state between components by using the same `id` parameter in multiple `useChat` hooks.
```tsx filename="AI SDK 4.0"
// Component A
const { messages } = useChat({
id: 'shared-chat',
api: '/api/chat',
});
// Component B - would share the same chat state
const { messages } = useChat({
id: 'shared-chat',
api: '/api/chat',
});
```
In v5, you need to explicitly share chat instances by passing a shared `Chat` instance.
```tsx filename="AI SDK 5.0"
// e.g. Store Chat instance in React Context and create a custom hook
// Component A
const { chat } = useSharedChat(); // Custom hook that accesses shared Chat from context
const { messages, sendMessage } = useChat({
chat, // Pass the shared chat instance
});
// Component B - shares the same chat instance
const { chat } = useSharedChat(); // Same hook to access shared Chat from context
const { messages } = useChat({
chat, // Same shared chat instance
});
```
For a complete example of sharing chat state across components, see the [Share Chat State Across Components](/cookbook/next/use-shared-chat-context) recipe.
#### Chat Transport Architecture
Configuration is now handled through transport objects instead of direct API options.
```tsx filename="AI SDK 4.0"
import { useChat } from '@ai-sdk/react';
const { messages } = useChat({
api: '/api/chat',
credentials: 'include',
headers: { 'Custom-Header': 'value' },
});
```
```tsx filename="AI SDK 5.0"
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
const { messages } = useChat({
transport: new DefaultChatTransport({
api: '/api/chat',
credentials: 'include',
headers: { 'Custom-Header': 'value' },
}),
});
```
#### Removed Managed Input State
The `useChat` hook no longer manages input state internally. You must now manage input state manually.
```tsx filename="AI SDK 4.0"
import { useChat } from '@ai-sdk/react';
export default function Page() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: '/api/chat',
});
return (
<form onSubmit={handleSubmit}>
<input value={input} onChange={handleInputChange} />
<button type="submit">Send</button>
</form>
);
}
```
```tsx filename="AI SDK 5.0"
import { useChat } from '@ai-sdk/react';
import { DefaultChatTransport } from 'ai';
import { useState } from 'react';
export default function Page() {
const [input, setInput] = useState('');
const { messages, sendMessage } = useChat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
});
const handleSubmit = e => {
e.preventDefault();
sendMessage({ text: input });
setInput('');
};
return (
<form onSubmit={handleSubmit}>
<input value={input} onChange={e => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
);
}
```
#### Message Sending: `append` → `sendMessage`
The `append` function has been replaced with `sendMessage` and requires structured message format.
```tsx filename="AI SDK 4.0"
const { append } = useChat();
// Simple text message
append({ role: 'user', content: 'Hello' });
// With custom body
append(
{
role: 'user',
content: 'Hello',
},
{ body: { imageUrl: 'https://...' } },
);
```
```tsx filename="AI SDK 5.0"
const { sendMessage } = useChat();
// Simple text message (most common usage)
sendMessage({ text: 'Hello' });
// Or with explicit parts array
sendMessage({
parts: [{ type: 'text', text: 'Hello' }],
});
// With custom body (via request options)
sendMessage(
{ role: 'user', parts: [{ type: 'text', text: 'Hello' }] },
{ body: { imageUrl: 'https://...' } },
);
```
#### Message Regeneration: `reload` → `regenerate`
The `reload` function has been renamed to `regenerate` with enhanced functionality.
```tsx filename="AI SDK 4.0"
const { reload } = useChat();
// Regenerate last message
reload();
```
```tsx filename="AI SDK 5.0"
const { regenerate } = useChat();
// Regenerate last message
regenerate();
// Regenerate specific message
regenerate({ messageId: 'message-123' });
```
#### onResponse Removal
The `onResponse` callback has been removed from `useChat` and `useCompletion`.
```tsx filename="AI SDK 4.0"
const { messages } = useChat({
onResponse(response) {
// handle response
},
});
```
```tsx filename="AI SDK 5.0"
const { messages } = useChat({
// onResponse is no longer available
});
```
#### Send Extra Message Fields Default
The `sendExtraMessageFields` option has been removed and is now the default behavior.
```tsx filename="AI SDK 4.0"
const { messages } = useChat({
sendExtraMessageFields: true,
});
```
```tsx filename="AI SDK 5.0"
const { messages } = useChat({
// sendExtraMessageFields is now the default
});
```
#### Keep Last Message on Error Removal
The `keepLastMessageOnError` option has been removed as it's no longer needed.
```tsx filename="AI SDK 4.0"
const { messages } = useChat({
keepLastMessageOnError: true,
});
```
```tsx filename="AI SDK 5.0"
const { messages } = useChat({
// keepLastMessageOnError is no longer needed
});
```
#### Chat Request Options Changes
The `data` and `allowEmptySubmit` options have been removed from `ChatRequestOptions`.
```tsx filename="AI SDK 4.0"
handleSubmit(e, {
data: { imageUrl: 'https://...' },
body: { custom: 'value' },
allowEmptySubmit: true,
});
```
```tsx filename="AI SDK 5.0"
sendMessage(
{
/* yourMessage */
},
{
body: {
custom: 'value',
imageUrl: 'https://...', // Move data to body
},
},
);
```
#### Request Options Type Rename
`RequestOptions` has been renamed to `CompletionRequestOptions`.
```tsx filename="AI SDK 4.0"
import type { RequestOptions } from 'ai';
```
```tsx filename="AI SDK 5.0"
import type { CompletionRequestOptions } from 'ai';
```
#### addToolResult Renamed to addToolOutput
The `addToolResult` method has been renamed to `addToolOutput`. Additionally, the `result` parameter has been renamed to `output` for consistency with other tool-related APIs.
```tsx filename="AI SDK 4.0"
const { addToolResult } = useChat();
// Add tool result with 'result' parameter
addToolResult({
toolCallId: 'tool-call-123',
result: 'Weather: 72°F, sunny',
});
```
```tsx filename="AI SDK 5.0"
const { addToolOutput } = useChat();
// Add tool output with 'output' parameter and 'tool' name for type safety
addToolOutput({
tool: 'getWeather',
toolCallId: 'tool-call-123',
output: 'Weather: 72°F, sunny',
});
```
<Note>
`addToolResult` is still available but deprecated. It will be removed in
version 6.
</Note>
#### Tool Result Submission Changes
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](#maxsteps-removal))
```tsx filename="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)];
}
},
});
```
```tsx filename="AI SDK 5.0"
import { useChat } from '@ai-sdk/react';
import {
DefaultChatTransport,
lastAssistantMessageIsCompleteWithToolCalls,
} from 'ai';
const { messages, sendMessage, addToolOutput } = useChat({
// Automatic submission with helper
sendAutomaticallyWhen: lastAssistantMessageIsCompleteWithToolCalls,
async onToolCall({ toolCall }) {
if (toolCall.toolName === 'getLocation') {
const cities = ['New York', 'Los Angeles', 'Chicago', 'San Francisco'];
// Important: Don't await inside onToolCall to avoid deadlocks
addToolOutput({
tool: 'getLocation',
toolCallId: toolCall.toolCallId,
output: cities[Math.floor(Math.random() * cities.length)],
});
}
},
});
```
#### Loading State Changes
The deprecated `isLoading` helper has been removed in favor of `status`.
```tsx filename="AI SDK 4.0"
const { isLoading } = useChat();
```
```tsx filename="AI SDK 5.0"
const { status } = useChat();
// Use state instead of isLoading for more granular control
```
#### Resume Stream Support
The resume functionality has been moved from `experimental_resume` to `resumeStream`.
```tsx filename="AI SDK 4.0"
// Resume was experimental
const { messages } = useChat({
experimental_resume: true,
});
```
```tsx filename="AI SDK 5.0"
const { messages } = useChat({
resumeStream: true, // Resume interrupted streams
});
```
#### Dynamic Body Values
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.
```tsx filename="AI SDK 4.0"
const [temperature, setTemperature] = useState(0.7);
const { messages } = useChat({
api: '/api/chat',
body: {
temperature, // This would update dynamically in v4
},
});
```
```tsx filename="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 time
sendMessage(
{ text: input },
{
body: {
temperature, // Current temperature value at request time
},
},
);
// Option 2: Use function configuration with useRef
const 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](/docs/ai-sdk-ui/chatbot#request-configuration).
#### Usage Information
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`.
```tsx filename="AI SDK 4.0"
const { messages } = useChat({
onFinish(message, options) {
const usage = options.usage;
console.log('Usage:', usage);
},
});
```
```tsx filename="AI SDK 5.0"
import {
convertToModelMessages,
streamText,
UIMessage,
type LanguageModelUsage,
} from 'ai';
__PROVIDER_IMPORT__;
// 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: __MODEL__,
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.
```tsx filename="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`:
```tsx filename="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);
},
});
}
```
#### Request Body Preparation: experimental_prepareRequestBody → prepareSendMessagesRequest
The `experimental_prepareRequestBody` option has been replaced with `prepareSendMessagesRequest` in the transport configuration.
```tsx filename="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 };
},
});
```
```tsx filename="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 } };
},
}),
});
```
### `@ai-sdk/vue` Changes
The Vue.js integration has been completely restructured, replacing the `useChat` composable with a `Chat` class.
#### useChat Replaced with Chat Class
```typescript filename="@ai-sdk/vue v1"
<script setup>
import { useChat } from '@ai-sdk/vue';
const { messages, input, handleSubmit } = useChat({
api: '/api/chat',
});
</script>
```
```typescript filename="@ai-sdk/vue v2"
<script setup>
import { Chat } from '@ai-sdk/vue';
import { DefaultChatTransport } from 'ai';
import { ref } from 'vue';
const input = ref('');
const chat = new Chat({
transport: new DefaultChatTransport({ api: '/api/chat' }),
})