UNPKG

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,913 lines • 70.8 kB
import {
  getRequestOrigin,
  sanitizeRequestMetadata,
} from '../telemetry/get-request-origin';
import type {
  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 { ToolChoiceViolationError } from '../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 {
  getChunkTimeoutMs,
  getFirstChunkTimeoutMs,
  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,
  Warning,
} 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 { convertLanguageModelContent } from './convert-language-model-content';
import { createToolSearchState } from '../tool-search/prepare-tool-search';
import { executeToolCall } from './execute-tool-call';
import {
  filterActiveTools,
  type ActiveToolSubset,
} from './filter-active-tools';
import type {
  GenerateTextOnEndCallback,
  GenerateTextOnStartCallback,
  GenerateTextOnStepEndCallback,
  GenerateTextOnStepErrorCallback,
  GenerateTextOnStepFinishCallback,
  GenerateTextOnStepStartCallback,
  GenerateTextStepStartEvent,
} from './generate-text-events';
import { DefaultGenerateTextResult } from './default-generate-text-result';
import type { GenerateTextResult } from './generate-text-result';
import { isToolExecutionAllowedFinishReason } from './is-tool-execution-allowed-finish-reason';
import type {
  OnLanguageModelCallEndCallback,
  OnLanguageModelCallStartCallback,
} from './language-model-events';
import { text, type Output } from './output';
import { parseToolCall } from './parse-tool-call';
import type { PrepareStepFunction } from './prepare-step';
import { prepareStepCallSettings } from './prepare-step-call-settings';
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 {
  startSteeringRun,
  type Steering,
  type SteeringNotDeliveredReason,
  type SteeringRun,
} from './steering';
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 {
  appendToolCallerMessages,
  prepareToolsForToolCallers,
  resolveToolCallerConfiguration,
  type Experimental_ToolCallers,
} from './tool-caller-configuration';
import type { TypedToolCall } from './tool-call';
import type { ToolCallRepairFunction } from './tool-call-repair-function';
import type {
  OnToolExecutionEndCallback,
  OnToolExecutionStartCallback,
} from './tool-execution-events';
import { validateToolContext } from './validate-tool-context';
import type { ToolInputRefinement } from './tool-input-refinement';
import type { ToolOrder } from './tool-order';
import type { ToolOutput } from './tool-output';
import type { ToolsContextParameter } from './tools-context-parameter';
import { maybeSignApproval } from './tool-approval-signature';
import { validateApprovedToolApprovals } from './validate-tool-approvals';
import {
  mapWithConcurrency,
  prepareToolCallConcurrency,
} from './tool-call-concurrency';

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 toolCallConcurrency - Maximum number of tool calls that may execute concurrently within a step. Set to 1 for sequential execution. Default: unlimited.
 *
 * @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 experimental_steering - A steering object that adds user messages to the generation at the next step boundary.
 * @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 onStepError - Callback that is called when a step that started fails before it produces a step result, i.e. when the provider call throws or rejects, or when the call is aborted or times out.
 * @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>,
>(
  options: GenerateTextOptions<TOOLS, RUNTIME_CONTEXT, OUTPUT>,
): Promise<GenerateTextResult<TOOLS, RUNTIME_CONTEXT, OUTPUT>> {
  const steeringRun = startSteeringRun(options.experimental_steering);

  try {
    return await generateTextInternal<TOOLS, RUNTIME_CONTEXT, OUTPUT>(
      options,
      steeringRun,
    );
  } catch (error) {
    // the call can fail before its loop starts, e.g. when the prompt is invalid:
    steeringRun?.end(options.abortSignal?.aborted ? 'aborted' : 'error');
    throw error;
  }
}

type GenerateTextOptions<
  TOOLS extends ToolSet,
  RUNTIME_CONTEXT extends Context,
  OUTPUT extends Output,
> = Parameters<typeof generateTextInternal<TOOLS, RUNTIME_CONTEXT, OUTPUT>>[0];

