ai
Version:
AI SDK by Vercel - The AI Toolkit for TypeScript and JavaScript
1,271 lines (1,139 loc) • 43.8 kB
text/typescript
import {
LanguageModelV3,
LanguageModelV3Content,
LanguageModelV3ToolCall,
} from '@ai-sdk/provider';
import {
createIdGenerator,
getErrorMessage,
IdGenerator,
ProviderOptions,
ToolApprovalResponse,
withUserAgentSuffix,
} from '@ai-sdk/provider-utils';
import { Tracer } from '@opentelemetry/api';
import { NoOutputGeneratedError } from '../error';
import { logWarnings } from '../logger/log-warnings';
import { resolveLanguageModel } from '../model/resolve-model';
import { ModelMessage } from '../prompt';
import {
CallSettings,
getStepTimeoutMs,
getTotalTimeoutMs,
} from '../prompt/call-settings';
import { convertToLanguageModelPrompt } from '../prompt/convert-to-language-model-prompt';
import { createToolModelOutput } from '../prompt/create-tool-model-output';
import { prepareCallSettings } from '../prompt/prepare-call-settings';
import { prepareToolsAndToolChoice } from '../prompt/prepare-tools-and-tool-choice';
import { Prompt } from '../prompt/prompt';
import { standardizePrompt } from '../prompt/standardize-prompt';
import { wrapGatewayError } from '../prompt/wrap-gateway-error';
import { ToolCallNotFoundForApprovalError } from '../error/tool-call-not-found-for-approval-error';
import { assembleOperationName } from '../telemetry/assemble-operation-name';
import { getBaseTelemetryAttributes } from '../telemetry/get-base-telemetry-attributes';
import { getTracer } from '../telemetry/get-tracer';
import { recordSpan } from '../telemetry/record-span';
import { selectTelemetryAttributes } from '../telemetry/select-telemetry-attributes';
import { stringifyForTelemetry } from '../telemetry/stringify-for-telemetry';
import { TelemetrySettings } from '../telemetry/telemetry-settings';
import {
LanguageModel,
LanguageModelRequestMetadata,
ToolChoice,
} from '../types';
import {
addLanguageModelUsage,
asLanguageModelUsage,
LanguageModelUsage,
} from '../types/usage';
import { asArray } from '../util/as-array';
import { DownloadFunction } from '../util/download/download-function';
import { mergeObjects } from '../util/merge-objects';
import { prepareRetries } from '../util/prepare-retries';
import { VERSION } from '../version';
import { collectToolApprovals } from './collect-tool-approvals';
import { ContentPart } from './content-part';
import { executeToolCall } from './execute-tool-call';
import { extractReasoningContent } from './extract-reasoning-content';
import { extractTextContent } from './extract-text-content';
import { GenerateTextResult } from './generate-text-result';
import { DefaultGeneratedFile } from './generated-file';
import { isApprovalNeeded } from './is-approval-needed';
import { Output, text } from './output';
import { InferCompleteOutput } from './output-utils';
import { parseToolCall } from './parse-tool-call';
import { PrepareStepFunction } from './prepare-step';
import { ResponseMessage } from './response-message';
import { DefaultStepResult, StepResult } from './step-result';
import {
isStopConditionMet,
stepCountIs,
StopCondition,
} from './stop-condition';
import { toResponseMessages } from './to-response-messages';
import { ToolApprovalRequestOutput } from './tool-approval-request-output';
import { TypedToolCall } from './tool-call';
import { ToolCallRepairFunction } from './tool-call-repair-function';
import { TypedToolError } from './tool-error';
import { ToolOutput } from './tool-output';
import { TypedToolResult } from './tool-result';
import { ToolSet } from './tool-set';
import { mergeAbortSignals } from '../util/merge-abort-signals';
const originalGenerateId = createIdGenerator({
prefix: 'aitxt',
size: 24,
});
/**
* Callback that is set using the `onStepFinish` option.
*
* @param stepResult - The result of the step.
*/
export type GenerateTextOnStepFinishCallback<TOOLS extends ToolSet> = (
stepResult: StepResult<TOOLS>,
) => Promise<void> | void;
/**
* Callback that is set using the `onFinish` option.
*
* @param event - The event that is passed to the callback.
*/
export type GenerateTextOnFinishCallback<TOOLS extends ToolSet> = (
event: StepResult<TOOLS> & {
/**
* Details for all steps.
*/
readonly steps: StepResult<TOOLS>[];
/**
* Total usage for all steps. This is the sum of the usage of all steps.
*/
readonly totalUsage: LanguageModelUsage;
/**
* Context that is passed into tool execution.
*
* Experimental (can break in patch releases).
*
* @default undefined
*/
experimental_context: unknown;
},
) => PromiseLike<void> | void;
/**
* 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 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 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 onStepFinish - Callback that is called when each step (LLM call) is finished, including intermediate steps.
* @param onFinish - Callback that is called when all steps are finished and the response is complete.
*
* @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,
OUTPUT extends Output = Output<string, string>,
>({
model: modelArg,
tools,
toolChoice,
system,
prompt,
messages,
maxRetries: maxRetriesArg,
abortSignal,
timeout,
headers,
stopWhen = stepCountIs(1),
experimental_output,
output = experimental_output,
experimental_telemetry: telemetry,
providerOptions,
experimental_activeTools,
activeTools = experimental_activeTools,
experimental_prepareStep,
prepareStep = experimental_prepareStep,
experimental_repairToolCall: repairToolCall,
experimental_download: download,
experimental_context,
experimental_include: include,
_internal: { generateId = originalGenerateId } = {},
onStepFinish,
onFinish,
...settings
}: CallSettings &
Prompt & {
/**
* The language model to use.
*/
model: LanguageModel;
/**
* The tools that the model can call. The model needs to support calling tools.
*/
tools?: TOOLS;
/**
* 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 stepCountIs(1)
*/
stopWhen?:
| StopCondition<NoInfer<TOOLS>>
| Array<StopCondition<NoInfer<TOOLS>>>;
/**
* Optional telemetry configuration (experimental).
*/
experimental_telemetry?: TelemetrySettings;
/**
* 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;
/**
* @deprecated Use `activeTools` instead.
*/
experimental_activeTools?: Array<keyof NoInfer<TOOLS>>;
/**
* Limits the tools that are available for the model to call without
* changing the tool call and result types in the result.
*/
activeTools?: Array<keyof NoInfer<TOOLS>>;
/**
* Optional specification for parsing structured outputs from the LLM response.
*/
output?: OUTPUT;
/**
* Optional specification for parsing structured outputs from the LLM response.
*
* @deprecated Use `output` instead.
*/
experimental_output?: OUTPUT;
/**
* 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;
/**
* @deprecated Use `prepareStep` instead.
*/
experimental_prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>;
/**
* Optional function that you can use to provide different settings for a step.
*/
prepareStep?: PrepareStepFunction<NoInfer<TOOLS>>;
/**
* A function that attempts to repair a tool call that failed to parse.
*/
experimental_repairToolCall?: ToolCallRepairFunction<NoInfer<TOOLS>>;
/**
* Callback that is called when each step (LLM call) is finished, including intermediate steps.
*/
onStepFinish?: GenerateTextOnStepFinishCallback<NoInfer<TOOLS>>;
/**
* Callback that is called when all steps are finished and the response is complete.
*/
onFinish?: GenerateTextOnFinishCallback<NoInfer<TOOLS>>;
/**
* Context that is passed into tool execution.
*
* Experimental (can break in patch releases).
*
* @default undefined
*/
experimental_context?: unknown;
/**
* 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, all data is included for backwards compatibility.
*/
experimental_include?: {
/**
* Whether to retain the request body in step results.
* The request body can be large when sending images or files.
* @default true
*/
requestBody?: boolean;
/**
* Whether to retain the response body in step results.
* @default true
*/
responseBody?: boolean;
};
/**
* Internal. For test use only. May change without notice.
*/
_internal?: {
generateId?: IdGenerator;
};
}): Promise<GenerateTextResult<TOOLS, OUTPUT>> {
const model = resolveLanguageModel(modelArg);
const stopConditions = asArray(stopWhen);
const totalTimeoutMs = getTotalTimeoutMs(timeout);
const stepTimeoutMs = getStepTimeoutMs(timeout);
const stepAbortController =
stepTimeoutMs != null ? new AbortController() : undefined;
const mergedAbortSignal = mergeAbortSignals(
abortSignal,
totalTimeoutMs != null ? AbortSignal.timeout(totalTimeoutMs) : undefined,
stepAbortController?.signal,
);
const { maxRetries, retry } = prepareRetries({
maxRetries: maxRetriesArg,
abortSignal: mergedAbortSignal,
});
const callSettings = prepareCallSettings(settings);
const headersWithUserAgent = withUserAgentSuffix(
headers ?? {},
`ai/${VERSION}`,
);
const baseTelemetryAttributes = getBaseTelemetryAttributes({
model,
telemetry,
headers: headersWithUserAgent,
settings: { ...callSettings, maxRetries },
});
const initialPrompt = await standardizePrompt({
system,
prompt,
messages,
} as Prompt);
const tracer = getTracer(telemetry);
try {
return await recordSpan({
name: 'ai.generateText',
attributes: selectTelemetryAttributes({
telemetry,
attributes: {
...assembleOperationName({
operationId: 'ai.generateText',
telemetry,
}),
...baseTelemetryAttributes,
// model:
'ai.model.provider': model.provider,
'ai.model.id': model.modelId,
// specific settings that only make sense on the outer level:
'ai.prompt': {
input: () => JSON.stringify({ system, prompt, messages }),
},
},
}),
tracer,
fn: async span => {
const initialMessages = initialPrompt.messages;
const responseMessages: Array<ResponseMessage> = [];
const { approvedToolApprovals, deniedToolApprovals } =
collectToolApprovals<TOOLS>({ messages: initialMessages });
const localApprovedToolApprovals = approvedToolApprovals.filter(
toolApproval => !toolApproval.toolCall.providerExecuted,
);
if (
deniedToolApprovals.length > 0 ||
localApprovedToolApprovals.length > 0
) {
const toolOutputs = await executeTools({
toolCalls: localApprovedToolApprovals.map(
toolApproval => toolApproval.toolCall,
),
tools: tools as TOOLS,
tracer,
telemetry,
messages: initialMessages,
abortSignal: mergedAbortSignal,
experimental_context,
});
const toolContent: Array<any> = [];
// add regular tool results for approved tool calls:
for (const output of toolOutputs) {
const modelOutput = await createToolModelOutput({
toolCallId: output.toolCallId,
input: output.input,
tool: tools?.[output.toolName],
output:
output.type === 'tool-result' ? output.output : output.error,
errorMode: output.type === 'tool-error' ? 'json' : '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,
},
},
}),
},
});
}
responseMessages.push({
role: 'tool',
content: toolContent,
});
}
// Forward provider-executed approval responses to the provider
const providerExecutedToolApprovals = [
...approvedToolApprovals,
...deniedToolApprovals,
].filter(toolApproval => toolApproval.toolCall.providerExecuted);
if (providerExecutedToolApprovals.length > 0) {
responseMessages.push({
role: 'tool',
content: providerExecutedToolApprovals.map(
toolApproval =>
({
type: 'tool-approval-response',
approvalId: toolApproval.approvalResponse.approvalId,
approved: toolApproval.approvalResponse.approved,
reason: toolApproval.approvalResponse.reason,
providerExecuted: true,
}) satisfies ToolApprovalResponse,
),
});
}
const callSettings = prepareCallSettings(settings);
let currentModelResponse: Awaited<
ReturnType<LanguageModelV3['doGenerate']>
> & { response: { id: string; timestamp: Date; modelId: string } };
let clientToolCalls: Array<TypedToolCall<TOOLS>> = [];
let clientToolOutputs: Array<ToolOutput<TOOLS>> = [];
const steps: GenerateTextResult<TOOLS, OUTPUT>['steps'] = [];
// 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 =
stepTimeoutMs != null
? setTimeout(() => stepAbortController!.abort(), stepTimeoutMs)
: undefined;
try {
const stepInputMessages = [...initialMessages, ...responseMessages];
const prepareStepResult = await prepareStep?.({
model,
steps,
stepNumber: steps.length,
messages: stepInputMessages,
experimental_context,
});
const stepModel = resolveLanguageModel(
prepareStepResult?.model ?? model,
);
const promptMessages = await convertToLanguageModelPrompt({
prompt: {
system: prepareStepResult?.system ?? initialPrompt.system,
messages: prepareStepResult?.messages ?? stepInputMessages,
},
supportedUrls: await stepModel.supportedUrls,
download,
});
experimental_context =
prepareStepResult?.experimental_context ?? experimental_context;
const { toolChoice: stepToolChoice, tools: stepTools } =
await prepareToolsAndToolChoice({
tools,
toolChoice: prepareStepResult?.toolChoice ?? toolChoice,
activeTools: prepareStepResult?.activeTools ?? activeTools,
});
currentModelResponse = await retry(() =>
recordSpan({
name: 'ai.generateText.doGenerate',
attributes: selectTelemetryAttributes({
telemetry,
attributes: {
...assembleOperationName({
operationId: 'ai.generateText.doGenerate',
telemetry,
}),
...baseTelemetryAttributes,
// model:
'ai.model.provider': stepModel.provider,
'ai.model.id': stepModel.modelId,
// prompt:
'ai.prompt.messages': {
input: () => stringifyForTelemetry(promptMessages),
},
'ai.prompt.tools': {
// convert the language model level tools:
input: () => stepTools?.map(tool => JSON.stringify(tool)),
},
'ai.prompt.toolChoice': {
input: () =>
stepToolChoice != null
? JSON.stringify(stepToolChoice)
: undefined,
},
// standardized gen-ai llm span attributes:
'gen_ai.system': stepModel.provider,
'gen_ai.request.model': stepModel.modelId,
'gen_ai.request.frequency_penalty':
settings.frequencyPenalty,
'gen_ai.request.max_tokens': settings.maxOutputTokens,
'gen_ai.request.presence_penalty': settings.presencePenalty,
'gen_ai.request.stop_sequences': settings.stopSequences,
'gen_ai.request.temperature':
settings.temperature ?? undefined,
'gen_ai.request.top_k': settings.topK,
'gen_ai.request.top_p': settings.topP,
},
}),
tracer,
fn: async span => {
const stepProviderOptions = mergeObjects(
providerOptions,
prepareStepResult?.providerOptions,
);
const result = await stepModel.doGenerate({
...callSettings,
tools: stepTools,
toolChoice: stepToolChoice,
responseFormat: await output?.responseFormat,
prompt: promptMessages,
providerOptions: stepProviderOptions,
abortSignal: mergedAbortSignal,
headers: headersWithUserAgent,
});
// Fill in default values:
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,
};
// Add response information to the span:
span.setAttributes(
await selectTelemetryAttributes({
telemetry,
attributes: {
'ai.response.finishReason': result.finishReason.unified,
'ai.response.text': {
output: () => extractTextContent(result.content),
},
'ai.response.reasoning': {
output: () => extractReasoningContent(result.content),
},
'ai.response.toolCalls': {
output: () => {
const toolCalls = asToolCalls(result.content);
return toolCalls == null
? undefined
: JSON.stringify(toolCalls);
},
},
'ai.response.id': responseData.id,
'ai.response.model': responseData.modelId,
'ai.response.timestamp':
responseData.timestamp.toISOString(),
'ai.response.providerMetadata': JSON.stringify(
result.providerMetadata,
),
// TODO rename telemetry attributes to inputTokens and outputTokens
'ai.usage.promptTokens': result.usage.inputTokens.total,
'ai.usage.completionTokens':
result.usage.outputTokens.total,
// standardized gen-ai llm span attributes:
'gen_ai.response.finish_reasons': [
result.finishReason.unified,
],
'gen_ai.response.id': responseData.id,
'gen_ai.response.model': responseData.modelId,
'gen_ai.usage.input_tokens':
result.usage.inputTokens.total,
'gen_ai.usage.output_tokens':
result.usage.outputTokens.total,
},
}),
);
return { ...result, response: responseData };
},
}),
);
// parse tool calls:
const stepToolCalls: TypedToolCall<TOOLS>[] = await Promise.all(
currentModelResponse.content
.filter(
(part): part is LanguageModelV3ToolCall =>
part.type === 'tool-call',
)
.map(toolCall =>
parseToolCall({
toolCall,
tools,
repairToolCall,
system,
messages: stepInputMessages,
}),
),
);
const toolApprovalRequests: Record<
string,
ToolApprovalRequestOutput<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 = 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: stepInputMessages,
abortSignal: mergedAbortSignal,
experimental_context,
});
}
if (
await isApprovalNeeded({
tool,
toolCall,
messages: stepInputMessages,
experimental_context,
})
) {
toolApprovalRequests[toolCall.toolCallId] = {
type: 'tool-approval-request',
approvalId: generateId(),
toolCall,
};
}
}
// 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,
);
if (tools != null) {
clientToolOutputs.push(
...(await executeTools({
toolCalls: clientToolCalls.filter(
toolCall =>
!toolCall.invalid &&
toolApprovalRequests[toolCall.toolCallId] == null,
),
tools,
tracer,
telemetry,
messages: stepInputMessages,
abortSignal: mergedAbortSignal,
experimental_context,
})),
);
}
// 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 = 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),
tools,
});
// append to messages for potential next step:
responseMessages.push(
...(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 =
(include?.requestBody ?? true)
? (currentModelResponse.request ?? {})
: { ...currentModelResponse.request, body: undefined };
const stepResponse = {
...currentModelResponse.response,
// deep clone msgs to avoid mutating past messages in multi-step:
messages: structuredClone(responseMessages),
// Conditionally include response body:
body:
(include?.responseBody ?? true)
? currentModelResponse.response?.body
: undefined,
};
const currentStepResult: StepResult<TOOLS> = new DefaultStepResult({
content: stepContent,
finishReason: currentModelResponse.finishReason.unified,
rawFinishReason: currentModelResponse.finishReason.raw,
usage: asLanguageModelUsage(currentModelResponse.usage),
warnings: currentModelResponse.warnings,
providerMetadata: currentModelResponse.providerMetadata,
request: stepRequest,
response: stepResponse,
});
logWarnings({
warnings: currentModelResponse.warnings ?? [],
provider: stepModel.provider,
model: stepModel.modelId,
});
steps.push(currentStepResult);
await onStepFinish?.(currentStepResult);
} finally {
if (stepTimeoutId != null) {
clearTimeout(stepTimeoutId);
}
}
} while (
// Continue if:
// 1. There are client tool calls that have all been executed, OR
// 2. There are pending deferred results from provider-executed tools
((clientToolCalls.length > 0 &&
clientToolOutputs.length === clientToolCalls.length) ||
pendingDeferredToolCalls.size > 0) &&
// continue until a stop condition is met:
!(await isStopConditionMet({ stopConditions, steps }))
);
// Add response information to the span:
span.setAttributes(
await selectTelemetryAttributes({
telemetry,
attributes: {
'ai.response.finishReason':
currentModelResponse.finishReason.unified,
'ai.response.text': {
output: () => extractTextContent(currentModelResponse.content),
},
'ai.response.reasoning': {
output: () =>
extractReasoningContent(currentModelResponse.content),
},
'ai.response.toolCalls': {
output: () => {
const toolCalls = asToolCalls(currentModelResponse.content);
return toolCalls == null
? undefined
: JSON.stringify(toolCalls);
},
},
'ai.response.providerMetadata': JSON.stringify(
currentModelResponse.providerMetadata,
),
// TODO rename telemetry attributes to inputTokens and outputTokens
'ai.usage.promptTokens':
currentModelResponse.usage.inputTokens.total,
'ai.usage.completionTokens':
currentModelResponse.usage.outputTokens.total,
},
}),
);
const lastStep = steps[steps.length - 1];
const totalUsage = steps.reduce(
(totalUsage, step) => {
return addLanguageModelUsage(totalUsage, step.usage);
},
{
inputTokens: undefined,
outputTokens: undefined,
totalTokens: undefined,
reasoningTokens: undefined,
cachedInputTokens: undefined,
} as LanguageModelUsage,
);
await onFinish?.({
finishReason: lastStep.finishReason,
rawFinishReason: lastStep.rawFinishReason,
usage: lastStep.usage,
content: lastStep.content,
text: lastStep.text,
reasoningText: lastStep.reasoningText,
reasoning: lastStep.reasoning,
files: lastStep.files,
sources: lastStep.sources,
toolCalls: lastStep.toolCalls,
staticToolCalls: lastStep.staticToolCalls,
dynamicToolCalls: lastStep.dynamicToolCalls,
toolResults: lastStep.toolResults,
staticToolResults: lastStep.staticToolResults,
dynamicToolResults: lastStep.dynamicToolResults,
request: lastStep.request,
response: lastStep.response,
warnings: lastStep.warnings,
providerMetadata: lastStep.providerMetadata,
steps,
totalUsage,
experimental_context,
});
// parse output only if the last step was finished with "stop":
let resolvedOutput;
if (lastStep.finishReason === 'stop') {
const outputSpecification = output ?? text();
resolvedOutput = await outputSpecification.parseCompleteOutput(
{ text: lastStep.text },
{
response: lastStep.response,
usage: lastStep.usage,
finishReason: lastStep.finishReason,
},
);
}
return new DefaultGenerateTextResult({
steps,
totalUsage,
output: resolvedOutput,
});
},
});
} catch (error) {
throw wrapGatewayError(error);
}
}
async function executeTools<TOOLS extends ToolSet>({
toolCalls,
tools,
tracer,
telemetry,
messages,
abortSignal,
experimental_context,
}: {
toolCalls: Array<TypedToolCall<TOOLS>>;
tools: TOOLS;
tracer: Tracer;
telemetry: TelemetrySettings | undefined;
messages: ModelMessage[];
abortSignal: AbortSignal | undefined;
experimental_context: unknown;
}): Promise<Array<ToolOutput<TOOLS>>> {
const toolOutputs = await Promise.all(
toolCalls.map(async toolCall =>
executeToolCall({
toolCall,
tools,
tracer,
telemetry,
messages,
abortSignal,
experimental_context,
}),
),
);
return toolOutputs.filter(
(output): output is NonNullable<typeof output> => output != null,
);
}
class DefaultGenerateTextResult<TOOLS extends ToolSet, OUTPUT extends Output>
implements GenerateTextResult<TOOLS, OUTPUT>
{
readonly steps: GenerateTextResult<TOOLS, OUTPUT>['steps'];
readonly totalUsage: LanguageModelUsage;
private readonly _output: InferCompleteOutput<OUTPUT> | undefined;
constructor(options: {
steps: GenerateTextResult<TOOLS, OUTPUT>['steps'];
output: InferCompleteOutput<OUTPUT> | undefined;
totalUsage: LanguageModelUsage;
}) {
this.steps = options.steps;
this._output = options.output;
this.totalUsage = options.totalUsage;
}
private get finalStep() {
return this.steps[this.steps.length - 1];
}
get content() {
return this.finalStep.content;
}
get text() {
return this.finalStep.text;
}
get files() {
return this.finalStep.files;
}
get reasoningText() {
return this.finalStep.reasoningText;
}
get reasoning() {
return this.finalStep.reasoning;
}
get toolCalls() {
return this.finalStep.toolCalls;
}
get staticToolCalls() {
return this.finalStep.staticToolCalls;
}
get dynamicToolCalls() {
return this.finalStep.dynamicToolCalls;
}
get toolResults() {
return this.finalStep.toolResults;
}
get staticToolResults() {
return this.finalStep.staticToolResults;
}
get dynamicToolResults() {
return this.finalStep.dynamicToolResults;
}
get sources() {
return this.finalStep.sources;
}
get finishReason() {
return this.finalStep.finishReason;
}
get rawFinishReason() {
return this.finalStep.rawFinishReason;
}
get warnings() {
return this.finalStep.warnings;
}
get providerMetadata() {
return this.finalStep.providerMetadata;
}
get response() {
return this.finalStep.response;
}
get request() {
return this.finalStep.request;
}
get usage() {
return this.finalStep.usage;
}
get experimental_output() {
return this.output;
}
get output() {
if (this._output == null) {
throw new NoOutputGeneratedError();
}
return this._output;
}
}
function asToolCalls(content: Array<LanguageModelV3Content>) {
const parts = content.filter(
(part): part is LanguageModelV3ToolCall => part.type === 'tool-call',
);
if (parts.length === 0) {
return undefined;
}
return parts.map(toolCall => ({
toolCallId: toolCall.toolCallId,
toolName: toolCall.toolName,
input: toolCall.input,
}));
}
function asContent<TOOLS extends ToolSet>({
content,
toolCalls,
toolOutputs,
toolApprovalRequests,
tools,
}: {
content: Array<LanguageModelV3Content>;
toolCalls: Array<TypedToolCall<TOOLS>>;
toolOutputs: Array<ToolOutput<TOOLS>>;
toolApprovalRequests: Array<ToolApprovalRequestOutput<TOOLS>>;
tools: TOOLS | undefined;
}): Array<ContentPart<TOOLS>> {
const contentParts: Array<ContentPart<TOOLS>> = [];
for (const part of content) {
switch (part.type) {
case 'text':
case 'reasoning':
case 'source':
contentParts.push(part);
break;
case 'file': {
contentParts.push({
type: 'file' as const,
file: new DefaultGeneratedFile(part),
...(part.providerMetadata != null
? { providerMetadata: part.providerMetadata }
: {}),
});
break;
}
case 'tool-call': {
contentParts.push(
toolCalls.find(toolCall => toolCall.toolCallId === part.toolCallId)!,
);
break;
}
case 'tool-result': {
const toolCall = toolCalls.find(
toolCall => toolCall.toolCallId === part.toolCallId,
);
// Handle deferred results for provider-executed tools (e.g., programmatic tool calling).
// When a server tool (like code_execution) triggers a client tool, the server tool's
// result may be deferred to a later turn. In this case, there's no matching tool-call
// in the current response.
if (toolCall == null) {
const tool = tools?.[part.toolName];
const supportsDeferredResults =
tool?.type === 'provider' && tool.supportsDeferredResults;
if (!supportsDeferredResults) {
throw new Error(`Tool call ${part.toolCallId} not found.`);
}
// Create tool result without tool call input (deferred result)
if (part.isError) {
contentParts.push({
type: 'tool-error' as const,
toolCallId: part.toolCallId,
toolName: part.toolName as keyof TOOLS & string,
input: undefined,
error: part.result,
providerExecuted: true,
dynamic: part.dynamic,
} as TypedToolError<TOOLS>);
} else {
contentParts.push({
type: 'tool-result' as const,
toolCallId: part.toolCallId,
toolName: part.toolName as keyof TOOLS & string,
input: undefined,
output: part.result,
providerExecuted: true,
dynamic: part.dynamic,
} as TypedToolResult<TOOLS>);
}
break;
}
if (part.isError) {
contentParts.push({
type: 'tool-error' as const,
toolCallId: part.toolCallId,
toolName: part.toolName as keyof TOOLS & string,
input: toolCall.input,
error: part.result,
providerExecuted: true,
dynamic: toolCall.dynamic,
} as TypedToolError<TOOLS>);
} else {
contentParts.push({
type: 'tool-result' as const,
toolCallId: part.toolCallId,
toolName: part.toolName as keyof TOOLS & string,
input: toolCall.input,
output: part.result,
providerExecuted: true,
dynamic: toolCall.dynamic,
} as TypedToolResult<TOOLS>);
}
break;
}
case 'tool-approval-request': {
const toolCall = toolCalls.find(
toolCall => toolCall.toolCallId === part.toolCallId,
);
if (toolCall == null) {
throw new ToolCallNotFoundForApprovalError({
toolCallId: part.toolCallId,
approvalId: part.approvalId,
});
}
contentParts.push({
type: 'tool-approval-request' as const,
approvalId: part.approvalId,
toolCall,
});
break;
}
}
}
return [...contentParts, ...toolOutputs, ...toolApprovalRequests];
}