UNPKG

@ai-sdk/openai

Version:

The **[OpenAI provider](https://ai-sdk.dev/providers/ai-sdk-providers/openai)** for the [AI SDK](https://ai-sdk.dev/docs) contains language model support for the OpenAI chat and completion APIs and embedding model support for the OpenAI embeddings API.

1,471 lines (1,333 loc) 74.6 kB
import { APICallError, JSONValue, LanguageModelV3, LanguageModelV3Prompt, LanguageModelV3CallOptions, LanguageModelV3Content, LanguageModelV3FinishReason, LanguageModelV3GenerateResult, LanguageModelV3ProviderTool, LanguageModelV3StreamPart, LanguageModelV3StreamResult, LanguageModelV3ToolApprovalRequest, SharedV3ProviderMetadata, SharedV3Warning, } from '@ai-sdk/provider'; import { combineHeaders, createEventSourceResponseHandler, createJsonResponseHandler, createToolNameMapping, generateId, InferSchema, parseProviderOptions, ParseResult, postJsonToApi, } from '@ai-sdk/provider-utils'; import { OpenAIConfig } from '../openai-config'; import { openaiFailedResponseHandler } from '../openai-error'; import { getOpenAILanguageModelCapabilities } from '../openai-language-model-capabilities'; import { applyPatchInputSchema } from '../tool/apply-patch'; import { codeInterpreterInputSchema, codeInterpreterOutputSchema, } from '../tool/code-interpreter'; import { fileSearchOutputSchema } from '../tool/file-search'; import { imageGenerationOutputSchema } from '../tool/image-generation'; import { localShellInputSchema } from '../tool/local-shell'; import { mcpOutputSchema } from '../tool/mcp'; import { shellInputSchema, shellOutputSchema } from '../tool/shell'; import { webSearchOutputSchema } from '../tool/web-search'; import { convertOpenAIResponsesUsage, OpenAIResponsesUsage, } from './convert-openai-responses-usage'; import { convertToOpenAIResponsesInput } from './convert-to-openai-responses-input'; import { mapOpenAIResponseFinishReason } from './map-openai-responses-finish-reason'; import { OpenAIResponsesChunk, openaiResponsesChunkSchema, OpenAIResponsesIncludeOptions, OpenAIResponsesIncludeValue, OpenAIResponsesLogprobs, openaiResponsesResponseSchema, OpenAIResponsesWebSearchAction, OpenAIResponsesApplyPatchOperationDiffDeltaChunk, OpenAIResponsesApplyPatchOperationDiffDoneChunk, } from './openai-responses-api'; import { OpenAIResponsesModelId, openaiLanguageModelResponsesOptionsSchema, TOP_LOGPROBS_MAX, } from './openai-responses-options'; import { prepareResponsesTools } from './openai-responses-prepare-tools'; import { ResponsesProviderMetadata, ResponsesReasoningProviderMetadata, ResponsesSourceDocumentProviderMetadata, ResponsesTextProviderMetadata, } from './openai-responses-provider-metadata'; /** * Extracts a mapping from MCP approval request IDs to their corresponding tool call IDs * from the prompt. When an MCP tool requires approval, we generate a tool call ID to track * the pending approval in our system. When the user responds to the approval (and we * continue the conversation), we need to map the approval request ID back to our tool call ID * so that tool results reference the correct tool call. */ function extractApprovalRequestIdToToolCallIdMapping( prompt: LanguageModelV3Prompt, ): Record<string, string> { const mapping: Record<string, string> = {}; for (const message of prompt) { if (message.role !== 'assistant') continue; for (const part of message.content) { if (part.type !== 'tool-call') continue; const approvalRequestId = part.providerOptions?.openai ?.approvalRequestId as string | undefined; if (approvalRequestId != null) { mapping[approvalRequestId] = part.toolCallId; } } } return mapping; } export class OpenAIResponsesLanguageModel implements LanguageModelV3 { readonly specificationVersion = 'v3'; readonly modelId: OpenAIResponsesModelId; private readonly config: OpenAIConfig; constructor(modelId: OpenAIResponsesModelId, config: OpenAIConfig) { this.modelId = modelId; this.config = config; } readonly supportedUrls: Record<string, RegExp[]> = { 'image/*': [/^https?:\/\/.*$/], 'application/pdf': [/^https?:\/\/.*$/], }; get provider(): string { return this.config.provider; } private async getArgs({ maxOutputTokens, temperature, stopSequences, topP, topK, presencePenalty, frequencyPenalty, seed, prompt, providerOptions, tools, toolChoice, responseFormat, }: LanguageModelV3CallOptions) { const warnings: SharedV3Warning[] = []; const modelCapabilities = getOpenAILanguageModelCapabilities(this.modelId); if (topK != null) { warnings.push({ type: 'unsupported', feature: 'topK' }); } if (seed != null) { warnings.push({ type: 'unsupported', feature: 'seed' }); } if (presencePenalty != null) { warnings.push({ type: 'unsupported', feature: 'presencePenalty' }); } if (frequencyPenalty != null) { warnings.push({ type: 'unsupported', feature: 'frequencyPenalty' }); } if (stopSequences != null) { warnings.push({ type: 'unsupported', feature: 'stopSequences' }); } const providerOptionsName = this.config.provider.includes('azure') ? 'azure' : 'openai'; let openaiOptions = await parseProviderOptions({ provider: providerOptionsName, providerOptions, schema: openaiLanguageModelResponsesOptionsSchema, }); if (openaiOptions == null && providerOptionsName !== 'openai') { openaiOptions = await parseProviderOptions({ provider: 'openai', providerOptions, schema: openaiLanguageModelResponsesOptionsSchema, }); } const isReasoningModel = openaiOptions?.forceReasoning ?? modelCapabilities.isReasoningModel; if (openaiOptions?.conversation && openaiOptions?.previousResponseId) { warnings.push({ type: 'unsupported', feature: 'conversation', details: 'conversation and previousResponseId cannot be used together', }); } const toolNameMapping = createToolNameMapping({ tools, providerToolNames: { 'openai.code_interpreter': 'code_interpreter', 'openai.file_search': 'file_search', 'openai.image_generation': 'image_generation', 'openai.local_shell': 'local_shell', 'openai.shell': 'shell', 'openai.web_search': 'web_search', 'openai.web_search_preview': 'web_search_preview', 'openai.mcp': 'mcp', 'openai.apply_patch': 'apply_patch', }, resolveProviderToolName: tool => tool.id === 'openai.custom' ? (tool.args as { name?: string }).name : undefined, }); const customProviderToolNames = new Set<string>(); const { tools: openaiTools, toolChoice: openaiToolChoice, toolWarnings, } = await prepareResponsesTools({ tools, toolChoice, toolNameMapping, customProviderToolNames, }); const { input, warnings: inputWarnings } = await convertToOpenAIResponsesInput({ prompt, toolNameMapping, systemMessageMode: openaiOptions?.systemMessageMode ?? (isReasoningModel ? 'developer' : modelCapabilities.systemMessageMode), providerOptionsName, fileIdPrefixes: this.config.fileIdPrefixes, store: openaiOptions?.store ?? true, hasConversation: openaiOptions?.conversation != null, hasLocalShellTool: hasOpenAITool('openai.local_shell'), hasShellTool: hasOpenAITool('openai.shell'), hasApplyPatchTool: hasOpenAITool('openai.apply_patch'), customProviderToolNames: customProviderToolNames.size > 0 ? customProviderToolNames : undefined, }); warnings.push(...inputWarnings); const strictJsonSchema = openaiOptions?.strictJsonSchema ?? true; let include: OpenAIResponsesIncludeOptions = openaiOptions?.include; function addInclude(key: OpenAIResponsesIncludeValue) { if (include == null) { include = [key]; } else if (!include.includes(key)) { include = [...include, key]; } } function hasOpenAITool(id: string) { return ( tools?.find(tool => tool.type === 'provider' && tool.id === id) != null ); } // when logprobs are requested, automatically include them: const topLogprobs = typeof openaiOptions?.logprobs === 'number' ? openaiOptions?.logprobs : openaiOptions?.logprobs === true ? TOP_LOGPROBS_MAX : undefined; if (topLogprobs) { addInclude('message.output_text.logprobs'); } // when a web search tool is present, automatically include the sources: const webSearchToolName = ( tools?.find( tool => tool.type === 'provider' && (tool.id === 'openai.web_search' || tool.id === 'openai.web_search_preview'), ) as LanguageModelV3ProviderTool | undefined )?.name; if (webSearchToolName) { addInclude('web_search_call.action.sources'); } // when a code interpreter tool is present, automatically include the outputs: if (hasOpenAITool('openai.code_interpreter')) { addInclude('code_interpreter_call.outputs'); } const store = openaiOptions?.store; // store defaults to true in the OpenAI responses API, so check for false exactly: if (store === false && isReasoningModel) { addInclude('reasoning.encrypted_content'); } const baseArgs = { model: this.modelId, input, temperature, top_p: topP, max_output_tokens: maxOutputTokens, ...((responseFormat?.type === 'json' || openaiOptions?.textVerbosity) && { text: { ...(responseFormat?.type === 'json' && { format: responseFormat.schema != null ? { type: 'json_schema', strict: strictJsonSchema, name: responseFormat.name ?? 'response', description: responseFormat.description, schema: responseFormat.schema, } : { type: 'json_object' }, }), ...(openaiOptions?.textVerbosity && { verbosity: openaiOptions.textVerbosity, }), }, }), // provider options: conversation: openaiOptions?.conversation, max_tool_calls: openaiOptions?.maxToolCalls, metadata: openaiOptions?.metadata, parallel_tool_calls: openaiOptions?.parallelToolCalls, previous_response_id: openaiOptions?.previousResponseId, store, user: openaiOptions?.user, instructions: openaiOptions?.instructions, service_tier: openaiOptions?.serviceTier, include, prompt_cache_key: openaiOptions?.promptCacheKey, prompt_cache_retention: openaiOptions?.promptCacheRetention, safety_identifier: openaiOptions?.safetyIdentifier, top_logprobs: topLogprobs, truncation: openaiOptions?.truncation, // model-specific settings: ...(isReasoningModel && (openaiOptions?.reasoningEffort != null || openaiOptions?.reasoningSummary != null) && { reasoning: { ...(openaiOptions?.reasoningEffort != null && { effort: openaiOptions.reasoningEffort, }), ...(openaiOptions?.reasoningSummary != null && { summary: openaiOptions.reasoningSummary, }), }, }), }; // remove unsupported settings for reasoning models // see https://platform.openai.com/docs/guides/reasoning#limitations if (isReasoningModel) { // when reasoning effort is none, gpt-5.1 models allow temperature, topP, logprobs // https://platform.openai.com/docs/guides/latest-model#gpt-5-1-parameter-compatibility if ( !( openaiOptions?.reasoningEffort === 'none' && modelCapabilities.supportsNonReasoningParameters ) ) { if (baseArgs.temperature != null) { baseArgs.temperature = undefined; warnings.push({ type: 'unsupported', feature: 'temperature', details: 'temperature is not supported for reasoning models', }); } if (baseArgs.top_p != null) { baseArgs.top_p = undefined; warnings.push({ type: 'unsupported', feature: 'topP', details: 'topP is not supported for reasoning models', }); } } } else { if (openaiOptions?.reasoningEffort != null) { warnings.push({ type: 'unsupported', feature: 'reasoningEffort', details: 'reasoningEffort is not supported for non-reasoning models', }); } if (openaiOptions?.reasoningSummary != null) { warnings.push({ type: 'unsupported', feature: 'reasoningSummary', details: 'reasoningSummary is not supported for non-reasoning models', }); } } // Validate flex processing support if ( openaiOptions?.serviceTier === 'flex' && !modelCapabilities.supportsFlexProcessing ) { warnings.push({ type: 'unsupported', feature: 'serviceTier', details: 'flex processing is only available for o3, o4-mini, and gpt-5 models', }); // Remove from args if not supported delete (baseArgs as any).service_tier; } // Validate priority processing support if ( openaiOptions?.serviceTier === 'priority' && !modelCapabilities.supportsPriorityProcessing ) { warnings.push({ type: 'unsupported', feature: 'serviceTier', details: 'priority processing is only available for supported models (gpt-4, gpt-5, gpt-5-mini, o3, o4-mini) and requires Enterprise access. gpt-5-nano is not supported', }); // Remove from args if not supported delete (baseArgs as any).service_tier; } const shellToolEnvType = ( tools?.find( tool => tool.type === 'provider' && tool.id === 'openai.shell', ) as { args?: { environment?: { type?: string } } } | undefined )?.args?.environment?.type; const isShellProviderExecuted = shellToolEnvType === 'containerAuto' || shellToolEnvType === 'containerReference'; return { webSearchToolName, args: { ...baseArgs, tools: openaiTools, tool_choice: openaiToolChoice, }, warnings: [...warnings, ...toolWarnings], store, toolNameMapping, providerOptionsName, isShellProviderExecuted, }; } async doGenerate( options: LanguageModelV3CallOptions, ): Promise<LanguageModelV3GenerateResult> { const { args: body, warnings, webSearchToolName, toolNameMapping, providerOptionsName, isShellProviderExecuted, } = await this.getArgs(options); const url = this.config.url({ path: '/responses', modelId: this.modelId, }); const approvalRequestIdToDummyToolCallIdFromPrompt = extractApprovalRequestIdToToolCallIdMapping(options.prompt); const { responseHeaders, value: response, rawValue: rawResponse, } = await postJsonToApi({ url, headers: combineHeaders(this.config.headers(), options.headers), body, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createJsonResponseHandler( openaiResponsesResponseSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); if (response.error) { throw new APICallError({ message: response.error.message, url, requestBodyValues: body, statusCode: 400, responseHeaders, responseBody: rawResponse as string, isRetryable: false, }); } const content: Array<LanguageModelV3Content> = []; const logprobs: Array<OpenAIResponsesLogprobs> = []; // flag that checks if there have been client-side tool calls (not executed by openai) let hasFunctionCall = false; // map response content to content array (defined when there is no error) for (const part of response.output!) { switch (part.type) { case 'reasoning': { // when there are no summary parts, we need to add an empty reasoning part: if (part.summary.length === 0) { part.summary.push({ type: 'summary_text', text: '' }); } for (const summary of part.summary) { content.push({ type: 'reasoning' as const, text: summary.text, providerMetadata: { [providerOptionsName]: { itemId: part.id, reasoningEncryptedContent: part.encrypted_content ?? null, } satisfies ResponsesReasoningProviderMetadata, }, }); } break; } case 'image_generation_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('image_generation'), input: '{}', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('image_generation'), result: { result: part.result, } satisfies InferSchema<typeof imageGenerationOutputSchema>, }); break; } case 'local_shell_call': { content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: toolNameMapping.toCustomToolName('local_shell'), input: JSON.stringify({ action: part.action, } satisfies InferSchema<typeof localShellInputSchema>), providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } case 'shell_call': { content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: toolNameMapping.toCustomToolName('shell'), input: JSON.stringify({ action: { commands: part.action.commands, }, } satisfies InferSchema<typeof shellInputSchema>), ...(isShellProviderExecuted && { providerExecuted: true }), providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } case 'shell_call_output': { content.push({ type: 'tool-result', toolCallId: part.call_id, toolName: toolNameMapping.toCustomToolName('shell'), result: { output: part.output.map(item => ({ stdout: item.stdout, stderr: item.stderr, outcome: item.outcome.type === 'exit' ? { type: 'exit' as const, exitCode: item.outcome.exit_code, } : { type: 'timeout' as const }, })), } satisfies InferSchema<typeof shellOutputSchema>, }); break; } case 'message': { for (const contentPart of part.content) { if ( options.providerOptions?.[providerOptionsName]?.logprobs && contentPart.logprobs ) { logprobs.push(contentPart.logprobs); } const providerMetadata: SharedV3ProviderMetadata[string] = { itemId: part.id, ...(part.phase != null && { phase: part.phase }), ...(contentPart.annotations.length > 0 && { annotations: contentPart.annotations, }), } satisfies ResponsesTextProviderMetadata; content.push({ type: 'text', text: contentPart.text, providerMetadata: { [providerOptionsName]: providerMetadata, }, }); for (const annotation of contentPart.annotations) { if (annotation.type === 'url_citation') { content.push({ type: 'source', sourceType: 'url', id: this.config.generateId?.() ?? generateId(), url: annotation.url, title: annotation.title, }); } else if (annotation.type === 'file_citation') { content.push({ type: 'source', sourceType: 'document', id: this.config.generateId?.() ?? generateId(), mediaType: 'text/plain', title: annotation.filename, filename: annotation.filename, providerMetadata: { [providerOptionsName]: { type: annotation.type, fileId: annotation.file_id, index: annotation.index, } satisfies Extract< ResponsesSourceDocumentProviderMetadata, { type: 'file_citation' } >, }, }); } else if (annotation.type === 'container_file_citation') { content.push({ type: 'source', sourceType: 'document', id: this.config.generateId?.() ?? generateId(), mediaType: 'text/plain', title: annotation.filename, filename: annotation.filename, providerMetadata: { [providerOptionsName]: { type: annotation.type, fileId: annotation.file_id, containerId: annotation.container_id, } satisfies Extract< ResponsesSourceDocumentProviderMetadata, { type: 'container_file_citation' } >, }, }); } else if (annotation.type === 'file_path') { content.push({ type: 'source', sourceType: 'document', id: this.config.generateId?.() ?? generateId(), mediaType: 'application/octet-stream', title: annotation.file_id, filename: annotation.file_id, providerMetadata: { [providerOptionsName]: { type: annotation.type, fileId: annotation.file_id, index: annotation.index, } satisfies Extract< ResponsesSourceDocumentProviderMetadata, { type: 'file_path' } >, }, }); } } } break; } case 'function_call': { hasFunctionCall = true; content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: part.name, input: part.arguments, providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } case 'custom_tool_call': { hasFunctionCall = true; const toolName = toolNameMapping.toCustomToolName(part.name); content.push({ type: 'tool-call', toolCallId: part.call_id, toolName, input: JSON.stringify(part.input), providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } case 'web_search_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), input: JSON.stringify({}), providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), result: mapWebSearchOutput(part.action), }); break; } case 'mcp_call': { const toolCallId = part.approval_request_id != null ? (approvalRequestIdToDummyToolCallIdFromPrompt[ part.approval_request_id ] ?? part.id) : part.id; const toolName = `mcp.${part.name}`; content.push({ type: 'tool-call', toolCallId, toolName, input: part.arguments, providerExecuted: true, dynamic: true, }); content.push({ type: 'tool-result', toolCallId, toolName, result: { type: 'call', serverLabel: part.server_label, name: part.name, arguments: part.arguments, ...(part.output != null ? { output: part.output } : {}), ...(part.error != null ? { error: part.error as unknown as JSONValue } : {}), } satisfies InferSchema<typeof mcpOutputSchema>, providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } case 'mcp_list_tools': { // skip break; } case 'mcp_approval_request': { const approvalRequestId = part.approval_request_id ?? part.id; const dummyToolCallId = this.config.generateId?.() ?? generateId(); const toolName = `mcp.${part.name}`; content.push({ type: 'tool-call', toolCallId: dummyToolCallId, toolName, input: part.arguments, providerExecuted: true, dynamic: true, }); content.push({ type: 'tool-approval-request', approvalId: approvalRequestId, toolCallId: dummyToolCallId, } satisfies LanguageModelV3ToolApprovalRequest); break; } case 'computer_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('computer_use'), input: '', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('computer_use'), result: { type: 'computer_use_tool_result', status: part.status || 'completed', }, }); break; } case 'file_search_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('file_search'), input: '{}', providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('file_search'), result: { queries: part.queries, results: part.results?.map(result => ({ attributes: result.attributes, fileId: result.file_id, filename: result.filename, score: result.score, text: result.text, })) ?? null, } satisfies InferSchema<typeof fileSearchOutputSchema>, }); break; } case 'code_interpreter_call': { content.push({ type: 'tool-call', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('code_interpreter'), input: JSON.stringify({ code: part.code, containerId: part.container_id, } satisfies InferSchema<typeof codeInterpreterInputSchema>), providerExecuted: true, }); content.push({ type: 'tool-result', toolCallId: part.id, toolName: toolNameMapping.toCustomToolName('code_interpreter'), result: { outputs: part.outputs, } satisfies InferSchema<typeof codeInterpreterOutputSchema>, }); break; } case 'apply_patch_call': { content.push({ type: 'tool-call', toolCallId: part.call_id, toolName: toolNameMapping.toCustomToolName('apply_patch'), input: JSON.stringify({ callId: part.call_id, operation: part.operation, } satisfies InferSchema<typeof applyPatchInputSchema>), providerMetadata: { [providerOptionsName]: { itemId: part.id, }, }, }); break; } } } const providerMetadata: SharedV3ProviderMetadata = { [providerOptionsName]: { responseId: response.id, ...(logprobs.length > 0 ? { logprobs } : {}), ...(typeof response.service_tier === 'string' ? { serviceTier: response.service_tier } : {}), } satisfies ResponsesProviderMetadata, }; const usage = response.usage!; // defined when there is no error return { content, finishReason: { unified: mapOpenAIResponseFinishReason({ finishReason: response.incomplete_details?.reason, hasFunctionCall, }), raw: response.incomplete_details?.reason ?? undefined, }, usage: convertOpenAIResponsesUsage(usage), request: { body }, response: { id: response.id, timestamp: new Date(response.created_at! * 1000), modelId: response.model, headers: responseHeaders, body: rawResponse, }, providerMetadata, warnings, }; } async doStream( options: LanguageModelV3CallOptions, ): Promise<LanguageModelV3StreamResult> { const { args: body, warnings, webSearchToolName, toolNameMapping, store, providerOptionsName, isShellProviderExecuted, } = await this.getArgs(options); const { responseHeaders, value: response } = await postJsonToApi({ url: this.config.url({ path: '/responses', modelId: this.modelId, }), headers: combineHeaders(this.config.headers(), options.headers), body: { ...body, stream: true, }, failedResponseHandler: openaiFailedResponseHandler, successfulResponseHandler: createEventSourceResponseHandler( openaiResponsesChunkSchema, ), abortSignal: options.abortSignal, fetch: this.config.fetch, }); const self = this; const approvalRequestIdToDummyToolCallIdFromPrompt = extractApprovalRequestIdToToolCallIdMapping(options.prompt); const approvalRequestIdToDummyToolCallIdFromStream = new Map< string, string >(); let finishReason: LanguageModelV3FinishReason = { unified: 'other', raw: undefined, }; let usage: OpenAIResponsesUsage | undefined = undefined; const logprobs: Array<OpenAIResponsesLogprobs> = []; let responseId: string | null = null; const ongoingToolCalls: Record< number, | { toolName: string; toolCallId: string; codeInterpreter?: { containerId: string; }; applyPatch?: { hasDiff: boolean; endEmitted: boolean; }; } | undefined > = {}; // set annotations in 'text-end' part providerMetadata. const ongoingAnnotations: Array< Extract< OpenAIResponsesChunk, { type: 'response.output_text.annotation.added' } >['annotation'] > = []; // track the phase of the current message being streamed let activeMessagePhase: 'commentary' | 'final_answer' | undefined; // flag that checks if there have been client-side tool calls (not executed by openai) let hasFunctionCall = false; const activeReasoning: Record< string, { encryptedContent?: string | null; // summary index as string to reasoning part state: summaryParts: Record<string, 'active' | 'can-conclude' | 'concluded'>; } > = {}; let serviceTier: string | undefined; return { stream: response.pipeThrough( new TransformStream< ParseResult<OpenAIResponsesChunk>, LanguageModelV3StreamPart >({ start(controller) { controller.enqueue({ type: 'stream-start', warnings }); }, transform(chunk, controller) { if (options.includeRawChunks) { controller.enqueue({ type: 'raw', rawValue: chunk.rawValue }); } // handle failed chunk parsing / validation: if (!chunk.success) { finishReason = { unified: 'error', raw: undefined }; controller.enqueue({ type: 'error', error: chunk.error }); return; } const value = chunk.value; if (isResponseOutputItemAddedChunk(value)) { if (value.item.type === 'function_call') { ongoingToolCalls[value.output_index] = { toolName: value.item.name, toolCallId: value.item.call_id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.call_id, toolName: value.item.name, }); } else if (value.item.type === 'custom_tool_call') { const toolName = toolNameMapping.toCustomToolName( value.item.name, ); ongoingToolCalls[value.output_index] = { toolName, toolCallId: value.item.call_id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.call_id, toolName, }); } else if (value.item.type === 'web_search_call') { ongoingToolCalls[value.output_index] = { toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), toolCallId: value.item.id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.id, toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), providerExecuted: true, }); controller.enqueue({ type: 'tool-input-end', id: value.item.id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), input: JSON.stringify({}), providerExecuted: true, }); } else if (value.item.type === 'computer_call') { ongoingToolCalls[value.output_index] = { toolName: toolNameMapping.toCustomToolName('computer_use'), toolCallId: value.item.id, }; controller.enqueue({ type: 'tool-input-start', id: value.item.id, toolName: toolNameMapping.toCustomToolName('computer_use'), providerExecuted: true, }); } else if (value.item.type === 'code_interpreter_call') { ongoingToolCalls[value.output_index] = { toolName: toolNameMapping.toCustomToolName('code_interpreter'), toolCallId: value.item.id, codeInterpreter: { containerId: value.item.container_id, }, }; controller.enqueue({ type: 'tool-input-start', id: value.item.id, toolName: toolNameMapping.toCustomToolName('code_interpreter'), providerExecuted: true, }); controller.enqueue({ type: 'tool-input-delta', id: value.item.id, delta: `{"containerId":"${value.item.container_id}","code":"`, }); } else if (value.item.type === 'file_search_call') { controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('file_search'), input: '{}', providerExecuted: true, }); } else if (value.item.type === 'image_generation_call') { controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('image_generation'), input: '{}', providerExecuted: true, }); } else if ( value.item.type === 'mcp_call' || value.item.type === 'mcp_list_tools' || value.item.type === 'mcp_approval_request' ) { // Emit MCP tool-call/approval parts on output_item.done instead, so we can: // - alias mcp_call IDs when an approval_request_id is present // - emit a proper tool-approval-request part for MCP approvals } else if (value.item.type === 'apply_patch_call') { const { call_id: callId, operation } = value.item; ongoingToolCalls[value.output_index] = { toolName: toolNameMapping.toCustomToolName('apply_patch'), toolCallId: callId, applyPatch: { // delete_file doesn't have diff hasDiff: operation.type === 'delete_file', endEmitted: operation.type === 'delete_file', }, }; controller.enqueue({ type: 'tool-input-start', id: callId, toolName: toolNameMapping.toCustomToolName('apply_patch'), }); if (operation.type === 'delete_file') { const inputString = JSON.stringify({ callId, operation, } satisfies InferSchema<typeof applyPatchInputSchema>); controller.enqueue({ type: 'tool-input-delta', id: callId, delta: inputString, }); controller.enqueue({ type: 'tool-input-end', id: callId, }); } else { controller.enqueue({ type: 'tool-input-delta', id: callId, delta: `{"callId":"${escapeJSONDelta(callId)}","operation":{"type":"${escapeJSONDelta(operation.type)}","path":"${escapeJSONDelta(operation.path)}","diff":"`, }); } } else if (value.item.type === 'shell_call') { ongoingToolCalls[value.output_index] = { toolName: toolNameMapping.toCustomToolName('shell'), toolCallId: value.item.call_id, }; } else if (value.item.type === 'shell_call_output') { // shell_call_output is handled in output_item.done } else if (value.item.type === 'message') { ongoingAnnotations.splice(0, ongoingAnnotations.length); activeMessagePhase = value.item.phase ?? undefined; controller.enqueue({ type: 'text-start', id: value.item.id, providerMetadata: { [providerOptionsName]: { itemId: value.item.id, ...(value.item.phase != null && { phase: value.item.phase, }), }, }, }); } else if ( isResponseOutputItemAddedChunk(value) && value.item.type === 'reasoning' ) { activeReasoning[value.item.id] = { encryptedContent: value.item.encrypted_content, summaryParts: { 0: 'active' }, }; controller.enqueue({ type: 'reasoning-start', id: `${value.item.id}:0`, providerMetadata: { [providerOptionsName]: { itemId: value.item.id, reasoningEncryptedContent: value.item.encrypted_content ?? null, } satisfies ResponsesReasoningProviderMetadata, }, }); } } else if (isResponseOutputItemDoneChunk(value)) { if (value.item.type === 'message') { const phase = value.item.phase ?? activeMessagePhase; activeMessagePhase = undefined; controller.enqueue({ type: 'text-end', id: value.item.id, providerMetadata: { [providerOptionsName]: { itemId: value.item.id, ...(phase != null && { phase }), ...(ongoingAnnotations.length > 0 && { annotations: ongoingAnnotations, }), } satisfies ResponsesTextProviderMetadata, }, }); } else if (value.item.type === 'function_call') { ongoingToolCalls[value.output_index] = undefined; hasFunctionCall = true; controller.enqueue({ type: 'tool-input-end', id: value.item.call_id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.call_id, toolName: value.item.name, input: value.item.arguments, providerMetadata: { [providerOptionsName]: { itemId: value.item.id, }, }, }); } else if (value.item.type === 'custom_tool_call') { ongoingToolCalls[value.output_index] = undefined; hasFunctionCall = true; const toolName = toolNameMapping.toCustomToolName( value.item.name, ); controller.enqueue({ type: 'tool-input-end', id: value.item.call_id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.call_id, toolName, input: JSON.stringify(value.item.input), providerMetadata: { [providerOptionsName]: { itemId: value.item.id, }, }, }); } else if (value.item.type === 'web_search_call') { ongoingToolCalls[value.output_index] = undefined; controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName( webSearchToolName ?? 'web_search', ), result: mapWebSearchOutput(value.item.action), }); } else if (value.item.type === 'computer_call') { ongoingToolCalls[value.output_index] = undefined; controller.enqueue({ type: 'tool-input-end', id: value.item.id, }); controller.enqueue({ type: 'tool-call', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('computer_use'), input: '', providerExecuted: true, }); controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('computer_use'), result: { type: 'computer_use_tool_result', status: value.item.status || 'completed', }, }); } else if (value.item.type === 'file_search_call') { ongoingToolCalls[value.output_index] = undefined; controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('file_search'), result: { queries: value.item.queries, results: value.item.results?.map(result => ({ attributes: result.attributes, fileId: result.file_id, filename: result.filename, score: result.score, text: result.text, })) ?? null, } satisfies InferSchema<typeof fileSearchOutputSchema>, }); } else if (value.item.type === 'code_interpreter_call') { ongoingToolCalls[value.output_index] = undefined; controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('code_interpreter'), result: { outputs: value.item.outputs, } satisfies InferSchema<typeof codeInterpreterOutputSchema>, }); } else if (value.item.type === 'image_generation_call') { controller.enqueue({ type: 'tool-result', toolCallId: value.item.id, toolName: toolNameMapping.toCustomToolName('image_generation'), result: { result: value.item.result, } satisfies InferSchema<typeof imageGenerationOutputSchema>, }); } else if (value.item.type === 'mcp_call') { ongoingToolCalls[value.output_index] = undefined; const approvalRequestId = value.item.approval_request_id ?? undefined; // when MCP tools require approval, we track them with our own // tool call IDs and then map OpenAI's approval_request_id back to our ID so results match. const aliasedToolCallId = approvalRequestId != null ? (approvalRequestIdToDummyToolCallIdFromStream.get( approvalRequestId, ) ?? approvalRequestIdToDummyToolCallIdFromPrompt[ approvalRequestId ] ?? value.item.id) : value.item.id; const toolName = `mcp.${value.item.name}`; controller.enqueue({ type: 'tool-call', toolCallId: aliasedToolCallId, toolName, input: value.item.arguments, providerExecuted: true, dynamic: true, }); controller.enqueue({ type: 'tool-result', toolCallId: aliasedToolCallId, toolName, result: { type: 'call', serverLabel: value.item.server_label, name: value.item.name, arguments: value.item.arguments, ...(value.item.output != null ? { output: value.item.output } : {}), ...(value.item.error != null ? { error: value.item.error as unknown as JSONValue } : {}), } satisfies InferSchema<typeof mcpOutputSchema>, providerMetadata: { [providerOptionsName]: { itemId: value.item.id, },