@ai-sdk/openai 3.0.98 → 3.0.100

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.98",
3
+ "version": "3.0.100",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -37,7 +37,7 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@ai-sdk/provider": "3.0.15",
40
- "@ai-sdk/provider-utils": "4.0.46"
40
+ "@ai-sdk/provider-utils": "4.0.47"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "20.17.24",
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  UnsupportedFunctionalityError,
3
3
  type LanguageModelV3Prompt,
4
+ type LanguageModelV3ToolResultOutput,
5
+ type LanguageModelV3ToolResultPart,
4
6
  type LanguageModelV3ToolApprovalResponsePart,
5
7
  type SharedV3ProviderOptions,
6
8
  type SharedV3Warning,
@@ -33,11 +35,206 @@ import type {
33
35
  OpenAIResponsesInput,
34
36
  OpenAIResponsesReasoning,
35
37
  } from './openai-responses-api';
38
+ import {
39
+ getParallelToolCallMetadata,
40
+ type ParallelToolCallMetadata,
41
+ } from './expand-parallel-tool-call';
36
42
 
37
43
  function serializeToolCallArguments(input: unknown): string {
38
44
  return JSON.stringify(input === undefined ? {} : input);
39
45
  }
40
46
 
47
+ async function convertFunctionToolResultOutput({
48
+ output,
49
+ providerOptionsName,
50
+ warnings,
51
+ }: {
52
+ output: LanguageModelV3ToolResultOutput;
53
+ providerOptionsName: string;
54
+ warnings: Array<SharedV3Warning>;
55
+ }): Promise<OpenAIResponsesFunctionCallOutput['output']> {
56
+ switch (output.type) {
57
+ case 'text':
58
+ case 'error-text':
59
+ return output.value;
60
+ case 'execution-denied':
61
+ return output.reason ?? 'Tool call execution denied.';
62
+ case 'json':
63
+ case 'error-json':
64
+ return JSON.stringify(output.value);
65
+ case 'content':
66
+ return output.value
67
+ .map(item => {
68
+ const promptCacheBreakpoint = getPromptCacheBreakpoint(
69
+ item.providerOptions,
70
+ providerOptionsName,
71
+ );
72
+ switch (item.type) {
73
+ case 'text': {
74
+ return {
75
+ type: 'input_text' as const,
76
+ text: item.text,
77
+ ...(promptCacheBreakpoint != null && {
78
+ prompt_cache_breakpoint: promptCacheBreakpoint,
79
+ }),
80
+ };
81
+ }
82
+
83
+ case 'image-data': {
84
+ return {
85
+ type: 'input_image' as const,
86
+ image_url: `data:${item.mediaType};base64,${item.data}`,
87
+ detail:
88
+ item.providerOptions?.[providerOptionsName]?.imageDetail,
89
+ ...(promptCacheBreakpoint != null && {
90
+ prompt_cache_breakpoint: promptCacheBreakpoint,
91
+ }),
92
+ };
93
+ }
94
+
95
+ case 'image-url': {
96
+ return {
97
+ type: 'input_image' as const,
98
+ image_url: item.url,
99
+ detail:
100
+ item.providerOptions?.[providerOptionsName]?.imageDetail,
101
+ ...(promptCacheBreakpoint != null && {
102
+ prompt_cache_breakpoint: promptCacheBreakpoint,
103
+ }),
104
+ };
105
+ }
106
+
107
+ case 'file-data': {
108
+ return {
109
+ type: 'input_file' as const,
110
+ filename: item.filename ?? 'data',
111
+ file_data: `data:${item.mediaType};base64,${item.data}`,
112
+ ...(promptCacheBreakpoint != null && {
113
+ prompt_cache_breakpoint: promptCacheBreakpoint,
114
+ }),
115
+ };
116
+ }
117
+
118
+ case 'file-url': {
119
+ return {
120
+ type: 'input_file' as const,
121
+ file_url: item.url,
122
+ ...(promptCacheBreakpoint != null && {
123
+ prompt_cache_breakpoint: promptCacheBreakpoint,
124
+ }),
125
+ };
126
+ }
127
+
128
+ default: {
129
+ warnings.push({
130
+ type: 'other',
131
+ message: `unsupported tool content part type: ${item.type}`,
132
+ });
133
+ return undefined;
134
+ }
135
+ }
136
+ })
137
+ .filter(isNonNullable);
138
+ }
139
+ }
140
+
141
+ type ParallelToolResultGroup = {
142
+ metadata: ParallelToolCallMetadata;
143
+ results: Array<LanguageModelV3ToolResultPart>;
144
+ };
145
+
146
+ function hasSameParallelToolCall(
147
+ first: ParallelToolCallMetadata,
148
+ second: ParallelToolCallMetadata,
149
+ ): boolean {
150
+ return (
151
+ first.itemId === second.itemId &&
152
+ first.toolCallId === second.toolCallId &&
153
+ first.toolName === second.toolName &&
154
+ first.input === second.input &&
155
+ first.count === second.count
156
+ );
157
+ }
158
+
159
+ function collectCompleteParallelToolResultGroups({
160
+ prompt,
161
+ providerOptionsName,
162
+ }: {
163
+ prompt: LanguageModelV3Prompt;
164
+ providerOptionsName: string;
165
+ }): Map<string, ParallelToolResultGroup> {
166
+ const pendingGroups = new Map<
167
+ string,
168
+ {
169
+ metadata: ParallelToolCallMetadata;
170
+ results: Map<number, LanguageModelV3ToolResultPart>;
171
+ invalid: boolean;
172
+ }
173
+ >();
174
+
175
+ for (const message of prompt) {
176
+ if (message.role !== 'tool') {
177
+ continue;
178
+ }
179
+
180
+ for (const part of message.content) {
181
+ if (part.type !== 'tool-result') {
182
+ continue;
183
+ }
184
+
185
+ const metadata = getParallelToolCallMetadata({
186
+ providerOptions: part.providerOptions,
187
+ providerOptionsName,
188
+ });
189
+
190
+ if (metadata == null) {
191
+ continue;
192
+ }
193
+
194
+ const existing = pendingGroups.get(metadata.toolCallId);
195
+ if (existing == null) {
196
+ pendingGroups.set(metadata.toolCallId, {
197
+ metadata,
198
+ results: new Map([[metadata.index, part]]),
199
+ invalid: false,
200
+ });
201
+ continue;
202
+ }
203
+
204
+ if (
205
+ !hasSameParallelToolCall(existing.metadata, metadata) ||
206
+ existing.results.has(metadata.index)
207
+ ) {
208
+ existing.invalid = true;
209
+ continue;
210
+ }
211
+
212
+ existing.results.set(metadata.index, part);
213
+ }
214
+ }
215
+
216
+ const completeGroups = new Map<string, ParallelToolResultGroup>();
217
+
218
+ for (const [toolCallId, group] of pendingGroups) {
219
+ if (group.invalid || group.results.size !== group.metadata.count) {
220
+ continue;
221
+ }
222
+
223
+ const results = Array.from({ length: group.metadata.count }, (_, index) =>
224
+ group.results.get(index),
225
+ );
226
+
227
+ if (results.every(isNonNullable)) {
228
+ completeGroups.set(toolCallId, {
229
+ metadata: group.metadata,
230
+ results,
231
+ });
232
+ }
233
+ }
234
+
235
+ return completeGroups;
236
+ }
237
+
41
238
  type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
