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,323 lines (1,208 loc) • 64.9 kB
text/typescript
import type {
LanguageModelV4Content,
LanguageModelV4GenerateResult,
LanguageModelV4ToolCall,
} from '@ai-sdk/provider';
import {
asArray,
createIdGenerator,
getErrorMessage,
withUserAgentSuffix,
type Arrayable,
type Context,
type Experimental_SandboxSession as SandboxSession,
type IdGenerator,
type InferToolSetContext,
type ProviderOptions,
type ToolSet,
} from '@ai-sdk/provider-utils';
import { NoOutputGeneratedError } from '../error';
import { ToolCallNotFoundForApprovalError } from '../error/tool-call-not-found-for-approval-error';
import { logWarnings } from '../logger/log-warnings';
import { resolveLanguageModel } from '../model/resolve-model';
import type { ModelMessage } from '../prompt';
import { cloneModelMessages } from '../prompt/clone-model-message';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { createToolModelOutput } from '../prompt/create-tool-model-output';
import type { LanguageModelCallOptions } from '../prompt/language-model-call-options';
import { prepareLanguageModelCallOptions } from '../prompt/prepare-language-model-call-options';
import { prepareToolChoice } from '../prompt/prepare-tool-choice';
import { prepareTools } from '../prompt/prepare-tools';
import type { Prompt } from '../prompt/prompt';
import {
getStepTimeoutMs,
getTotalTimeoutMs,
type RequestOptions,
type TimeoutConfiguration,
} from '../prompt/request-options';
import { standardizePrompt } from '../prompt/standardize-prompt';
import { wrapGatewayError } from '../prompt/wrap-gateway-error';
import type { Telemetry, TelemetryDispatcher } from '../telemetry/telemetry';
import type { TelemetryOptions } from '../telemetry/telemetry-options';
import type {
LanguageModel,
LanguageModelRequestMetadata,
ToolChoice,
} from '../types';
import {
addLanguageModelUsage,
asLanguageModelUsage,
type LanguageModelUsage,
} from '../types/usage';
import type { DownloadFunction } from '../util/download/download-function';
import { getOwn } from '../util/get-own';
import { mergeAbortSignals } from '../util/merge-abort-signals';
import { mergeObjects } from '../util/merge-objects';
import { now as originalNow } from '../util/now';
import { notify } from '../util/notify';
import { prepareRetries } from '../util/prepare-retries';
import { setAbortTimeout } from '../util/set-abort-timeout';
import { VERSION } from '../version';
import type { ActiveTools } from './active-tools';
import { calculateTokensPerSecond } from './calculate-tokens-per-second';
import { collectToolApprovals } from './collect-tool-approvals';
import type { ContentPart } from './content-part';
import { executeToolCall } from './execute-tool-call';
import {
filterActiveTools,
type ActiveToolSubset,
} from './filter-active-tools';
import type {
GenerateTextOnEndCallback,
GenerateTextOnStartCallback,
GenerateTextOnStepEndCallback,
GenerateTextOnStepFinishCallback,
GenerateTextOnStepStartCallback,
} from './generate-text-events';
import type { GenerateTextResult } from './generate-text-result';
import { DefaultGeneratedFile } from './generated-file';
import type {
OnLanguageModelCallEndCallback,
OnLanguageModelCallStartCallback,
} from './language-model-events';
import { text, type Output } from './output';
import type { InferCompleteOutput } from './output-utils';
import { parseToolCall } from './parse-tool-call';
import type { PrepareStepFunction } from './prepare-step';
import { convertToReasoningOutputs } from './reasoning-output';
import { resolveToolApproval } from './resolve-tool-approval';
import type { ResponseMessage } from './response-message';
import { createRestrictedTelemetryDispatcher } from './restricted-telemetry-dispatcher';
import {
DefaultStepResult,
type StepResult,
type StepResultPerformance,
} from './step-result';
import {
isStepCount,
isStopConditionMet,
type StopCondition,
} from './stop-condition';
import { sumTokenCounts } from './sum-token-counts';
import { toResponseMessages } from './to-response-messages';
import type { ToolApprovalConfiguration } from './tool-approval-configuration';
import type { ToolApprovalRequestOutput } from './tool-approval-request-output';
import type { ToolApprovalResponseOutput } from './tool-approval-response-output';
import type { TypedToolCall } from './tool-call';
import type { ToolCallRepairFunction } from './tool-call-repair-function';
import type { TypedToolError } from './tool-error';
import type {
OnToolExecutionEndCallback,
OnToolExecutionStartCallback,
} from './tool-execution-events';
import type { ToolInputRefinement } from './tool-input-refinement';
import type { ToolOrder } from './tool-order';
import type { ToolOutput } from './tool-output';
import type { TypedToolResult } from './tool-result';
import type { ToolsContextParameter } from './tools-context-parameter';
import { maybeSignApproval } from './tool-approval-signature';
import { validateApprovedToolApprovals } from './validate-tool-approvals';
const originalGenerateId = createIdGenerator({
prefix: 'aitxt',
size: 24,
});
const originalGenerateCallId = createIdGenerator({
prefix: 'call',
size: 24,
});
export type GenerateTextInclude = {
/**
* Whether to retain the request body in step results.
* The request body can be large when sending images or files.
*
* @default false
*/
requestBody?: boolean;
/**
* Whether to retain the request messages in step results.
* The request messages can be large when sending images or files.
*
* @default false
*/
requestMessages?: boolean;
/**
* Whether to retain the response body in step results.
*
* @default false
*/
responseBody?: boolean;
};
/**
* Generate a text and call tools for a given prompt using a language model.
*
* This function does not stream the output. If you want to stream the output, use `streamText` instead.
*
* @param model - The language model to use.
*
* @param tools - Tools that are accessible to and can be called by the model. The model needs to support calling tools.
* @param toolChoice - The tool choice strategy. Default: 'auto'.
* @param toolOrder - Controls the order in which tools are sent to the provider. Tools not listed are appended alphabetically.
*
* @param system - A system message that will be part of the prompt.
* @param prompt - A simple text prompt. You can either use `prompt` or `messages` but not both.
* @param messages - A list of messages. You can either use `prompt` or `messages` but not both.
* @param allowSystemInMessages - Whether system messages are allowed in the `prompt` or `messages` fields. Default: false.
*
* @param maxOutputTokens - Maximum number of tokens to generate.
* @param temperature - Temperature setting.
* The value is passed through to the provider. The range depends on the provider and model.
* It is recommended to set either `temperature` or `topP`, but not both.
* @param topP - Nucleus sampling.
* The value is passed through to the provider. The range depends on the provider and model.
* It is recommended to set either `temperature` or `topP`, but not both.
* @param topK - Only sample from the top K options for each subsequent token.
* Used to remove "long tail" low probability responses.
* Recommended for advanced use cases only. You usually only need to use temperature.
* @param presencePenalty - Presence penalty setting.
* It affects the likelihood of the model to repeat information that is already in the prompt.
* The value is passed through to the provider. The range depends on the provider and model.
* @param frequencyPenalty - Frequency penalty setting.
* It affects the likelihood of the model to repeatedly use the same words or phrases.
* The value is passed through to the provider. The range depends on the provider and model.
* @param stopSequences - Stop sequences.
* If set, the model will stop generating text when one of the stop sequences is generated.
* @param seed - The seed (integer) to use for random sampling.
* If set and supported by the model, calls will generate deterministic results.
*
* @param maxRetries - Maximum number of retries. Set to 0 to disable retries. Default: 2.
* @param abortSignal - An optional abort signal that can be used to cancel the call.
* @param timeout - An optional timeout in milliseconds. The call will be aborted if it takes longer than the specified timeout.
* @param headers - Additional HTTP headers to be sent with the request. Only applicable for HTTP-based providers.
*
* @param experimental_sandbox - The sandbox environment that is passed through to tool execution.
* @param runtimeContext - User-defined runtime context that flows through the entire generation lifecycle.
* @param experimental_refineToolInput - Optional mapping of tool names to functions that refine parsed tool inputs before tools are executed and before outputs, callbacks, and telemetry are recorded.
* @param onStart - Callback invoked when generation begins, before any LLM calls.
* @param experimental_onStart - Deprecated alias for `onStart`.
* @param onStepStart - Callback invoked when each step begins, before the provider is called.
* @param experimental_onStepStart - Deprecated alias for `onStepStart`.
* Receives step number, messages (in ModelMessage format), tools, and runtimeContext.
* @param onLanguageModelCallStart - Callback invoked immediately before each provider model call begins.
* @param experimental_onLanguageModelCallStart - Deprecated alias for `onLanguageModelCallStart`.
* @param onLanguageModelCallEnd - Callback invoked after each provider model call response is normalized and parsed.
* @param experimental_onLanguageModelCallEnd - Deprecated alias for `onLanguageModelCallEnd`.
* @param onToolExecutionStart - Callback invoked before each tool execution begins.
* Receives tool name, call ID, input, and context.
* @param experimental_onToolCallStart - Deprecated alias for `onToolExecutionStart`.
* @param onToolExecutionEnd - Callback invoked after each tool execution completes.
* Uses a discriminated union: check `success` to determine if `output` or `error` is present.
* @param experimental_onToolCallFinish - Deprecated alias for `onToolExecutionEnd`.
* @param onStepFinish - Callback that is called when each step (LLM call) is finished, including intermediate steps.
* @param onEnd - Callback that is called when all steps are finished and the response is complete.
* @param onFinish - Deprecated alias for `onEnd`.
*
* @returns
* A result object that contains the generated text, the results of the tool calls, and additional information.
*/
export async function generateText<
TOOLS extends ToolSet,
RUNTIME_CONTEXT extends Context = Context,
OUTPUT extends Output = Output<string, string>,
>({
model: modelArg,
tools,
toolChoice,
instructions,
system,
prompt,
messages,
allowSystemInMessages,
maxRetries: maxRetriesArg,
abortSignal,
timeout,
headers,
stopWhen = isStepCount(1),
experimental_sandbox: sandbox,
output,
toolApproval,
experimental_toolApprovalSecret,
experimental_telemetry,
telemetry = experimental_telemetry,
providerOptions,
activeTools,
toolOrder,
prepareStep,
experimental_repairToolCall,
repairToolCall = experimental_repairToolCall,
experimental_refineToolInput: refineToolInput,
experimental_download: download,
runtimeContext = {} as RUNTIME_CONTEXT,
toolsContext = {} as InferToolSetContext<TOOLS>,
experimental_include,
include = experimental_include,
_internal: {
generateId = originalGenerateId,
generateCallId = originalGenerateCallId,
now = originalNow,
} = {},
onStart,
experimental_onStart,
onStepStart,
experimental_onStepStart,
onLanguageModelCallStart,
experimental_onLanguageModelCallStart,
onLanguageModelCallEnd,
experimental_onLanguageModelCallEnd,
onToolExecutionStart,
onToolExecutionEnd,
experimental_onToolCallStart,
experimental_onToolCallFinish,
onStepEnd,
onStepFinish,
onFinish,
onEnd = onFinish,
...settings
}: LanguageModelCallOptions &
RequestOptions<TOOLS> &
Prompt &
ToolsContextParameter<TOOLS> & {
/**
* The language model to use.
*/
model: LanguageModel;
/**
* The tool choice strategy. Default: 'auto'.
*/
toolChoice?: ToolChoice<NoInfer<TOOLS>>;
/**
* Condition for stopping the generation when there are tool results in the last step.
* When the condition is an array, any of the conditions can be met to stop the generation.
*
* @default isStepCount(1)
*/
stopWhen?: Arrayable<StopCondition<NoInfer<TOOLS>, RUNTIME_CONTEXT>>;
/**
* Optional telemetry configuration.
*/
telemetry?: TelemetryOptions<RUNTIME_CONTEXT, NoInfer<TOOLS>>;
/**
* Optional telemetry configuration.
*
* @deprecated Use `telemetry` instead. This alias will be removed in a future major release.
*/
experimental_telemetry?: TelemetryOptions<RUNTIME_CONTEXT, NoInfer<TOOLS>>;
/**
* Additional provider-specific options. They are passed through
* to the provider from the AI SDK and enable provider-specific
* functionality that can be fully encapsulated in the provider.
*/
providerOptions?: ProviderOptions;
/**
* The sandbox environment that is passed through to tool execution.
*/
experimental_sandbox?: SandboxSession;
/**
* Runtime context. Treat runtime context as immutable.
* If you need to mutate runtime context, update it in `prepareStep`.
*/
runtimeContext?: RUNTIME_CONTEXT;
/**
* Limits the tools that are available for the model to call without
* changing the tool call and result types in the result.
*/
activeTools?: ActiveTools<NoInfer<TOOLS>>;
/**
* Controls the order in which tools are sent to the provider.
*
* The list can be partial. Tools not listed in `toolOrder` are sent after
* the listed tools, sorted alphabetically. This can improve provider-side
* caching by keeping tool definitions in a stable order.
*/
toolOrder?: ToolOrder<NoInfer<TOOLS>>;
/**
* Optional specification for parsing structured outputs from the LLM response.
*/
output?: OUTPUT;
/**
* Optional tool approval configuration.
*
* This configuration takes precedence over tool-defined approval settings.
*/
toolApproval?: ToolApprovalConfiguration<TOOLS, RUNTIME_CONTEXT>;
/**
* Secret for HMAC-signing tool approval requests. When set, the server
* signs each approval request at issuance and verifies the signature when
* the approval is replayed, preventing client-forged approvals.
*/
experimental_toolApprovalSecret?: string | Uint8Array;
/**
* Custom download function to use for URLs.
*
* By default, files are downloaded if the model does not support the URL for the given media type.
*/
experimental_download?: DownloadFunction | undefined;
/**
* Optional function that you can use to provide different settings for a step.
*/
prepareStep?: PrepareStepFunction<NoInfer<TOOLS>, RUNTIME_CONTEXT>;
/**
* A function that attempts to repair a tool call that failed to parse.
*/
repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>;
/**
* A function that attempts to repair a tool call that failed to parse.
*
* @deprecated Use `repairToolCall` instead.
*/
experimental_repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>;
/**
* Optional mapping of tool names to functions that refine parsed tool inputs.
*
* The refined input must have the same type shape as the tool input. Refined
* inputs are used for tool execution, outputs, callbacks, and telemetry.
*/
experimental_refineToolInput?: ToolInputRefinement<NoInfer<TOOLS>>;
/**
* Callback that is called when the generateText operation begins,
* before any LLM calls are made.
*/
onStart?: GenerateTextOnStartCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>,
NoInfer<OUTPUT>
>;
/**
* Callback that is called when the generateText operation begins,
* before any LLM calls are made.
*
* @deprecated Use `onStart` instead.
*/
experimental_onStart?: GenerateTextOnStartCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>,
NoInfer<OUTPUT>
>;
/**
* Callback that is called when a step (LLM call) begins,
* before the provider is called.
*/
onStepStart?: GenerateTextOnStepStartCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>,
NoInfer<OUTPUT>
>;
/**
* Callback that is called when a step (LLM call) begins,
* before the provider is called.
*
* @deprecated Use `onStepStart` instead.
*/
experimental_onStepStart?: GenerateTextOnStepStartCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>,
NoInfer<OUTPUT>
>;
/**
* Callback that is called immediately before the provider model call begins.
*/
onLanguageModelCallStart?: OnLanguageModelCallStartCallback;
/**
* Callback that is called immediately before the provider model call begins.
*
* @deprecated Use `onLanguageModelCallStart` instead.
*/
experimental_onLanguageModelCallStart?: OnLanguageModelCallStartCallback;
/**
* Callback that is called after the model response has been normalized and parsed,
* but before any client-side tool execution begins.
*/
onLanguageModelCallEnd?: OnLanguageModelCallEndCallback<NoInfer<TOOLS>>;
/**
* Callback that is called after the model response has been normalized and parsed,
* but before any client-side tool execution begins.
*
* @deprecated Use `onLanguageModelCallEnd` instead.
*/
experimental_onLanguageModelCallEnd?: OnLanguageModelCallEndCallback<
NoInfer<TOOLS>
>;
/**
* Callback that is called right before a tool's execute function runs.
*/
onToolExecutionStart?: OnToolExecutionStartCallback<NoInfer<TOOLS>>;
/**
* Callback that is called right before a tool's execute function runs.
*
* @deprecated Use `onToolExecutionStart` instead.
*/
experimental_onToolCallStart?: OnToolExecutionStartCallback<NoInfer<TOOLS>>;
/**
* Callback that is called right after a tool's execute function completes (or errors).
*/
onToolExecutionEnd?: OnToolExecutionEndCallback<NoInfer<TOOLS>>;
/**
* Callback that is called right after a tool's execute function completes (or errors).
*
* @deprecated Use `onToolExecutionEnd` instead.
*/
experimental_onToolCallFinish?: OnToolExecutionEndCallback<NoInfer<TOOLS>>;
/**
* Callback that is called when each step (LLM call) ends, including intermediate steps.
*/
onStepEnd?: GenerateTextOnStepEndCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>
>;
/**
* Callback that is called when each step (LLM call) ends, including intermediate steps.
*
* @deprecated Use `onStepEnd` instead.
*/
onStepFinish?: GenerateTextOnStepFinishCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>
>;
/**
* Callback that is called when all steps are finished and the response is complete.
*/
onEnd?: GenerateTextOnEndCallback<NoInfer<TOOLS>, NoInfer<RUNTIME_CONTEXT>>;
/**
* Callback that is called when all steps are finished and the response is complete.
*
* @deprecated Use `onEnd` instead.
*/
onFinish?: GenerateTextOnEndCallback<
NoInfer<TOOLS>,
NoInfer<RUNTIME_CONTEXT>
>;
/**
* Settings for controlling what data is included in step results.
* Disabling inclusion can help reduce memory usage when processing
* large payloads like images.
*
* By default, request bodies, request messages, and response bodies are
* excluded.
*/
include?: GenerateTextInclude;
/**
* Settings for controlling what data is included in step results.
*
* @deprecated Use `include` instead.
*/
experimental_include?: GenerateTextInclude;
/**
* Internal. For test use only. May change without notice.
*/
_internal?: {
generateId?: IdGenerator;
generateCallId?: IdGenerator;
now?: () => number;
};
}): Promise<GenerateTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>> {
// assign default values to include:
include = {
requestBody: include?.requestBody ?? false,
requestMessages: include?.requestMessages ?? false,
responseBody: include?.responseBody ?? false,
};
const model = resolveLanguageModel(modelArg);
const stopConditions = asArray(stopWhen);
const resolvedOnStart = onStart ?? experimental_onStart;
const resolvedOnStepStart = onStepStart ?? experimental_onStepStart;
const resolvedOnLanguageModelCallStart =
onLanguageModelCallStart ?? experimental_onLanguageModelCallStart;
const resolvedOnLanguageModelCallEnd =
onLanguageModelCallEnd ?? experimental_onLanguageModelCallEnd;
const resolvedOnToolExecutionStart =
onToolExecutionStart ?? experimental_onToolCallStart;
const resolvedOnToolExecutionEnd =
onToolExecutionEnd ?? experimental_onToolCallFinish;
const resolvedOnStepEnd = onStepEnd ?? onStepFinish;
const totalTimeoutMs = getTotalTimeoutMs(timeout);
const stepTimeoutMs = getStepTimeoutMs(timeout);
const stepAbortController =
stepTimeoutMs != null ? new AbortController() : undefined;
const mergedAbortSignal = mergeAbortSignals(
abortSignal,
totalTimeoutMs,
stepAbortController?.signal,
);
const { maxRetries, retry } = prepareRetries({
maxRetries: maxRetriesArg,
abortSignal: mergedAbortSignal,
});
const callSettings = prepareLanguageModelCallOptions(settings);
const headersWithUserAgent = withUserAgentSuffix(
headers ?? {},
`ai/${VERSION}`,
);
const initialPrompt = await standardizePrompt({
instructions,
system,
prompt,
messages,
allowSystemInMessages,
} as Prompt);
const callId = generateCallId();
const telemetryDispatcher = createRestrictedTelemetryDispatcher<
TOOLS,
RUNTIME_CONTEXT,
OUTPUT
>({
telemetry,
includeRuntimeContext: telemetry?.includeRuntimeContext,
includeToolsContext: telemetry?.includeToolsContext,
});
const runInTracingChannelSpan =
telemetryDispatcher.runInTracingChannelSpan ??
(async <T>({ execute }: { execute: () => PromiseLike<T> }) =>
await execute());
const generateTextStartEvent = {
callId,
operationId: 'ai.generateText',
provider: model.provider,
modelId: model.modelId,
instructions: initialPrompt.instructions,
messages: initialPrompt.messages,
tools,
toolChoice,
activeTools,
toolOrder,
maxOutputTokens: callSettings.maxOutputTokens,
temperature: callSettings.temperature,
topP: callSettings.topP,
topK: callSettings.topK,
presencePenalty: callSettings.presencePenalty,
frequencyPenalty: callSettings.frequencyPenalty,
stopSequences: callSettings.stopSequences,
seed: callSettings.seed,
reasoning: callSettings.reasoning,
maxRetries,
timeout,
headers: headersWithUserAgent,
providerOptions,
output,
runtimeContext,
toolsContext,
};
const executeGenerateText = async () => {
await notify({
event: generateTextStartEvent,
callbacks: [resolvedOnStart, telemetryDispatcher.onStart],
});
try {
const initialMessages = initialPrompt.messages;
const initialResponseMessages: Array<ResponseMessage> = [];
const {
approvedToolApprovals,
deniedToolApprovals: collectedDeniedToolApprovals,
} = collectToolApprovals<TOOLS>({ messages: initialMessages });
const {
approvedToolApprovals: localApprovedToolApprovals,
deniedToolApprovals: revalidationDeniedToolApprovals,
} = await validateApprovedToolApprovals<TOOLS, RUNTIME_CONTEXT>({
approvedToolApprovals: approvedToolApprovals.filter(
toolApproval => !toolApproval.toolCall.providerExecuted,
),
tools,
toolApproval,
messages: initialMessages,
toolsContext,
runtimeContext,
toolApprovalSecret: experimental_toolApprovalSecret,
});
const deniedToolApprovals = [
...collectedDeniedToolApprovals,
...revalidationDeniedToolApprovals,
];
if (
deniedToolApprovals.length > 0 ||
localApprovedToolApprovals.length > 0
) {
const toolResults = await executeTools({
toolCalls: localApprovedToolApprovals.map(
toolApproval => toolApproval.toolCall,
),
tools: tools as TOOLS,
callId,
messages: initialMessages,
abortSignal: mergedAbortSignal,
timeout,
experimental_sandbox: sandbox,
toolsContext,
onToolExecutionStart: event =>
notify({
event,
callbacks: [
resolvedOnToolExecutionStart,
telemetryDispatcher.onToolExecutionStart,
],
}),
onToolExecutionEnd: event =>
notify({
event,
callbacks: [
resolvedOnToolExecutionEnd,
telemetryDispatcher.onToolExecutionEnd,
],
}),
executeToolInTelemetryContext: telemetryDispatcher.executeTool,
runInTracingChannelSpan,
});
const toolContent: Array<any> = [];
// add regular tool results for approved tool calls:
for (const result of toolResults) {
const output = result.output;
const modelOutput = await createToolModelOutput({
toolCallId: output.toolCallId,
input: output.input,
tool: getOwn(tools, output.toolName),
output:
output.type === 'tool-result' ? output.output : output.error,
errorMode: output.type === 'tool-error' ? 'text' : 'none',
});
toolContent.push({
type: 'tool-result' as const,
toolCallId: output.toolCallId,
toolName: output.toolName,
output: modelOutput,
});
}
// add execution denied tool results for all denied tool approvals:
for (const toolApproval of deniedToolApprovals) {
toolContent.push({
type: 'tool-result' as const,
toolCallId: toolApproval.toolCall.toolCallId,
toolName: toolApproval.toolCall.toolName,
output: {
type: 'execution-denied' as const,
reason: toolApproval.approvalResponse.reason,
// For provider-executed tools, include approvalId so provider can correlate
...(toolApproval.toolCall.providerExecuted && {
providerOptions: {
openai: {
approvalId: toolApproval.approvalResponse.approvalId,
},
},
}),
},
});
}
initialResponseMessages.push({
role: 'tool',
content: toolContent,
});
}
const callSettings = prepareLanguageModelCallOptions(settings);
let currentModelResponse: LanguageModelV4GenerateResult & {
response: { id: string; timestamp: Date; modelId: string };
};
let clientToolCalls: Array<TypedToolCall<TOOLS>> = [];
let clientToolOutputs: Array<ToolOutput<TOOLS>> = [];
let toolApprovalResponses: Array<ToolApprovalResponseOutput<TOOLS>> = [];
let deniedToolApprovalResponses: Array<
ToolApprovalResponseOutput<TOOLS>
> = [];
const steps: GenerateTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>['steps'] =
[];
let instructionsForNextStep = initialPrompt.instructions;
let messagesForNextStep = [
...initialMessages,
...initialResponseMessages,
];
// Track provider-executed tool calls that support deferred results
// (e.g., code_execution in programmatic tool calling scenarios).
// These tools may not return their results in the same turn as their call.
const pendingDeferredToolCalls = new Map<string, { toolName: string }>();
do {
// Set up step timeout if configured
const stepTimeoutId = setAbortTimeout({
abortController: stepAbortController,
label: 'Step',
timeoutMs: stepTimeoutMs,
});
const stepNumber = steps.length;
try {
await runInTracingChannelSpan({
type: 'step',
event: { callId, stepNumber },
execute: async () => {
const accumulatedResponseMessages = [
...initialResponseMessages,
...steps.flatMap(step => step.response.messages),
];
const stepInputMessages = messagesForNextStep;
const prepareStepResult = await prepareStep?.({
model,
steps,
stepNumber: steps.length,
instructions: instructionsForNextStep,
initialInstructions: initialPrompt.instructions,
messages: stepInputMessages,
initialMessages,
responseMessages: accumulatedResponseMessages,
runtimeContext,
toolsContext,
experimental_sandbox: sandbox,
});
const stepSandbox =
prepareStepResult?.experimental_sandbox ?? sandbox;
const stepModel = resolveLanguageModel(
prepareStepResult?.model ?? model,
);
const stepInstructions =
prepareStepResult?.instructions ??
prepareStepResult?.system ??
instructionsForNextStep;
const promptMessages = await convertToLanguageModelPrompt({
prompt: {
instructions: stepInstructions,
messages: prepareStepResult?.messages ?? stepInputMessages,
},
supportedUrls: await stepModel.supportedUrls,
download,
provider: stepModel.provider.split('.')[0],
});
runtimeContext =
prepareStepResult?.runtimeContext ?? runtimeContext;
toolsContext = prepareStepResult?.toolsContext ?? toolsContext;
const stepActiveTools = filterActiveTools({
tools,
activeTools: prepareStepResult?.activeTools ?? activeTools,
});
const stepToolOrder = prepareStepResult?.toolOrder ?? toolOrder;
const stepTools = await prepareTools({
tools: stepActiveTools,
toolOrder: stepToolOrder as ToolOrder<
ActiveToolSubset<TOOLS, ActiveTools<NoInfer<TOOLS>>>
>,
// active tools context is a subset of the tools context, so we can cast to the unknown type
toolsContext: toolsContext as unknown as InferToolSetContext<
ActiveToolSubset<TOOLS, ActiveTools<NoInfer<TOOLS>>>
>,
experimental_sandbox: stepSandbox,
});
const stepToolChoice = prepareToolChoice({
toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
});
const stepMessages =
prepareStepResult?.messages ?? stepInputMessages;
const stepProviderOptions = mergeObjects(
providerOptions,
prepareStepResult?.providerOptions,
);
await notify({
event: {
callId,
provider: stepModel.provider,
modelId: stepModel.modelId,
stepNumber,
instructions: stepInstructions,
messages: stepMessages,
tools,
toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
activeTools: prepareStepResult?.activeTools ?? activeTools,
toolOrder: stepToolOrder,
steps: [...steps],
providerOptions: stepProviderOptions,
output,
runtimeContext,
promptMessages,
stepTools,
stepToolChoice,
toolsContext,
},
callbacks: [
resolvedOnStepStart,
telemetryDispatcher.onStepStart,
],
});
const languageModelCallContext = {
provider: stepModel.provider,
modelId: stepModel.modelId,
instructions: stepInstructions,
messages: stepMessages,
tools: stepTools,
...callSettings,
};
const languageModelCallStartEvent = {
callId,
...languageModelCallContext,
};
const stepStartTimestampMs = now();
await notify({
event: languageModelCallStartEvent,
callbacks: [
resolvedOnLanguageModelCallStart,
telemetryDispatcher.onLanguageModelCallStart as
| undefined
| OnLanguageModelCallStartCallback,
],
});
const executeLanguageModelCallInTelemetryContext =
telemetryDispatcher.executeLanguageModelCall ??
(async <T>({ execute }: { execute: () => PromiseLike<T> }) =>
await execute());
currentModelResponse = await retry(async () => {
const result = await executeLanguageModelCallInTelemetryContext(
{
...languageModelCallStartEvent,
execute: async () =>
await stepModel.doGenerate({
...callSettings,
tools: stepTools,
toolChoice: stepToolChoice,
responseFormat: await output?.responseFormat,
prompt: promptMessages,
providerOptions: stepProviderOptions,
abortSignal: mergedAbortSignal,
headers: headersWithUserAgent,
}),
},
);
const responseData = {
id: result.response?.id ?? generateId(),
timestamp: result.response?.timestamp ?? new Date(),
modelId: result.response?.modelId ?? stepModel.modelId,
headers: result.response?.headers,
body: result.response?.body,
};
return { ...result, response: responseData };
});
const responseTimeMs = now() - stepStartTimestampMs;
const stepUsage = asLanguageModelUsage(
currentModelResponse.usage,
);
// parse tool calls:
const stepToolCalls: TypedToolCall<TOOLS>[] = await Promise.all(
currentModelResponse.content
.filter(
(part): part is LanguageModelV4ToolCall =>
part.type === 'tool-call',
)
.map(toolCall =>
parseToolCall({
toolCall,
tools,
repairToolCall,
refineToolInput,
instructions: stepInstructions,
messages: stepMessages,
}),
),
);
const toolApprovalRequests: Record<
string,
ToolApprovalRequestOutput<TOOLS>
> = {};
const stepToolApprovalResponses: Record<
string,
ToolApprovalResponseOutput<TOOLS>
> = {};
const blockedToolCallIds = new Set<string>();
const modelCallContent = asContent({
content: currentModelResponse.content,
toolCalls: stepToolCalls,
toolOutputs: [],
toolApprovalRequests: [],
toolApprovalResponses: [],
tools,
});
await notify({
event: {
callId,
provider: stepModel.provider,
modelId: stepModel.modelId,
finishReason: currentModelResponse.finishReason.unified,
usage: stepUsage,
content: modelCallContent,
responseId: currentModelResponse.response.id,
performance: {
responseTimeMs,
effectiveOutputTokensPerSecond: calculateTokensPerSecond({
tokens: stepUsage.outputTokens,
durationMs: responseTimeMs,
}),
outputTokensPerSecond: undefined,
inputTokensPerSecond: undefined,
effectiveTotalTokensPerSecond: calculateTokensPerSecond({
tokens: sumTokenCounts(
stepUsage.inputTokens,
stepUsage.outputTokens,
),
durationMs: responseTimeMs,
}),
timeToFirstOutputMs: undefined,
},
},
callbacks: [
resolvedOnLanguageModelCallEnd,
telemetryDispatcher.onLanguageModelCallEnd as
| undefined
| OnLanguageModelCallEndCallback<TOOLS>,
],
});
// notify the tools that the tool calls are available:
for (const toolCall of stepToolCalls) {
if (toolCall.invalid) {
continue; // ignore invalid tool calls
}
const tool = getOwn(tools, toolCall.toolName);
if (tool == null) {
// ignore tool calls for tools that are not available,
// e.g. provider-executed dynamic tools
continue;
}
if (tool?.onInputAvailable != null) {
await tool.onInputAvailable({
input: toolCall.input,
toolCallId: toolCall.toolCallId,
messages: stepMessages,
abortSignal: mergedAbortSignal,
context: runtimeContext,
});
}
const toolApprovalStatus = await resolveToolApproval({
tools,
toolApproval,
toolCall,
messages: stepMessages,
toolsContext,
runtimeContext,
});
// Tools that don't require approval ('not-applicable') must not
// consume an approval id, so that id generation stays stable for
// callers that rely on deterministic id sequences.
if (toolApprovalStatus.type === 'not-applicable') {
continue;
}
const approvalId = generateId();
const signature = await maybeSignApproval({
secret: experimental_toolApprovalSecret,
approvalId,
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
});
switch (toolApprovalStatus.type) {
case 'user-approval': {
toolApprovalRequests[toolCall.toolCallId] = {
type: 'tool-approval-request',
approvalId,
toolCall,
...(signature != null ? { signature } : {}),
};
blockedToolCallIds.add(toolCall.toolCallId);
break;
}
case 'approved': {
toolApprovalRequests[toolCall.toolCallId] = {
type: 'tool-approval-request',
approvalId,
toolCall,
isAutomatic: true,
...(signature != null ? { signature } : {}),
};
stepToolApprovalResponses[toolCall.toolCallId] = {
type: 'tool-approval-response',
approvalId,
toolCall,
approved: true,
reason: toolApprovalStatus.reason,
providerExecuted: toolCall.providerExecuted,
};
break;
}
case 'denied': {
toolApprovalRequests[toolCall.toolCallId] = {
type: 'tool-approval-request',
approvalId,
toolCall,
isAutomatic: true,
...(signature != null ? { signature } : {}),
};
stepToolApprovalResponses[toolCall.toolCallId] = {
type: 'tool-approval-response',
approvalId,
toolCall,
approved: false,
reason: toolApprovalStatus.reason,
providerExecuted: toolCall.providerExecuted,
};
blockedToolCallIds.add(toolCall.toolCallId);
break;
}
}
}
// insert error tool outputs for invalid tool calls:
// TODO AI SDK 6: invalid inputs should not require output parts
const invalidToolCalls = stepToolCalls.filter(
toolCall => toolCall.invalid && toolCall.dynamic,
);
clientToolOutputs = [];
for (const toolCall of invalidToolCalls) {
clientToolOutputs.push({
type: 'tool-error',
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
error: getErrorMessage(toolCall.error!),
dynamic: true,
});
}
// execute client tool calls:
clientToolCalls = stepToolCalls.filter(
toolCall => !toolCall.providerExecuted,
);
toolApprovalResponses = Object.values(stepToolApprovalResponses);
deniedToolApprovalResponses = toolApprovalResponses.filter(
toolApprovalResponse => toolApprovalResponse.approved === false,
);
const toolExecutionMs: Record<string, number> = {};
if (tools != null) {
const toolExecutionResults = await executeTools({
toolCalls: clientToolCalls.filter(
toolCall =>
!toolCall.invalid &&
!blockedToolCallIds.has(toolCall.toolCallId),
),
tools,
callId,
messages: stepMessages,
abortSignal: mergedAbortSignal,
timeout,
experimental_sandbox: stepSandbox,
toolsContext,
onToolExecutionStart: event =>
notify({
event,
callbacks: [
resolvedOnToolExecutionStart,
telemetryDispatcher.onToolExecutionStart,
],
}),
onToolExecutionEnd: event =>
notify({
event,
callbacks: [
resolvedOnToolExecutionEnd,
telemetryDispatcher.onToolExecutionEnd,
],
}),
executeToolInTelemetryContext:
telemetryDispatcher.executeTool,
runInTracingChannelSpan,
});
for (const result of toolExecutionResults) {
toolExecutionMs[result.output.toolCallId] =
result.toolExecutionMs;
clientToolOutputs.push(result.output);
}
}
const stepTimeMs = now() - stepStartTimestampMs;
const stepPerformance: StepResultPerformance = {
effectiveOutputTokensPerSecond: calculateTokensPerSecond({
tokens: stepUsage.outputTokens,
durationMs: responseTimeMs,
}),
outputTokensPerSecond: undefined,
inputTokensPerSecond: undefined,
effectiveTotalTokensPerSecond: calculateTokensPerSecond({
tokens: sumTokenCounts(
stepUsage.inputTokens,
stepUsage.outputTokens,
),
durationMs: responseTimeMs,
}),
stepTimeMs,
responseTimeMs,
toolExecutionMs,
timeToFirstOutputMs: undefined,
};
// Track provider-executed tool calls that support deferred results.
// In programmatic tool calling, a server tool (e.g., code_execution) may
// trigger a client tool, and the server tool's result is deferred until
// the client tool's result is sent back.
for (const toolCall of stepToolCalls) {
if (!toolCall.providerExecuted) continue;
const tool = getOwn(tools, toolCall.toolName);
if (tool?.type === 'provider' && tool.supportsDeferredResults) {
// Check if this tool call already has a result in the current response
const hasResultInResponse = currentModelResponse.content.some(
part =>
part.type === 'tool-result' &&
part.toolCallId === toolCall.toolCallId,
);
if (!hasResultInResponse) {
pendingDeferredToolCalls.set(toolCall.toolCallId, {
toolName: toolCall.toolName,
});
}
}
}
// Mark deferred tool calls as resolved when we receive their results
for (const part of currentModelResponse.content) {
if (part.type === 'tool-result') {
pendingDeferredToolCalls.delete(part.toolCallId);
}
}
// content:
const stepContent = asContent({
content: currentModelResponse.content,
toolCalls: stepToolCalls,
toolOutputs: clientToolOutputs,
toolApprovalRequests: Object.values(toolApprovalRequests),
toolApprovalResponses,
tools,
});
const stepResponseMessages = await toResponseMessages({
content: stepContent,
tools,
});
// Add step information (after response messages are updated):
// Conditionally include request.body and response.body based on include settings.
// Large payloads (e.g., base64-encoded images) can cause memory issues.
const stepRequest: LanguageModelRequestMetadata = {
...currentModelResponse.request,
body: include.requestBody
? currentModelResponse.request?.body
: undefined,
messages: include.requestMessages
? cloneModelMessages(stepMessages)
: undefined,
};
const stepResponse = {
...currentModelResponse.response,
// deep clone msgs to avoid mutating step results in multi-step:
messages: cloneModelMessages(stepResponseMessages),
// Conditionally include response body:
body: include.responseBody
? currentModelResponse.response?.body
: undefined,
};
const currentStepResult: StepResult<TOOLS, RUNTIME_CONTEXT> =
new DefaultStepResult({
callId,
stepNumber,
provider: stepModel.provider,
modelId: stepModel.modelId,
runtimeContext,
content: stepContent,
finishReason: currentModelResponse.finishReason.unified,
rawFinishReason: currentModelResponse.finishReason.raw,
usage: stepUsage,
performance: stepPerformance,
warnings: currentModelResponse.warnings,
providerMetadata: currentModelResponse.providerMetadata,
request: stepRequest,
response: stepResponse,