async function generateTextInternal<
  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_toolCallers,
    experimental_toolApprovalSecret,
    experimental_telemetry,
    telemetry = experimental_telemetry,
    providerOptions,
    activeTools,
    toolOrder,
    toolCallConcurrency,
    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,
    onStepError,
    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;

      /**
       * A steering object created with `experimental_createSteering()`.
       * Messages sent with it are added to the generation at the next step
       * boundary.
       */
      experimental_steering?: Steering;

      /**
       * 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>>;

      /**
       * Maximum number of tool calls that may execute concurrently within a step.
       *
       * Set to `1` to execute tool calls sequentially in the order they were
       * generated. By default, all tool calls execute concurrently.
       */
      toolCallConcurrency?: number;

      /**
       * 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>;

      /**
       * Configures which caller tools may invoke each tool.
       */
      experimental_toolCallers?: Experimental_ToolCallers<NoInfer<TOOLS>>;

      /**
       * 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 in the prompt and for
       * generated file URLs returned by the model.
       *
       * By default, prompt files are downloaded if the model does not support
       * the URL for the given media type. Generated file URLs are always
       * downloaded; returning `null` for them is an error.
       */
      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 a step that started fails before it
       * produces a step result. `onStepEnd` is not called for such a step.
       */
      onStepError?: GenerateTextOnStepErrorCallback<
        NoInfer<TOOLS>,
        NoInfer<RUNTIME_CONTEXT>,
        NoInfer<OUTPUT>
      >;

      /**
       * 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;
      };
    },
  steeringRun: SteeringRun | undefined,
): 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 resolvedToolCallConcurrency =
    prepareToolCallConcurrency(toolCallConcurrency);
  const model = resolveLanguageModel(modelArg);
  const resolvedToolCallers = resolveToolCallerConfiguration({
    tools,
    toolCallers: experimental_toolCallers,
  });
  const prepareToolSearch = createToolSearchState({
    tools,
    toolCallers: resolvedToolCallers,
  });
  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 unsupportedTimeoutWarnings: Warning[] = [];

  if (getFirstChunkTimeoutMs(timeout) != null) {
    unsupportedTimeoutWarnings.push({
      type: 'unsupported',
      feature: 'timeout.firstChunkMs',
      details:
        'The firstChunkMs timeout is only supported by streaming functions.',
    });
  }

  if (getChunkTimeoutMs(timeout) != null) {
    unsupportedTimeoutWarnings.push({
      type: 'unsupported',
      feature: 'timeout.chunkMs',
      details: 'The chunkMs timeout is only supported by streaming functions.',
    });
  }

  if (unsupportedTimeoutWarnings.length > 0) {
    logWarnings({
      warnings: unsupportedTimeoutWarnings,
      provider: model.provider,
      model: model.modelId,
    });
  }

  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],
    });

    // the step that has started but not ended, reported to onStepError when
    // the call fails before the step produces a result:
    let openStep:
      | GenerateTextStepStartEvent<
          NoInfer<TOOLS>,
          NoInfer<RUNTIME_CONTEXT>,
          NoInfer<OUTPUT>
        >
      | undefined;

    try {
      const initialMessages = initialPrompt.messages;
      const initialResponseMessages: Array<ResponseMessage> = [];

      const {
        approvedToolApprovals,
        deniedToolApprovals: collectedDeniedToolApprovals,
      } = collectToolApprovals<TOOLS>({ messages: initialMessages });

      const {
        approvedToolApprovals: localApprovedToolApprovals,
        deniedToolApprovals: revalidationDeniedToolApprovals,
        invalidToolApprovals,
      } = await validateApprovedToolApprovals<TOOLS, RUNTIME_CONTEXT>({
        approvedToolApprovals: approvedToolApprovals.filter(
          toolApproval => !toolApproval.toolCall.providerExecuted,
        ),
        tools,
        toolApproval,
        messages: initialMessages,
        toolsContext,
        runtimeContext,
        toolApprovalSecret: experimental_toolApprovalSecret,
        refineToolInput,
      });

      const deniedToolApprovals = [
        ...collectedDeniedToolApprovals,
        ...revalidationDeniedToolApprovals,
      ];
      const deniedToolApprovalsWithoutResults = deniedToolApprovals.filter(
        toolApproval => toolApproval.existingToolResult == null,
      );

      if (
        deniedToolApprovalsWithoutResults.length > 0 ||
        localApprovedToolApprovals.length > 0 ||
        invalidToolApprovals.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,
          toolCallConcurrency: resolvedToolCallConcurrency,
        });

        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,
          });
        }

        // Report invalid approved tool calls to the model without executing
        // them. Repairing the input after approval would change the operation
        // that the user authorized.
        for (const toolApproval of invalidToolApprovals) {
          toolContent.push({
            type: 'tool-result' as const,
            toolCallId: toolApproval.toolCall.toolCallId,
            toolName: toolApproval.toolCall.toolName,
            output: await createToolModelOutput({
              toolCallId: toolApproval.toolCall.toolCallId,
              input: toolApproval.toolCall.input,
              tool: getOwn(tools, toolApproval.toolCall.toolName),
              output: toolApproval.error,
              errorMode: 'text',
            }),
          });
        }

        // add execution denied tool results for all denied tool approvals:
        for (const toolApproval of deniedToolApprovalsWithoutResults) {
          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 }>();

      // why waiting steering messages are not delivered when the loop ends:
      let steeringEndReason: SteeringNotDeliveredReason = 'finished';

      const shouldStartNextStep = async () => {
        if (steps[steps.length - 1].finishReason === 'interrupted') {
          return false;
        }

        // Continue only after all client tool calls have been executed or denied:
        if (
          clientToolOutputs.length + deniedToolApprovalResponses.length !==
          clientToolCalls.length
        ) {
          steeringEndReason = 'waiting-for-tool-results';
          return false;
        }

        // Continue if there are client results, pending deferred provider
        // results, or steering messages:
        if (
          clientToolCalls.length === 0 &&
          pendingDeferredToolCalls.size === 0
        ) {
          // queued messages wait for a task that was not cut off:
          const isTruncated =
            steps[steps.length - 1].finishReason === 'length' ||
            steps[steps.length - 1].finishReason === 'content-filter';

          if (
            !steeringRun?.hasPendingMessages() &&
            (isTruncated || !steeringRun?.promoteQueuedMessage())
          ) {
            return false;
          }

          // steering messages do not continue a failed step:
          if (steps[steps.length - 1].finishReason === 'error') {
            steeringEndReason = 'error';
            return false;
          }
        }

        // continue until a stop condition is met:
        if (await isStopConditionMet({ stopConditions, steps })) {
          steeringEndReason = 'stop-condition';
          return false;
        }

        return true;
      };

      do {
        if (steps.length > 0) {
          mergedAbortSignal?.throwIfAborted();
        }

        // Set up step timeout if configured
        const stepTimeoutId = setAbortTimeout({
          abortController: stepAbortController,
          label: 'Step',
          timeoutMs: stepTimeoutMs,
        });
        const stepNumber = steps.length;

        const steeringMessages = steeringRun?.takeMessages() ?? [];
        if (steeringMessages.length > 0) {
          messagesForNextStep = [...messagesForNextStep, ...steeringMessages];
        }

        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,
                ...(steeringMessages.length > 0
                  ? { experimental_steeringMessages: steeringMessages }
                  : {}),
                runtimeContext,
                toolsContext,
                experimental_sandbox: sandbox,
              });

              const stepSandbox =
                prepareStepResult?.experimental_sandbox ?? sandbox;

              const stepModel = resolveLanguageModel(
                prepareStepResult?.model ?? model,
              );

              const stepInstructions =
                prepareStepResult?.instructions ??
                prepareStepResult?.system ??
                instructionsForNextStep;

              runtimeContext =
                prepareStepResult?.runtimeContext ?? runtimeContext;
              toolsContext = prepareStepResult?.toolsContext ?? toolsContext;

              const stepActiveTools = filterActiveTools({
                tools,
                activeTools: prepareStepResult?.activeTools ?? activeTools,
              });
              const {
                executionTools,
                modelTools: stepModelTools,
                toolCallerMessages,
              } = prepareToolsForToolCallers({
                tools: prepareToolSearch(stepActiveTools, {
                  toolsContext,
                  experimental_sandbox: stepSandbox,
                }),
                toolCallers: resolvedToolCallers,
              });
              const stepToolOrder = prepareStepResult?.toolOrder ?? toolOrder;

              const stepTools = await prepareTools({
                tools: stepModelTools as ActiveToolSubset<
                  TOOLS,
                  ActiveTools<NoInfer<TOOLS>>
                >,
                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 stepExecutionTools =
                stepToolChoice.type === 'none' ? undefined : executionTools;

              const stepMessages = appendToolCallerMessages({
                messages: prepareStepResult?.messages ?? stepInputMessages,
                toolCallerMessages,
              });

              // steering messages are delivered when they are in the messages
              // that are sent to the model after prepareStep:
              const deliveredSteeringMessages = (
                steeringRun?.resolveTakenMessages({
                  stepNumber,
                  sentMessages: stepMessages,
                }) ?? []
              ).map(({ message }) => message);

              const promptMessages = await convertToLanguageModelPrompt({
                prompt: {
                  instructions: stepInstructions,
                  messages: stepMessages,
                },
                supportedUrls: await stepModel.supportedUrls,
                download,
                abortSignal: mergedAbortSignal,
                provider: stepModel.provider.split('.')[0],
              });

              const stepProviderOptions = mergeObjects(
                providerOptions,
                prepareStepResult?.providerOptions,
              );

              const stepCallSettings = prepareStepCallSettings({
                callSettings,
                stepSettings: prepareStepResult,
              });

              const stepStartEvent = {
                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,
              };

              await notify({
                event: stepStartEvent,
                callbacks: [
                  resolvedOnStepStart,
                  telemetryDispatcher.onStepStart,
                ],
              });

              openStep = stepStartEvent;

              const languageModelCallContext = {
                provider: stepModel.provider,
                modelId: stepModel.modelId,
                instructions: stepInstructions,
                messages: stepMessages,
                tools: stepTools,
                ...stepCallSettings,
              };
              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 () => {
                      const result = await stepModel.doGenerate({
                        ...stepCallSettings,
                        tools: stepTools,
                        toolChoice: stepToolChoice,
                        responseFormat: await output?.responseFormat,
                        prompt: promptMessages,
                        providerOptions: stepProviderOptions,
                        abortSignal: mergedAbortSignal,
                        headers: headersWithUserAgent,
                      });
                      return {
                        ...result,
                        ...(result.request && {
                          request: sanitizeRequestMetadata(result.request),
                        }),
                      };
                    },
                  },
                );

                return {
                  ...result,
                  response: {
                    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,
                  },
                };
              });
              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: stepModelTools as TOOLS,
                      // tool calls are rejected with tool choice none,
                      // so user callbacks must not process them:
                      repairToolCall:
                        stepToolChoice.type === 'none'
                          ? undefined
                          : repairToolCall,
                      refineToolInput:
                        stepToolChoice.type === 'none'
                          ? undefined
                          : refineToolInput,
                      instructions: stepInstructions,
                      messages: stepMessages,
                      abortSignal: mergedAbortSignal,
                    }),
                  ),
              );

              const toolApprovalRequests: Record<
                string,
                ToolApprovalRequestOutput<TOOLS>
              > = {};
              const stepToolApprovalResponses: Record<
                string,
                ToolApprovalResponseOutput<TOOLS>
              > = {};
              const blockedToolCallIds = new Set<string>();

              const generatedFileDataCache: Parameters<
                typeof convertLanguageModelContent
              >[0]['generatedFileDataCache'] = new WeakMap();
              const modelCallContent = await convertLanguageModelContent({
                content: currentModelResponse.content,
                toolCalls: stepToolCalls,
                toolOutputs: [],
                toolApprovalRequests: [],
                toolApprovalResponses: [],
                tools,
                download,
                abortSignal: mergedAbortSignal,
                generatedFileDataCache,
              });

              await notify({
                event: {
                  callId,
                  provider: stepModel.provider,
                  modelId: currentModelResponse.response.modelId,
                  finishReason: currentModelResponse.finishReason.unified,
                  usage: stepUsage,
                  content: modelCallContent,
                  responseId: currentModelResponse.response.id,
                  ...(currentModelResponse.request?.url != null
                    ? {
                        request: {
                          url: getRequestOrigin(
                            currentModelResponse.request.url,
                          ),
                        },
                      }
                    : {}),
                  ...(currentModelResponse.providerMetadata != null
                    ? {
                        providerMetadata: currentModelResponse.providerMetadata,
                      }
                    : {}),
                  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>,
                ],
              });

              const enforcedToolChoice =
                stepToolChoice.type === 'auto' ? undefined : stepToolChoice;

              if (
                enforcedToolChoice != null &&
                (enforcedToolChoice.type === 'none'
                  ? stepToolCalls.length > 0
                  : currentModelResponse.finishReason.unified !==
                      'interrupted' &&
                    !stepToolCalls.some(
                      toolCall =>
                        enforcedToolChoice.type === 'required' ||
                        toolCall.toolName === enforcedToolChoice.toolName,
                    ))
              ) {
                throw new ToolChoiceViolationError({
                  toolChoice: enforcedToolChoice,
                  finishReason: currentModelResponse.finishReason.unified,
                  provider: stepModel.provider,
                  modelId: stepModel.modelId,
                  content: currentModelResponse.content,
                });
              }

              // 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(stepExecutionTools, toolCall.toolName);

                if (tool == null) {
                  // ignore tool calls for tools that are not available,
                  // e.g. provider-executed dynamic tools
                  continue;
                }

                if (
                  tool.onInputStart != null ||
                  tool.onInputAvailable != null
                ) {
                  const context = await validateToolContext({
                    toolName: toolCall.toolName,
                    context: getOwn(toolsContext, toolCall.toolName),
                    contextSchema: tool.contextSchema,
                  });

                  if (tool.onInputStart != null) {
                    await tool.onInputStart({
                      toolCallId: toolCall.toolCallId,
                      messages: stepMessages,
                      abortSignal: mergedAbortSignal,
                      context,
                    });
                  }

                  if (tool.onInputAvailable != null) {
                    await tool.onInputAvailable({
                      input: toolCall.input,
                      toolCallId: toolCall.toolCallId,
                      messages: stepMessages,
                      abortSignal: mergedAbortSignal,
                      context,
                    });
                  }
                }

                const toolApprovalStatus = await resolveToolApproval({
                  tools: stepExecutionTools as 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,
                      ...(toolApprovalStatus.reason != null
                        ? { reason: toolApprovalStatus.reason }
                        : {}),
                      ...(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 &&
                  !toolCall.providerExecuted,
              );

              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 (
                stepExecutionTools != null &&
                isToolExecutionAllowedFinishReason(
                  currentModelResponse.finishReason.unified,
                )
              ) {
                const toolExecutionResults = await executeTools({
                  toolCalls: clientToolCalls.filter(
                    toolCall =>
                      !toolCall.invalid &&
                      !blockedToolCallIds.has(toolCall.toolCallId),
                  ),
                  tools: stepExecutionTools as 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,
                  toolCallConcurrency: resolvedToolCallConcurrency,
                });

                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(stepExecutionTools, 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 = await convertLanguageModelContent({
                content: currentModelResponse.content,
                toolCalls: stepToolCalls,
                toolOutputs: clientToolOutputs,
                toolApprovalRequests: Object.values(toolApprovalRequests),
                toolApprovalResponses,
                tools,
                download,
                abortSignal: mergedAbortSignal,
                generatedFileDataCache,
              });

              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,
                  toolsContext,
                  steeringMessages: deliveredSteeringMessages,
                });

              logWarnings({
                warnings: currentModelResponse.warnings ?? [],
                provider: stepModel.provider,
                model: stepModel.modelId,
              });

              steps.push(currentStepResult);
              instructionsForNextStep = stepInstructions;
              messagesForNextStep = [...stepMessages, ...stepResponseMessages];

              openStep = undefined;

              await notify({
                event: currentStepResult,
                callbacks: [resolvedOnStepEnd, telemetryDispatcher.onStepEnd],
              });

              return currentStepResult;
            },
          });
        } finally {
          if (stepTimeoutId != null) {
            clearTimeout(stepTimeoutId);
          }
        }
      } while (await shouldStartNextStep());

      steeringRun?.end(steeringEndReason);

      const lastStep = steps[steps.length - 1];

      const totalUsage = steps.reduce(
        (totalUsage, step) => {
          return addLanguageModelUsage(totalUsage, step.usage);
        },
        {
          inputTokens: undefined,
          inputTokenDetails: {
            noCacheTokens: undefined,
            cacheReadTokens: undefined,
            cacheWriteTokens: undefined,
          },
          outputTokens: undefined,
          outputTokenDetails: {
            textTokens: undefined,
            reasoningTokens: undefined,
          },
          totalTokens: undefined,
        } as LanguageModelUsage,
      );

      const files = steps.flatMap(step => step.files);
      const sources = steps.flatMap(step => step.sources);
      const toolCalls = steps.flatMap(step => step.toolCalls);
      const staticToolCalls = steps.flatMap(step => step.staticToolCalls);
      const dynamicToolCalls = steps.flatMap(step => step.dynamicToolCalls);
      const toolResults = steps.flatMap(step => step.toolResults);
      const staticToolResults = steps.flatMap(step => step.staticToolResults);
      const dynamicToolResults = steps.flatMap(step => step.dynamicToolResults);
      const warnings = steps.flatMap(step => step.warnings ?? []);

      const onEndEvent = {
        callId,
        stepNumber: lastStep.stepNumber,
        model: lastStep.model,
        runtimeContext: lastStep.runtimeContext,
        finishReason: lastStep.finishReason,
        rawFinishReason: lastStep.rawFinishReason,
        usage: totalUsage,
        totalUsage,
        content: steps.flatMap(step => step.content),
        text: lastStep.text,
        reasoning: lastStep.reasoning,
        reasoningText: lastStep.reasoningText,
        files,
        sources,
        toolCalls,
        staticToolCalls,
        dynamicToolCalls,
        toolResults,
        staticToolResults,
        dynamicToolResults,
        responseMessages: [
          ...initialResponseMessages,
          ...steps.flatMap(step => step.response.messages),
        ],
        warnings,
        request: lastStep.request,
        response: lastStep.response,
        providerMetadata: lastStep.providerMetadata,
        steps,
        finalStep: lastStep,
        toolsContext,
      };

      await notify({
        event: onEndEvent,
        callbacks: [onEnd, telemetryDispatcher.onEnd],
      });

      // parse output for stop responses and non-empty responses that are not
      // tool calls:
      let resolvedOutput;
      if (
        lastStep.finishReason === 'stop' ||
        (lastStep.finishReason !== 'tool-calls' && lastStep.text.length > 0)
      ) {
        const outputSpecification = output ?? text();
        // Select the structured output without changing the complete response
        // content, text, or messages exposed to callers.
        const textForOutput =
          (await outputSpecification.responseFormat)?.type === 'json'
            ? (currentModelResponse!.structuredOutputText ?? lastStep.text)
            : lastStep.text;
        resolvedOutput = await outputSpecification.parseCompleteOutput(
          { text: textForOutput },
          {
            response: lastStep.response,
            usage: lastStep.usage,
            finishReason: lastStep.finishReason,
          },
        );
      }

      return new DefaultGenerateTextResult({
        initialResponseMessages,
        steps,
        totalUsage,
        output: resolvedOutput,
      });
    } catch (error) {
      const wrappedError = wrapGatewayError(error);

      steeringRun?.end(mergedAbortSignal?.aborted ? 'aborted' : 'error');

      // generateText has no onAbort, so aborts and timeouts also end the step here:
      if (openStep != null) {
        await notify({
          event: { ...openStep, error: wrappedError },
          callbacks: [onStepError, telemetryDispatcher.onStepError],
        });
      }
      await telemetryDispatcher.onError?.({ callId, error });
      throw wrappedError;
    }
  };

  return await runInTracingChannelSpan({
    type: 'generateText',
    event: generateTextStartEvent,
    execute: executeGenerateText,
  });
}

async function executeTools<TOOLS extends ToolSet>({
  toolCalls,
  tools,
  callId,
  messages,
  abortSignal,
  timeout,
  experimental_sandbox: sandbox,
  toolsContext,
  onToolExecutionStart,
  onToolExecutionEnd,
  executeToolInTelemetryContext,
  runInTracingChannelSpan,
  toolCallConcurrency,
}: {
  toolCalls: Array<TypedToolCall<TOOLS>>;
  tools: TOOLS;
  callId: string;
  messages: ModelMessage[];
  abortSignal: AbortSignal | undefined;
  timeout?: TimeoutConfiguration<TOOLS>;
  experimental_sandbox?: SandboxSession;
  toolsContext: InferToolSetContext<TOOLS>;
  onToolExecutionStart?: OnToolExecutionStartCallback<TOOLS>;
  onToolExecutionEnd?: OnToolExecutionEndCallback<TOOLS>;
  executeToolInTelemetryContext?: Telemetry['executeTool'];
  runInTracingChannelSpan?: NonNullable<
    TelemetryDispatcher['runInTracingChannelSpan']
  >;
  toolCallConcurrency: number | undefined;
}): Promise<
  Array<{
    output: ToolOutput<TOOLS>;
    toolExecutionMs: number;
  }>
> {
  const toolResults = await mapWithConcurrency({
    items: toolCalls,
    concurrency: toolCallConcurrency,
    abortSignal,
    execute: async toolCall =>
      await executeToolCall({
        toolCall,
        tools,
        callId,
        messages,
        abortSignal,
        timeout,
        experimental_sandbox: sandbox,
        toolsContext,
        onToolExecutionStart,
        onToolExecutionEnd,
        executeToolInTelemetryContext,
        runInTracingChannelSpan,
      }),
  });

  return toolResults.filter(
    (result): result is NonNullable<typeof result> => result != null,
  );
}