42
239
 
43
240
  function getPromptCacheBreakpoint(
@@ -93,6 +290,15 @@ export async function convertToOpenAIResponsesInput({
93
290
  let input: OpenAIResponsesInput = [];
94
291
  const warnings: Array<SharedV3Warning> = [];
95
292
  const processedApprovalIds = new Set<string>();
293
+ const parallelToolResultGroups =
294
+ hasConversation || hasPreviousResponseId
295
+ ? collectCompleteParallelToolResultGroups({
296
+ prompt,
297
+ providerOptionsName,
298
+ })
299
+ : new Map<string, ParallelToolResultGroup>();
300
+ const emittedParallelToolCalls = new Set<string>();
301
+ const emittedParallelToolResults = new Set<string>();
96
302
 
97
303
  for (const { role, content, providerOptions } of prompt) {
98
304
  switch (role) {
@@ -279,6 +485,49 @@ export async function convertToOpenAIResponsesInput({
279
485
  break;
280
486
  }
281
487
  case 'tool-call': {
488
+ const parallelToolCallMetadata = getParallelToolCallMetadata({
489
+ providerOptions: part.providerOptions,
490
+ providerOptionsName,
491
+ });
492
+ const parallelToolResultGroup =
493
+ parallelToolCallMetadata == null
494
+ ? undefined
495
+ : parallelToolResultGroups.get(
496
+ parallelToolCallMetadata.toolCallId,
497
+ );
498
+
499
+ if (
500
+ parallelToolCallMetadata != null &&
501
+ parallelToolResultGroup != null &&
502
+ hasSameParallelToolCall(
503
+ parallelToolResultGroup.metadata,
504
+ parallelToolCallMetadata,
505
+ )
506
+ ) {
507
+ if (
508
+ !emittedParallelToolCalls.has(
509
+ parallelToolResultGroup.metadata.toolCallId,
510
+ )
511
+ ) {
512
+ emittedParallelToolCalls.add(
513
+ parallelToolResultGroup.metadata.toolCallId,
514
+ );
515
+
516
+ // Conversations already contain the original wrapper item.
517
+ // previousResponseId chains require plain client function
518
+ // calls to be reconstructed in full.
519
+ if (!hasConversation) {
520
+ input.push({
521
+ type: 'function_call',
522
+ call_id: parallelToolResultGroup.metadata.toolCallId,
523
+ name: parallelToolResultGroup.metadata.toolName,
524
+ arguments: parallelToolResultGroup.metadata.input,
525
+ });
526
+ }
527
+ }
528
+ break;
529
+ }
530
+
282
531
  const id = (part.providerOptions?.[providerOptionsName]?.itemId ??
283
532
  (
284
533
  part as {
@@ -709,6 +958,61 @@ export async function convertToOpenAIResponsesInput({
709
958
  continue;
710
959
  }
711
960
 
961
+ const parallelToolCallMetadata = getParallelToolCallMetadata({
962
+ providerOptions: part.providerOptions,
963
+ providerOptionsName,
964
+ });
965
+ const parallelToolResultGroup =
966
+ parallelToolCallMetadata == null
967
+ ? undefined
968
+ : parallelToolResultGroups.get(
969
+ parallelToolCallMetadata.toolCallId,
970
+ );
971
+
972
+ if (
973
+ parallelToolCallMetadata != null &&
974
+ parallelToolResultGroup != null &&
975
+ hasSameParallelToolCall(
976
+ parallelToolResultGroup.metadata,
977
+ parallelToolCallMetadata,
978
+ )
979
+ ) {
980
+ if (
981
+ !emittedParallelToolResults.has(
982
+ parallelToolResultGroup.metadata.toolCallId,
983
+ )
984
+ ) {
985
+ emittedParallelToolResults.add(
986
+ parallelToolResultGroup.metadata.toolCallId,
987
+ );
988
+
989
+ const toolOutputs = await Promise.all(
990
+ parallelToolResultGroup.results.map(async result =>
991
+ convertFunctionToolResultOutput({
992
+ output: result.output,
993
+ providerOptionsName,
994
+ warnings,
995
+ }),
996
+ ),
997
+ );
998
+
999
+ input.push({
1000
+ type: 'function_call_output',
1001
+ call_id: parallelToolResultGroup.metadata.toolCallId,
1002
+ // The internal wrapper returns one output containing the child
1003
+ // results in the same order as the original tool_uses array.
1004
+ output: toolOutputs
1005
+ .map(output =>
1006
+ typeof output === 'string'
1007
+ ? output
1008
+ : JSON.stringify(output),
1009
+ )
1010
+ .join('\n'),
1011
+ });
1012
+ }
1013
+ continue;
1014
+ }
1015
+
712
1016
  const output = part.output;
713
1017
 
714
1018
  // Skip execution-denied with approvalId - already handled via tool-approval-response
@@ -897,96 +1201,11 @@ export async function convertToOpenAIResponsesInput({
897
1201
  continue;
898
1202
  }
899
1203
 
900
- let contentValue: OpenAIResponsesFunctionCallOutput['output'];
901
- switch (output.type) {
902
- case 'text':
903
- case 'error-text':
904
- contentValue = output.value;
905
- break;
906
- case 'execution-denied':
907
- contentValue = output.reason ?? 'Tool call execution denied.';
908
- break;
909
- case 'json':
910
- case 'error-json':
911
- contentValue = JSON.stringify(output.value);
912
- break;
913
- case 'content':
914
- contentValue = output.value
915
- .map(item => {
916
- const promptCacheBreakpoint = getPromptCacheBreakpoint(
917
- item.providerOptions,
918
- providerOptionsName,
919
- );
920
- switch (item.type) {
921
- case 'text': {
922
- return {
923
- type: 'input_text' as const,
924
- text: item.text,
925
- ...(promptCacheBreakpoint != null && {
926
- prompt_cache_breakpoint: promptCacheBreakpoint,
927
- }),
928
- };
929
- }
930
-
931
- case 'image-data': {
932
- return {
933
- type: 'input_image' as const,
934
- image_url: `data:${item.mediaType};base64,${item.data}`,
935
- detail:
936
- item.providerOptions?.[providerOptionsName]
937
- ?.imageDetail,
938
- ...(promptCacheBreakpoint != null && {
939
- prompt_cache_breakpoint: promptCacheBreakpoint,
940
- }),
941
- };
942
- }
943
-
944
- case 'image-url': {
945
- return {
946
- type: 'input_image' as const,
947
- image_url: item.url,
948
- detail:
949
- item.providerOptions?.[providerOptionsName]
950
- ?.imageDetail,
951
- ...(promptCacheBreakpoint != null && {
952
- prompt_cache_breakpoint: promptCacheBreakpoint,
953
- }),
954
- };
955
- }
956
-
957
- case 'file-data': {
958
- return {
959
- type: 'input_file' as const,
960
- filename: item.filename ?? 'data',
961
- file_data: `data:${item.mediaType};base64,${item.data}`,
962
- ...(promptCacheBreakpoint != null && {
963
- prompt_cache_breakpoint: promptCacheBreakpoint,
964
- }),
965
- };
966
- }
967
-
968
- case 'file-url': {
969
- return {
970
- type: 'input_file' as const,
971
- file_url: item.url,
972
- ...(promptCacheBreakpoint != null && {
973
- prompt_cache_breakpoint: promptCacheBreakpoint,
974
- }),
975
- };
976
- }
977
-
978
- default: {
979
- warnings.push({
980
- type: 'other',
981
- message: `unsupported tool content part type: ${item.type}`,
982
- });
983
- return undefined;
984
- }
985
- }
986
- })
987
- .filter(isNonNullable);
988
- break;
989
- }
1204
+ const contentValue = await convertFunctionToolResultOutput({
1205
+ output,
1206
+ providerOptionsName,
1207
+ warnings,
1208
+ });
990
1209
 
991
1210
  input.push({
992
1211
  type: 'function_call_output',
@@ -0,0 +1,142 @@
1
+ import {
2
+ isJSONObject,
3
+ type LanguageModelV3FunctionTool,
4
+ type LanguageModelV3ToolCall,
5
+ type SharedV3ProviderOptions,
6
+ } from '@ai-sdk/provider';
7
+ import { safeParseJSON } from '@ai-sdk/provider-utils';
8
+
9
+ const parallelToolName = 'parallel';
10
+ const recipientNamePrefix = 'functions.';
11
+
12
+ /**
13
+ * Preserves the original wrapper identity so child results can be sent back as
14
+ * one function output when Responses API server-side state is used.
15
+ */
16
+ export type ParallelToolCallMetadata = {
17
+ itemId: string;
18
+ toolCallId: string;
19
+ toolName: string;
20
+ input: string;
21
+ index: number;
22
+ count: number;
23
+ };
24
+
25
+ export function getParallelToolCallMetadata({
26
+ providerOptions,
27
+ providerOptionsName,
28
+ }: {
29
+ providerOptions: SharedV3ProviderOptions | undefined;
30
+ providerOptionsName: string;
31
+ }): ParallelToolCallMetadata | undefined {
32
+ const metadata = providerOptions?.[providerOptionsName]?.parallelToolCall;
33
+
34
+ if (
35
+ !isJSONObject(metadata) ||
36
+ typeof metadata.itemId !== 'string' ||
37
+ typeof metadata.toolCallId !== 'string' ||
38
+ typeof metadata.toolName !== 'string' ||
39
+ typeof metadata.input !== 'string' ||
40
+ typeof metadata.index !== 'number' ||
41
+ !Number.isInteger(metadata.index) ||
42
+ typeof metadata.count !== 'number' ||
43
+ !Number.isInteger(metadata.count) ||
44
+ metadata.index < 0 ||
45
+ metadata.count <= metadata.index
46
+ ) {
47
+ return undefined;
48
+ }
49
+
50
+ return metadata as ParallelToolCallMetadata;
51
+ }
52
+
53
+ export function isUndeclaredParallelToolCall({
54
+ toolName,
55
+ tools,
56
+ }: {
57
+ toolName: string;
58
+ tools: Array<LanguageModelV3FunctionTool>;
59
+ }): boolean {
60
+ return (
61
+ toolName === parallelToolName &&
62
+ !tools.some(tool => tool.name === parallelToolName)
63
+ );
64
+ }
65
+
66
+ /**
67
+ * Expands the internal parallel tool wrapper that OpenAI models can emit as a
68
+ * regular function call. The wrapper is only recognized when every nested
69
+ * recipient is a declared client-side function tool.
70
+ */
71
+ export async function expandParallelToolCall({
72
+ toolCall,
73
+ tools,
74
+ providerOptionsName,
75
+ itemId,
76
+ }: {
77
+ toolCall: Pick<LanguageModelV3ToolCall, 'toolCallId' | 'toolName' | 'input'>;
78
+ tools: Array<LanguageModelV3FunctionTool>;
79
+ providerOptionsName: string;
80
+ itemId: string;
81
+ }): Promise<Array<LanguageModelV3ToolCall> | undefined> {
82
+ if (!isUndeclaredParallelToolCall({ toolName: toolCall.toolName, tools })) {
83
+ return undefined;
84
+ }
85
+
86
+ const parsedInput = await safeParseJSON({ text: toolCall.input });
87
+
88
+ if (!parsedInput.success || !isJSONObject(parsedInput.value)) {
89
+ return undefined;
90
+ }
91
+
92
+ const toolUses = parsedInput.value.tool_uses;
93
+ if (!Array.isArray(toolUses) || toolUses.length === 0) {
94
+ return undefined;
95
+ }
96
+
97
+ const availableToolNames = new Set(tools.map(tool => tool.name));
98
+ const expandedToolCalls: Array<LanguageModelV3ToolCall> = [];
99
+
100
+ for (const [index, toolUse] of toolUses.entries()) {
101
+ if (!isJSONObject(toolUse)) {
102
+ return undefined;
103
+ }
104
+
105
+ const recipientName = toolUse.recipient_name;
106
+ const parameters = toolUse.parameters;
107
+
108
+ if (
109
+ typeof recipientName !== 'string' ||
110
+ !recipientName.startsWith(recipientNamePrefix) ||
111
+ !isJSONObject(parameters)
112
+ ) {
113
+ return undefined;
114
+ }
115
+
116
+ const toolName = recipientName.slice(recipientNamePrefix.length);
117
+ if (toolName.length === 0 || !availableToolNames.has(toolName)) {
118
+ return undefined;
119
+ }
120
+
121
+ expandedToolCalls.push({
122
+ type: 'tool-call',
123
+ toolCallId: `${toolCall.toolCallId}_${index}`,
124
+ toolName,
125
+ input: JSON.stringify(parameters),
126
+ providerMetadata: {
127
+ [providerOptionsName]: {
128
+ parallelToolCall: {
129
+ itemId,
130
+ toolCallId: toolCall.toolCallId,
131
+ toolName: toolCall.toolName,
132
+ input: toolCall.input,
133
+ index,
134
+ count: toolUses.length,
135
+ } satisfies ParallelToolCallMetadata,
136
+ },
137
+ },
138
+ });
139
+ }
140
+
141
+ return expandedToolCalls;
142
+ }