Tool Calling
As covered under Foundations, tools are objects that can be called by the model to perform a specific task. Function tools and dynamic tools contain several core elements:
description: An optional description of the tool that can influence when the tool is picked. It can be a string or a function that derives the description from the tool's context and experimental sandbox.inputSchema: A Zod schema or a JSON schema that defines the input parameters. The schema is consumed by the LLM, and also used to validate the LLM tool calls.execute: An optional async function that is called with the inputs from the tool call. It produces a value of typeRESULT(generic type). It is optional because you might want to forward tool calls to the client or to a queue instead of executing them in the same process.strict: (optional, boolean) Enables strict tool calling when supported by the provider
You can use the tool helper function to
infer the types of the execute parameters.
The tools parameter of generateText and streamText is an object that has the tool names as keys and the tools as values:
import { z } from 'zod';import { generateText, tool, isStepCount } from 'ai';
const result = await generateText({ model: "xai/grok-4.5", tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: isStepCount(5), prompt: 'What is the weather in San Francisco?',});When a model uses a tool, it is called a "tool call" and the output of the tool is called a "tool result".
Tool calling is not restricted to only text generation. You can also use it to render user interfaces (Generative UI).
Dynamic Descriptions
Tool descriptions can be fixed strings or functions. Use a function when the description sent to the model should depend on the current tool context or experimental sandbox, such as a tenant, project, environment, or workspace.
The description function is resolved before the tool definition is sent to the
model for each generation step. It receives the matching tool context from
toolsContext and the current experimental_sandbox, if one was provided. If prepareStep
updates toolsContext or experimental_sandbox, the next step uses those updated values.
import { generateText, tool } from 'ai';import { z } from 'zod';
const shell = tool({ contextSchema: z.object({ projectName: z.string(), }), description: ({ context, experimental_sandbox }) => [ `Run shell commands for the ${context.projectName} project.`, experimental_sandbox != null ? `Sandbox: ${experimental_sandbox.description}` : undefined, ] .filter(Boolean) .join('\n'), inputSchema: z.object({ command: z.string(), }), execute: async ({ command }, { experimental_sandbox }) => { if (!experimental_sandbox) { throw new Error('Experimental sandbox is not available'); }
return experimental_sandbox.run({ command }); },});
const result = await generateText({ model: "xai/grok-4.5", tools: { shell }, toolsContext: { shell: { projectName: 'web-app' }, }, experimental_sandbox, prompt: 'List the project files.',});Strict Mode
When enabled, language model providers that support strict tool calling will only generate tool calls that are valid according to your defined inputSchema.
This increases the reliability of tool calling.
However, not all schemas may be supported in strict mode, and what is supported depends on the specific provider.
By default, strict mode is disabled. You can enable it per-tool by setting strict: true:
tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string(), }), strict: true, // Enable strict validation for this tool execute: async ({ location }) => ({ // ... }),});Not all providers or models support strict mode. For those that do not, this option is ignored.
Input Examples
You can specify example inputs for your tools to help guide the model on how input data should be structured. When supported by providers, input examples can help when JSON schema itself does not fully specify the intended usage or when there are optional values.
tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), inputExamples: [ { input: { location: 'San Francisco' } }, { input: { location: 'London' } }, ], execute: async ({ location }) => { // ... },});Only the Anthropic providers supports tool input examples natively. Other providers ignore the setting.
Tool Execution Approval
By default, tools with an execute function run automatically as the model calls them. Configure approval with toolApproval on generateText, streamText, or ToolLoopAgent.
toolApproval can be either:
- A
GenericToolApprovalFunctionfor all tool calls - A per-tool map of statuses and/or
SingleToolApprovalFunctioncallbacks (either function may returnundefinedfor the same effect as'not-applicable')
The older needsApproval property on tool() definitions is deprecated. Existing code still works, but new code should move approval logic to toolApproval.
Configure toolApproval
const result = await generateText({ model: "xai/grok-4.5", tools: { runCommand }, toolApproval: { runCommand: 'user-approval', }, prompt: 'Remove the most recent file in the downloads folder',});In the per-tool object form, each key can be one of four statuses, either as a
string or as an object with a type field:
'not-applicable': the tool is executed without an approval flow. This is the default behavior.'approved': record an automatic approval by emitting approval request/response parts in the output, then execute the tool'denied': record an automatic denial by emitting approval request/response parts in the output, then surface a denied tool output'user-approval': emit an approval request and wait for an explicit response
For automatic approvals and denials, you can also return an object to include a reason:
toolApproval: { runCommand: { type: 'denied', reason: 'blocked by policy', },}Generic toolApproval function
Pass a GenericToolApprovalFunction as toolApproval to handle every tool
call in one place. The callback receives toolCall, tools, toolsContext,
messages, and runtimeContext (the same runtimeContext you pass to
generateText or streamText, typed as the second ToolApprovalConfiguration
type parameter; it defaults to Context). It may return undefined for the same effect as 'not-applicable'.
const result = await generateText({ model: "xai/grok-4.5", tools: { runCommand }, toolApproval: ({ toolCall, tools, toolsContext, messages, runtimeContext, }) => { if (toolCall.toolName === 'runCommand' && !toolCall.dynamic) { // Inspect toolCall.input, cross-tool state, messages, or runtimeContext for policy. return 'user-approval'; } return undefined; // or 'not-applicable' }, prompt: 'Remove the most recent file in the downloads folder',});The same pattern works for streamText and for toolApproval on
ToolLoopAgent when you need one policy
across all tools.
For a per-tool object instead, each key can be a SingleToolApprovalFunction
that receives the tool input and options toolCallId, messages, toolContext
(the same shape as tool execution context, without abortSignal), and
runtimeContext. Return undefined for the same effect as 'not-applicable'.
This is useful for tools that perform sensitive operations like executing commands, processing payments, modifying data, and more potentially dangerous actions.
How It Works
When a tool requires manual approval, generateText and streamText don't pause execution. Instead, they complete and return tool-approval-request parts in the result content. This means the manual approval flow requires two calls to the model: the first returns the approval request, and the second (after receiving the approval response) either executes the tool or informs the model that approval was denied.
Manual approval comes from toolApproval returning 'user-approval'. If the
tool should just execute normally, use 'not-applicable', return undefined
from a GenericToolApprovalFunction or SingleToolApprovalFunction, or omit
the setting entirely. If toolApproval returns 'approved' or 'denied' or
their object forms, the SDK records that decision automatically in the same
generation by emitting approval request/response parts. When you provide a
reason on an automatic approval or denial, that reason is included in the
emitted approval response and can be rendered in the UI.
Here's the manual approval flow:
- Call
generateTextorstreamTextwithtoolApproval - The model generates a tool call
- The call returns
tool-approval-requestparts inresult.content - Your app requests approval and collects the user's decision
- Add a
tool-approval-responseto the messages array - Call
generateTextorstreamTextagain with the updated messages - If approved, the tool runs and returns a result. If denied, the model sees the denial and responds accordingly.
Handling Approval Requests
After calling generateText or streamText, check result.content for tool-approval-request parts:
import { type ModelMessage, generateText } from 'ai';
const messages: ModelMessage[] = [ { role: 'user', content: 'Remove the most recent file' },];const result = await generateText({ model: "xai/grok-4.5", tools: { runCommand }, messages,});
messages.push(...result.responseMessages);
for (const part of result.content) { if (part.type === 'tool-approval-request' && !part.isAutomatic) { console.log(part.approvalId); // Unique ID for this approval request console.log(part.toolCall); // Contains toolName, input, etc. }}To respond, create a tool-approval-response and add it to your messages:
import { type ToolApprovalResponse } from 'ai';
const approvals: ToolApprovalResponse[] = [];
for (const part of result.content) { if (part.type === 'tool-approval-request' && !part.isAutomatic) { const response: ToolApprovalResponse = { type: 'tool-approval-response', approvalId: part.approvalId, approved: true, // or false to deny reason: 'User confirmed the command', // Optional context for the model }; approvals.push(response); }}
// add approvals to messagesmessages.push({ role: 'tool', content: approvals });Then call generateText or streamText again with the updated messages. If approved, the tool executes. If denied, the model receives the denial and can respond accordingly.
When the tool should execute without any approval metadata in the output, use
'not-applicable', return undefined from an approval function, or omit toolApproval. Use 'approved', 'denied', or
their object forms only when you want the result to include an automatic
tool-approval-request with isAutomatic: true followed by a
tool-approval-response, so you can inspect or render the decision without
prompting the user again.
When a tool execution is denied, consider adding a system instruction like "When a tool execution is not approved, do not retry it" to prevent the model from attempting the same call again.
Provider-executed tools are executed provider-side without considering the
tool approval setting. toolApproval (and the deprecated needsApproval)
only control tools that the AI SDK executes locally.
Dynamic Approval
You can make approval decisions based on tool input by providing an async
function in a per-tool toolApproval object:
const paymentTool = tool({ description: 'Process a payment', inputSchema: z.object({ amount: z.number(), recipient: z.string(), }), execute: async ({ amount, recipient }) => { return await processPayment(amount, recipient); },});
const result = await generateText({ model: "xai/grok-4.5", tools: { processPayment: paymentTool, }, toolApproval: { processPayment: async ({ amount }) => amount > 1000 ? 'user-approval' : undefined, }, prompt: 'Send $1500 to the contractor',});In this example, only transactions over $1000 require approval. Smaller transactions execute automatically.
You can use a SingleToolApprovalFunction in a per-tool toolApproval object when you want to return
'not-applicable', undefined (equivalent to 'not-applicable'), 'approved', 'denied', or 'user-approval' at call time
instead of defining the default on the tool itself. For automatic approvals and
denials, the callback can also return { type, reason }, for example
{ type: 'denied', reason: 'blocked by policy' }. For decisions that need the full toolCall or
several tools at once, pass a GenericToolApprovalFunction as toolApproval instead
of a per-tool map. The generic function can return undefined the same way.
Tool Execution Approval with useChat
When using useChat, the approval flow is handled through UI state. See Chatbot Tool Usage for details on handling approvals in your UI with addToolApprovalResponse.
Multi-Step Calls (using stopWhen)
With the stopWhen setting, you can enable multi-step calls in generateText and streamText. When stopWhen is set and the model generates a tool call, the AI SDK will trigger a new generation passing in the tool result until there are no further tool calls or the stopping condition is met.
The AI SDK provides several built-in stopping conditions:
isStepCount(count)— stops after a specified number of steps (default:isStepCount(20))hasToolCall(...toolNames)— stops when any of the specified tools is calledisLoopFinished()— never triggers, letting the loop run until naturally finished
You can also combine multiple conditions in an array or create custom conditions. See Loop Control for more details.
The stopWhen conditions are only evaluated when the last step contains tool
results.
By default, when you use generateText or streamText, it triggers a single generation. This works well for many use cases where you can rely on the model's training data to generate a response. However, when you provide tools, the model now has the choice to either generate a normal text response, or generate a tool call. If the model generates a tool call, its generation is complete and that step is finished.
You may want the model to generate text after the tool has been executed, either to summarize the tool results in the context of the users query. In many cases, you may also want the model to use multiple tools in a single response. This is where multi-step calls come in.
You can think of multi-step calls in a similar way to a conversation with a human. When you ask a question, if the person does not have the requisite knowledge in their common knowledge (a model's training data), the person may need to look up information (use a tool) before they can provide you with an answer. In the same way, the model may need to call a tool to get the information it needs to answer your question where each generation (tool call or text generation) is a step.
Example
In the following example, there are two steps:
- Step 1
- The prompt
'What is the weather in San Francisco?'is sent to the model. - The model generates a tool call.
- The tool call is executed.
- The prompt
- Step 2
- The tool result is sent to the model.
- The model generates a response considering the tool result.
import { z } from 'zod';import { generateText, tool, isStepCount } from 'ai';
const { text, steps } = await generateText({ model: "xai/grok-4.5", tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, stopWhen: isStepCount(5), // stop after a maximum of 5 steps if tools were called prompt: 'What is the weather in San Francisco?',});streamText in a similar way.Steps
To access intermediate tool calls and results, you can use the steps property in the result object
or the streamText onEnd callback.
It contains all the text, tool calls, tool results, per-step performance, and more from each step.
Example: Extract tool results from all steps
import { generateText } from 'ai';
const { steps } = await generateText({ model: "xai/grok-4.5", stopWhen: isStepCount(10), // ...});
// extract all tool calls from the steps:const allToolCalls = steps.flatMap(step => step.toolCalls);onStepEnd callback
When using generateText or streamText, you can provide an onStepEnd callback that
is triggered when a step is finished,
i.e. all text deltas, tool calls, and tool results for the step are available.
When you have multiple steps, the callback is triggered for each step.
The callback receives a stepNumber (zero-based) to identify which step just completed:
import { generateText } from 'ai';
const result = await generateText({ // ... onStepEnd({ stepNumber, text, toolCalls, toolResults, finishReason, usage, performance, }) { console.log(`Step ${stepNumber} finished (${finishReason})`, { usage, performance, }); // your own logic, e.g. for saving the chat history or recording usage },});Tool execution lifecycle callbacks
You can use onToolExecutionStart and onToolExecutionEnd to observe tool execution.
These callbacks are called right before and after each tool's execute function, giving you
visibility into tool execution timing, inputs, outputs, and errors:
import { generateText } from 'ai';
const result = await generateText({ // ... model, tools, prompt onToolExecutionStart({ toolCall }) { console.log(`Calling tool: ${toolCall.toolName}`, { toolCallId: toolCall.toolCallId, input: toolCall.input, }); }, onToolExecutionEnd({ toolCall, toolExecutionMs, toolOutput }) { if (toolOutput.type === 'tool-error') { console.error( `Tool ${toolCall.toolName} failed after ${toolExecutionMs}ms:`, toolOutput.error, ); } else { console.log( `Tool ${toolCall.toolName} completed in ${toolExecutionMs}ms`, { output: toolOutput.output, }, ); } },});Errors thrown inside these callbacks are silently caught and do not break the generation flow.
prepareStep callback
The prepareStep callback is called before a step is started.
It is called with the following parameters:
model: The model that was passed intogenerateText.stopWhen: The stopping condition that was passed intogenerateText.stepNumber: The number of the step that is being executed.steps: The steps that have been executed so far.instructions: The instructions that will be sent to the model for the current step. IfprepareStepreturns aninstructionsoverride, those instructions carry forward as the default for later steps.initialInstructions: The instructions that were passed intogenerateTextorstreamText.messages: The messages that will be sent to the model for the current step. Treat this as the loop's current message state. IfprepareStepreturns amessagesoverride, those messages carry forward as the base for later steps.initialMessages: The messages that were passed intogenerateTextorstreamText.responseMessages: The accumulated assistant/tool response messages so far, including any tool results generated before the first model step from approved tool calls in the input messages.runtimeContext: The runtime context passed via theruntimeContextsetting.toolsContext: The per-tool context map passed via thetoolsContextsetting.experimental_sandbox: The experimental sandbox passed via theexperimental_sandboxsetting.
You can use it to provide different settings for a step, including modifying the input messages.
import { generateText } from 'ai';
const result = await generateText({ // ... prepareStep: async ({ model, stepNumber, steps, messages }) => { if (stepNumber === 0) { return { // use a different model for this step: model: modelForThisParticularStep, // force a tool choice for this step: toolChoice: { type: 'tool', toolName: 'tool1' }, // limit the tools that are available for this step: activeTools: ['tool1'], }; }
// when nothing is returned, the default settings are used },});If you return instructions, those instructions carry forward to later steps until prepareStep returns another instructions or system override. Use initialInstructions when you need to restore or compare against the top-level instructions from the original call.
Message Modification for Longer Agentic Loops
In longer agentic loops, you can use the messages parameter to mutate the message state that will be used by later steps. This is particularly useful for context compaction, and you decide when compaction should happen.
The messages parameter contains the messages for the current step. By default, this is initialMessages followed by the accumulated responseMessages. If a previous prepareStep returned messages, later steps use those persisted messages plus the response messages from the previous step.
Use initialMessages when you need the original input messages and responseMessages when you need the discrete assistant/tool response messages from the model so far.
The pruneMessages helper provides a built-in way to remove selected messages or message parts. You can use it inside prepareStep when you want a simple compaction strategy.
import { generateText, pruneMessages, type ModelMessage } from 'ai';
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => { return JSON.stringify(messages).length / 4;};
const result = await generateText({ // ... prepareStep: ({ messages }) => { if (estimateTokens(messages) > COMPACTION_THRESHOLD) { return { messages: pruneMessages({ messages, reasoning: 'all', toolCalls: 'before-last-3-messages', emptyMessages: 'remove', }), }; } },});This example uses an estimated token threshold, but you can use any trigger. The key behavior is that returning messages mutates the message state for later steps.
Returned message changes persist across steps. If you want to derive each step's messages from the original input plus the discrete response messages instead of the persisted message state, rebuild them from initialMessages and responseMessages each time:
prepareStep: ({ initialMessages, responseMessages, stepNumber }) => { if (stepNumber > 0) { return { messages: [...initialMessages, ...responseMessages.slice(-10)], }; }},Provider Options for Step Configuration
You can use providerOptions in prepareStep to pass provider-specific configuration for each step. This is useful for features like Anthropic's code execution container persistence:
import { forwardAnthropicContainerIdFromLastStep } from '@ai-sdk/anthropic';
// Propagate container ID from previous step for code execution continuityprepareStep: forwardAnthropicContainerIdFromLastStep,Response Messages
Adding the generated assistant and tool messages to your conversation history is a common task, especially if you are using multi-step tool calls.
Both generateText and streamText have a responseMessages property that you can use to
add the assistant and tool messages to your conversation history.
It is also available in the onEnd callback of streamText.
The responseMessages property contains the accumulated response messages from the call as an array of ModelMessage objects that you can add to your conversation history:
import { generateText, ModelMessage } from 'ai';
const messages: ModelMessage[] = [ // ...];
const { responseMessages } = await generateText({ // ... messages,});
// add the response messages to your conversation history:messages.push(...responseMessages); // streamText: ...(await result.responseMessages)Dynamic Tools
AI SDK Core supports dynamic tools for scenarios where tool schemas are not known at compile time. This is useful for:
- MCP (Model Context Protocol) tools without schemas
- User-defined functions at runtime
- Tools loaded from external sources
Using dynamicTool
The dynamicTool helper creates tools with unknown input/output types:
import { dynamicTool } from 'ai';import { z } from 'zod';
const customTool = dynamicTool({ description: 'Execute a custom function', inputSchema: z.object({}), execute: async input => { // input is typed as 'unknown' // You need to validate/cast it at runtime const { action, parameters } = input as any;
// Execute your dynamic logic return { result: `Executed ${action}` }; },});Type-Safe Handling
When using both static and dynamic tools, use the dynamic flag for type narrowing:
const result = await generateText({ model: "xai/grok-4.5", tools: { // Static tool with known types weather: weatherTool, // Dynamic tool custom: dynamicTool({ /* ... */ }), }, onStepEnd: ({ toolCalls, toolResults }) => { // Type-safe iteration for (const toolCall of toolCalls) { if (toolCall.dynamic) { // Dynamic tool: input is 'unknown' console.log('Dynamic:', toolCall.toolName, toolCall.input); continue; }
// Static tool: full type inference switch (toolCall.toolName) { case 'weather': console.log(toolCall.input.location); // typed as string break; } } },});Preliminary Tool Results
You can return an AsyncIterable over multiple results.
In this case, the last value from the iterable is the final tool result.
This can be used in combination with generator functions to e.g. stream status information during the tool execution:
tool({ description: 'Get the current weather.', inputSchema: z.object({ location: z.string(), }), async *execute({ location }) { yield { status: 'loading' as const, text: `Getting weather for ${location}`, weather: undefined, };
await new Promise(resolve => setTimeout(resolve, 3000));
const temperature = 72 + Math.floor(Math.random() * 21) - 10;
yield { status: 'success' as const, text: `The weather in ${location} is ${temperature}°F`, temperature, }; },});Tool Choice
You can use the toolChoice setting to influence when a tool is selected.
It supports the following settings:
auto(default): the model can choose whether and which tools to call.required: the model must call a tool. It can choose which tool to call.none: the model must not call tools{ type: 'tool', toolName: string (typed) }: the model must call the specified tool
import { z } from 'zod';import { generateText, tool } from 'ai';
const result = await generateText({ model: "xai/grok-4.5", tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }), }), }, toolChoice: 'required', // force the model to call a tool prompt: 'What is the weather in San Francisco?',});Tool Execution Options
When tools are called, they receive additional options as a second parameter.
Tool Call ID
The ID of the tool call is forwarded to the tool execution. You can use it e.g. when sending tool-call related information with stream data.
import { streamText, tool, createUIMessageStream, createUIMessageStreamResponse, toUIMessageStream,} from 'ai';
export async function POST(req: Request) { const { messages } = await req.json();
const stream = createUIMessageStream({ execute: ({ writer }) => { const result = streamText({ // ... messages, tools: { myTool: tool({ // ... execute: async (args, { toolCallId }) => { // return e.g. custom status for tool call writer.write({ type: 'data-tool-status', id: toolCallId, data: { name: 'myTool', status: 'in-progress', }, }); // ... }, }), }, });
writer.merge(toUIMessageStream({ stream: result.stream })); }, });
return createUIMessageStreamResponse({ stream });}Messages
The messages that were sent to the language model to initiate the response that contained the tool call are forwarded to the tool execution.
You can access them in the second parameter of the execute function.
In multi-step calls, the messages contain the text, tool calls, and tool results from all previous steps.
import { generateText, tool } from 'ai';
const result = await generateText({ // ... tools: { myTool: tool({ // ... execute: async (args, { messages }) => { // use the message history in e.g. calls to other language models return { ... }; }, }), },});Abort Signals
The abort signals from generateText and streamText are forwarded to the tool execution.
You can access them in the second parameter of the execute function and e.g. abort long-running computations or forward them to fetch calls inside tools.
import { z } from 'zod';import { generateText, tool } from 'ai';
const result = await generateText({ model: "xai/grok-4.5", abortSignal: myAbortSignal, // signal that will be forwarded to tools tools: { weather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string() }), execute: async ({ location }, { abortSignal }) => { return fetch( `https://api.weatherapi.com/v1/current.json?q=${location}`, { signal: abortSignal }, // forward the abort signal to fetch ); }, }), }, prompt: 'What is the weather in San Francisco?',});Experimental Sandbox
Pass experimental_sandbox to generateText, streamText, or a ToolLoopAgent
call when a tool needs to run commands or code in an execution environment. The experimental sandbox is available to tool description functions and on the second parameter of the tool's execute function.
This API is experimental and can change in patch releases. Passing an experimental sandbox does not sandbox the tool itself.
Tool code still runs wherever your application runs. Only the operations that your tool explicitly delegates to
the experimental sandbox, such as experimental_sandbox.run(...),
run in the experimental sandbox environment.
import { generateText, tool } from 'ai';import { z } from 'zod';
const result = await generateText({ model: "xai/grok-4.5", tools: { shell: tool({ inputSchema: z.object({ command: z.string(), workingDirectory: z.string().optional(), }), execute: async ( { command, workingDirectory }, { abortSignal, experimental_sandbox }, ) => { if (!experimental_sandbox) { throw new Error('Experimental sandbox is not available'); }
return experimental_sandbox.run({ command, workingDirectory, abortSignal, }); }, }), }, experimental_sandbox, prompt: 'List the files in the project.',});The experimental sandbox description is not added to the model prompt automatically. If the model should know about details such as the root directory, exposed ports, or public hostname, include experimental_sandbox.description in your system prompt,
instructions, user-visible context, or a tool description function.
run also accepts an optional workingDirectory, env, and abortSignal.
Use workingDirectory when a tool needs to run a command from a directory other
than the experimental sandbox implementation's default working directory. Use env
to set environment variables for the command. Forward
abortSignal from the tool execution options so the experimental sandbox can cancel the command if the overall operation is aborted or times out.
The AI SDK forwards your experimental sandbox object but does not create or isolate one for you.
The Experimental_SandboxSession interface is only a contract. Its isolation guarantees depend on your implementation or experimental sandbox provider. A local implementation that uses
child_process.exec with a working directory is not a security boundary because
commands can still access paths outside that directory. For untrusted commands
or user-directed coding agents, use a real isolation provider, restrict
available commands, set command timeouts, and combine experimental sandbox usage with tool approval for sensitive actions.
Runtime Context
You can pass in arbitrary runtime context from generateText or streamText via the runtimeContext setting.
This runtime context is available in prepareStep.
To avoid confusion with prompt context or retrieved context, the docs refer to this feature as runtime context.
This is useful for values like tenant information, feature flags, session data, or other server-side state that should influence step preparation without being embedded into the prompt.
Tool execution context is now separate. If a tool needs server-side values such as API keys, pass them via toolsContext, keyed by tool name. Each tool then receives only its own typed context value based on its contextSchema.
For the full mental model, examples, lifecycle details, and guidance on choosing between prompt context, runtime context, and tool context, see Runtime and Tool Context.
Tool Context Telemetry
Tool context often contains server-side values such as API keys, access tokens, or internal identifiers.
Use telemetry.includeToolsContext to include selected top-level context properties in telemetry integrations:
const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string(), }), contextSchema: z.object({ weatherApiKey: z.string(), defaultUnit: z.enum(['celsius', 'fahrenheit']), }), execute: async ({ location }, { context }) => { return fetchWeather({ location, apiKey: context.weatherApiKey, unit: context.defaultUnit, }); },});
const result = await generateText({ model: "xai/grok-4.5", tools: { weather: weatherTool }, toolsContext: { weather: { weatherApiKey: process.env.WEATHER_API_KEY, defaultUnit: 'fahrenheit', }, }, prompt: 'What is the weather in San Francisco?', telemetry: { includeToolsContext: { weather: { defaultUnit: true, }, }, },});Telemetry integrations receive the weather tool context as { defaultUnit: 'fahrenheit' }.
Properties set to false or omitted are excluded. If telemetry.includeToolsContext is omitted, no tool context properties are included.
telemetry.includeToolsContext only filters telemetry integrations. Tool
execution, lifecycle callbacks, and returned results still receive the full
typed tool context. See Runtime and Tool
Context for how tool context
flows through execution and telemetry.
Tool Input Lifecycle Hooks
The following tool input lifecycle hooks are available:
onInputStart: Called when the model starts generating the input (arguments) for the tool callonInputDelta: Called for each chunk of text as the input is streamedonInputAvailable: Called when the complete input is available and validated
onInputStart and onInputDelta are only called in streaming contexts (when using streamText). They are not called when using generateText.
Example
import { streamText, tool } from 'ai';import { z } from 'zod';
const result = streamText({ model: "xai/grok-4.5", tools: { getWeather: tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ temperature: 72 + Math.floor(Math.random() * 21) - 10, }), onInputStart: () => { console.log('Tool call starting'); }, onInputDelta: ({ inputTextDelta }) => { console.log('Received input chunk:', inputTextDelta); }, onInputAvailable: ({ input }) => { console.log('Complete input:', input); }, }), }, prompt: 'What is the weather in San Francisco?',});Types
Modularizing your code often requires defining types to ensure type safety and reusability. To enable this, the AI SDK provides several helper types for tools, tool calls, and tool results.
You can use them to strongly type your variables, function parameters, and return types
in parts of the code that are not directly related to streamText or generateText.
Each tool call is typed with ToolCall<NAME extends string, ARGS>, depending
on the tool that has been invoked.
Similarly, the tool results are typed with ToolResult<NAME extends string, ARGS, RESULT>.
The tools in streamText and generateText are defined as a ToolSet.
The type inference helpers TypedToolCall<TOOLS extends ToolSet>
and TypedToolResult<TOOLS extends ToolSet> can be used to
extract the tool call and tool result types from the tools.
import { TypedToolCall, TypedToolResult, generateText, tool } from 'ai';import { z } from 'zod';
const myToolSet = { firstTool: tool({ description: 'Greets the user', inputSchema: z.object({ name: z.string() }), execute: async ({ name }) => `Hello, ${name}!`, }), secondTool: tool({ description: 'Tells the user their age', inputSchema: z.object({ age: z.number() }), execute: async ({ age }) => `You are ${age} years old!`, }),};
type MyToolCall = TypedToolCall<typeof myToolSet>;type MyToolResult = TypedToolResult<typeof myToolSet>;
async function generateSomething(prompt: string): Promise<{ text: string; toolCalls: Array<MyToolCall>; // typed tool calls toolResults: Array<MyToolResult>; // typed tool results}> { return generateText({ model: "xai/grok-4.5", tools: myToolSet, prompt, });}Handling Errors
The AI SDK has three tool-call related errors:
NoSuchToolError: the model tries to call a tool that is not defined in the tools objectInvalidToolInputError: the model calls a tool with inputs that do not match the tool's input schemaToolCallRepairError: an error that occurred during tool call repair
When tool execution fails (errors thrown by your tool's execute function), the AI SDK adds them as tool-error content parts to enable automated LLM roundtrips in multi-step scenarios.
generateText
generateText throws errors for tool schema validation issues and other errors, and can be handled using a try/catch block. Tool execution errors appear as tool-error parts in the result steps:
try { const result = await generateText({ //... });} catch (error) { if (NoSuchToolError.isInstance(error)) { // handle the no such tool error } else if (InvalidToolInputError.isInstance(error)) { // handle the invalid tool inputs error } else { // handle other errors }}Tool execution errors are available in the result steps:
const { steps } = await generateText({ // ...});
// check for tool errors in the stepsconst 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);});streamText
streamText sends errors as part of the stream result. Tool execution errors appear as tool-error parts, while other errors appear as error parts.
When using toUIMessageStream, you can pass an onError function to extract the error message from the error part and forward it as part of the stream response:
const result = streamText({ // ...});
return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream, onError: error => { if (NoSuchToolError.isInstance(error)) { return 'The model tried to call a unknown tool.'; } else if (InvalidToolInputError.isInstance(error)) { return 'The model called a tool with invalid inputs.'; } else { return 'An unknown error occurred.'; } }, }),});Tool Call Repair
Language models sometimes fail to generate valid tool calls, especially when the input schema is complex or the model is smaller.
If you use multiple steps, those failed tool calls will be sent back to the LLM in the next step to give it an opportunity to fix it. However, you may want to control how invalid tool calls are repaired without requiring additional steps that pollute the message history.
You can use the repairToolCall function to attempt to repair the tool call
with a custom function.
You can use different strategies to repair the tool call:
- Use a model with structured outputs to generate the inputs.
- Send the messages, instructions, and tool schema to a stronger model to generate the inputs.
- Provide more specific repair instructions based on which tool was called.
Example: Use a model with structured outputs for repair
import { openai } from '@ai-sdk/openai';import { generateText, NoSuchToolError, Output, tool } from 'ai';
const result = await generateText({ model, tools, prompt,
repairToolCall: async ({ toolCall, tools, inputSchema, error }) => { if (NoSuchToolError.isInstance(error)) { return null; // do not attempt to fix invalid tool names }
const tool = tools[toolCall.toolName as keyof typeof tools];
const { output: repairedArgs } = await generateText({ model: "xai/grok-4.5", output: Output.object({ schema: tool.inputSchema }), prompt: [ `The model tried to call the tool "${toolCall.toolName}"` + ` with the following inputs:`, JSON.stringify(toolCall.input), `The tool accepts the following schema:`, JSON.stringify(await inputSchema({ toolName: toolCall.toolName })), 'Please fix the inputs.', ].join('\n'), });
return { ...toolCall, input: JSON.stringify(repairedArgs) }; },});Example: Use the re-ask strategy for repair
import { openai } from '@ai-sdk/openai';import { generateText, NoSuchToolError, tool } from 'ai';
const result = await generateText({ model, tools, prompt,
repairToolCall: async ({ toolCall, tools, error, messages, instructions, }) => { const result = await generateText({ model, instructions, messages: [ ...messages, { role: 'assistant', content: [ { type: 'tool-call', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: toolCall.input, }, ], }, { role: 'tool' as const, content: [ { type: 'tool-result', toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, output: error.message, }, ], }, ], tools, });
const newToolCall = result.toolCalls.find( newToolCall => newToolCall.toolName === toolCall.toolName, );
return newToolCall != null ? { type: 'tool-call' as const, toolCallId: toolCall.toolCallId, toolName: toolCall.toolName, input: JSON.stringify(newToolCall.input), } : null; },});Active Tools
Language models can only handle a limited number of tools at a time, depending on the model.
To allow for static typing using a large number of tools and limiting the available tools to the model at the same time,
the AI SDK provides the activeTools property.
It is an array of tool names that are currently active.
By default, the value is undefined and all tools are active.
import { openai } from '@ai-sdk/openai';import { generateText } from 'ai';
const { text } = await generateText({ model: "xai/grok-4.5", tools: myToolSet, activeTools: ['firstTool'],});Tool Order
Some providers include tool definitions in the cached portion of a request. If the order of those definitions changes between otherwise similar requests, the provider may not be able to reuse the cached prefix as effectively.
Use toolOrder when you want a stable provider request shape for caching or
debugging. The list can be partial: tools listed in toolOrder are sent first
in that order, and any remaining tools are sent afterwards in alphabetical
order. Tool names are typed from your tools object.
import { generateText } from 'ai';
const { text } = await generateText({ model: "xai/grok-4.5", tools: myToolSet, toolOrder: ['search', 'readFile'], prompt: 'Summarize the latest project notes.',});toolOrder controls only the order of tool definitions sent to the provider.
It does not force the model to call a tool. Use toolChoice to
control tool selection, and use activeTools when you want to
limit which tools are available. When both activeTools and toolOrder are
provided, activeTools filters the available tools first and toolOrder
orders the remaining tools.
Multi-modal Tool Results
Multi-modal tool results are experimental and supported by Anthropic, OpenAI, and Google (Gemini 3 models).
For Google, use base64 inline-data file parts
({ type: 'file', mediaType, data: { type: 'data', data } }) or base64
data: URLs in URL-style file parts
({ type: 'file', mediaType, data: { type: 'url', url: new URL('data:...') } }).
Remote HTTP(S) URLs in tool-result URL parts are not supported.
In order to send multi-modal tool results, e.g. screenshots, back to the model, they need to be converted into a specific format.
AI SDK Core tools have an optional toModelOutput function
that converts the tool result into a content part.
Here is an example for converting a screenshot into a content part:
const result = await generateText({ model: "xai/grok-4.5", tools: { computer: anthropic.tools.computer_20241022({ // ... async execute({ action, coordinate, text }) { switch (action) { case 'screenshot': { return { type: 'file', mediaType: 'image', data: fs .readFileSync('./data/screenshot-editor.png') .toString('base64'), }; } default: { return `executed ${action}`; } } },
// map to tool result content for LLM consumption: toModelOutput({ output }) { return { type: 'content', value: typeof output === 'string' ? [{ type: 'text', text: output }] : [ { type: 'file', mediaType: 'image/png', data: { type: 'data', data: output.data }, }, ], }; }, }), }, // ...});Extracting Tools
Once you start having many tools, you might want to extract them into separate files.
The tool helper function is crucial for this, because it ensures correct type inference.
Here is an example of an extracted tool:
import { tool } from 'ai';import { z } from 'zod';
// the `tool` helper function ensures correct type inference:export const weatherTool = tool({ description: 'Get the weather in a location', inputSchema: z.object({ location: z.string().describe('The location to get the weather for'), }), execute: async ({ location }) => ({ location, temperature: 72 + Math.floor(Math.random() * 21) - 10, }),});MCP Tools
The AI SDK supports connecting to Model Context Protocol (MCP) servers to access their tools. MCP enables your AI applications to discover and use tools across various services through a standardized interface.
For detailed information about MCP tools, including initialization, transport options, and usage patterns, see the MCP Tools documentation.
AI SDK Tools vs MCP Tools
In most cases, you should define your own AI SDK tools for production applications. They provide full control, type safety, and optimal performance. MCP tools are best suited for rapid development iteration and scenarios where users bring their own tools.
| Aspect | AI SDK Tools | MCP Tools |
|---|---|---|
| Type Safety | Full static typing end-to-end | Dynamic discovery at runtime |
| Execution | Same process as your request (low latency) | Separate server (network overhead) |
| Prompt Control | Full control over descriptions and schemas | Controlled by MCP server owner |
| Schema Control | You define and optimize for your model | Controlled by MCP server owner |
| Version Management | Full visibility over updates | Can update independently (version skew risk) |
| Authentication | Same process, no additional auth required | Separate server introduces additional auth complexity |
| Best For | Production applications requiring control and performance | Development iteration, user-provided tools |
Examples
You can see tools in action using various frameworks in the following examples: