@ai-sdk/openai 4.0.43 → 4.0.45

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": "4.0.43",
3
+ "version": "4.0.45",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "@ai-sdk/provider": "4.0.7",
39
- "@ai-sdk/provider-utils": "5.0.27"
39
+ "@ai-sdk/provider-utils": "5.0.28"
40
40
  },
41
41
  "devDependencies": {
42
42
  "@types/node": "22.19.19",
@@ -377,7 +377,7 @@ export class OpenAIChatLanguageModel implements LanguageModelV4 {
377
377
  for (const toolCall of choice.message.tool_calls ?? []) {
378
378
  content.push({
379
379
  type: 'tool-call' as const,
380
- toolCallId: toolCall.id ?? generateId(),
380
+ toolCallId: toolCall.id || generateId(),
381
381
  toolName: toolCall.function.name,
382
382
  input: toolCall.function.arguments!,
383
383
  });
@@ -1,6 +1,8 @@
1
1
  import {
2
2
  UnsupportedFunctionalityError,
3
3
  type LanguageModelV4Prompt,
4
+ type LanguageModelV4ToolResultOutput,
5
+ type LanguageModelV4ToolResultPart,
4
6
  type SharedV4ProviderOptions,
5
7
  type LanguageModelV4ToolApprovalResponsePart,
6
8
  type SharedV4Warning,
@@ -43,6 +45,10 @@ import {
43
45
  programmaticToolCallingInputSchema,
44
46
  programmaticToolCallingOutputSchema,
45
47
  } from '../tool/programmatic-tool-calling';
48
+ import {
49
+ getParallelToolCallMetadata,
50
+ type ParallelToolCallMetadata,
51
+ } from './expand-parallel-tool-call';
46
52
 
47
53
  function serializeToolCallArguments(input: unknown): string {
48
54
  return JSON.stringify(input === undefined ? {} : input);
@@ -61,6 +67,217 @@ function mapToolCaller(
61
67
  : caller;
62
68
  }
63
69
 
70
+ async function convertFunctionToolResultOutput({
71
+ output,
72
+ toolName,
73
+ outputSchemaToolNames,
74
+ providerOptionsName,
75
+ warnings,
76
+ }: {
77
+ output: LanguageModelV4ToolResultOutput;
78
+ toolName: string;
79
+ outputSchemaToolNames: Set<string> | undefined;
80
+ providerOptionsName: string;
81
+ warnings: Array<SharedV4Warning>;
82
+ }): Promise<OpenAIResponsesFunctionCallOutput['output']> {
83
+ // `output` is always a string, but for functions with output_schema OpenAI
84
+ // parses the contents of that string as JSON. Text-like results therefore
85
+ // need JSON.stringify to become valid JSON string literals.
86
+ const hasOutputSchema = outputSchemaToolNames?.has(toolName);
87
+
88
+ switch (output.type) {
89
+ case 'text':
90
+ case 'error-text':
91
+ return hasOutputSchema ? JSON.stringify(output.value) : output.value;
92
+ case 'execution-denied': {
93
+ const reason = output.reason ?? 'Tool call execution denied.';
94
+ return hasOutputSchema ? JSON.stringify(reason) : reason;
95
+ }
96
+ case 'json':
97
+ case 'error-json':
98
+ return JSON.stringify(output.value);
99
+ case 'content':
100
+ return output.value
101
+ .map(item => {
102
+ const promptCacheBreakpoint = getPromptCacheBreakpoint(
103
+ item.providerOptions,
104
+ providerOptionsName,
105
+ );
106
+ switch (item.type) {
107
+ case 'text': {
108
+ return {
109
+ type: 'input_text' as const,
110
+ text: item.text,
111
+ ...(promptCacheBreakpoint != null && {
112
+ prompt_cache_breakpoint: promptCacheBreakpoint,
113
+ }),
114
+ };
115
+ }
116
+
117
+ case 'file': {
118
+ const topLevel = getTopLevelMediaType(item.mediaType);
119
+ const imageDetail =
120
+ item.providerOptions?.[providerOptionsName]?.imageDetail;
121
+
122
+ if (item.data.type === 'data') {
123
+ const fullMediaType = resolveFullMediaType({ part: item });
124
+ if (topLevel === 'image') {
125
+ return {
126
+ type: 'input_image' as const,
127
+ image_url: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
128
+ detail: imageDetail,
129
+ ...(promptCacheBreakpoint != null && {
130
+ prompt_cache_breakpoint: promptCacheBreakpoint,
131
+ }),
132
+ };
133
+ }
134
+ return {
135
+ type: 'input_file' as const,
136
+ filename: item.filename ?? 'data',
137
+ file_data: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
138
+ ...(promptCacheBreakpoint != null && {
139
+ prompt_cache_breakpoint: promptCacheBreakpoint,
140
+ }),
141
+ };
142
+ }
143
+
144
+ if (item.data.type === 'url') {
145
+ if (topLevel === 'image') {
146
+ return {
147
+ type: 'input_image' as const,
148
+ image_url: item.data.url.toString(),
149
+ detail: imageDetail,
150
+ ...(promptCacheBreakpoint != null && {
151
+ prompt_cache_breakpoint: promptCacheBreakpoint,
152
+ }),
153
+ };
154
+ }
155
+ return {
156
+ type: 'input_file' as const,
157
+ file_url: item.data.url.toString(),
158
+ ...(promptCacheBreakpoint != null && {
159
+ prompt_cache_breakpoint: promptCacheBreakpoint,
160
+ }),
161
+ };
162
+ }
163
+
164
+ warnings.push({
165
+ type: 'other',
166
+ message: `unsupported tool content part type: ${item.type} with data type: ${item.data.type}`,
167
+ });
168
+ return undefined;
169
+ }
170
+
171
+ default: {
172
+ warnings.push({
173
+ type: 'other',
174
+ message: `unsupported tool content part type: ${item.type}`,
175
+ });
176
+ return undefined;
177
+ }
178
+ }
179
+ })
180
+ .filter(isNonNullable);
181
+ }
182
+ }
183
+
184
+ type ParallelToolResultGroup = {
185
+ metadata: ParallelToolCallMetadata;
186
+ results: Array<LanguageModelV4ToolResultPart>;
187
+ };
188
+
189
+ function hasSameParallelToolCall(
190
+ first: ParallelToolCallMetadata,
191
+ second: ParallelToolCallMetadata,
192
+ ): boolean {
193
+ return (
194
+ first.itemId === second.itemId &&
195
+ first.toolCallId === second.toolCallId &&
196
+ first.toolName === second.toolName &&
197
+ first.input === second.input &&
198
+ first.count === second.count
199
+ );
200
+ }
201
+
202
+ function collectCompleteParallelToolResultGroups({
203
+ prompt,
204
+ providerOptionsName,
205
+ }: {
206
+ prompt: LanguageModelV4Prompt;
207
+ providerOptionsName: string;
208
+ }): Map<string, ParallelToolResultGroup> {
209
+ const pendingGroups = new Map<
210
+ string,
211
+ {
212
+ metadata: ParallelToolCallMetadata;
213
+ results: Map<number, LanguageModelV4ToolResultPart>;
214
+ invalid: boolean;
215
+ }
216
+ >();
217
+
218
+ for (const message of prompt) {
219
+ if (message.role !== 'tool') {
220
+ continue;
221
+ }
222
+
223
+ for (const part of message.content) {
224
+ if (part.type !== 'tool-result') {
225
+ continue;
226
+ }
227
+
228
+ const metadata = getParallelToolCallMetadata({
229
+ providerOptions: part.providerOptions,
230
+ providerOptionsName,
231
+ });
232
+
233
+ if (metadata == null) {
234
+ continue;
235
+ }
236
+
237
+ const existing = pendingGroups.get(metadata.toolCallId);
238
+ if (existing == null) {
239
+ pendingGroups.set(metadata.toolCallId, {
240
+ metadata,
241
+ results: new Map([[metadata.index, part]]),
242
+ invalid: false,
243
+ });
244
+ continue;
245
+ }
246
+
247
+ if (
248
+ !hasSameParallelToolCall(existing.metadata, metadata) ||
249
+ existing.results.has(metadata.index)
250
+ ) {
251
+ existing.invalid = true;
252
+ continue;
253
+ }
254
+
255
+ existing.results.set(metadata.index, part);
256
+ }
257
+ }
258
+
259
+ const completeGroups = new Map<string, ParallelToolResultGroup>();
260
+
261
+ for (const [toolCallId, group] of pendingGroups) {
262
+ if (group.invalid || group.results.size !== group.metadata.count) {
263
+ continue;
264
+ }
265
+
266
+ const results = Array.from({ length: group.metadata.count }, (_, index) =>
267
+ group.results.get(index),
268
+ );
269
+
270
+ if (results.every(isNonNullable)) {
271
+ completeGroups.set(toolCallId, {
272
+ metadata: group.metadata,
273
+ results,
274
+ });
275
+ }
276
+ }
277
+
278
+ return completeGroups;
279
+ }
280
+
64
281
  type OpenAIPromptCacheBreakpoint = { mode: 'explicit' };
65
282
 
66
283
  function getPromptCacheBreakpoint(
@@ -123,6 +340,15 @@ export async function convertToOpenAIResponsesInput({
123
340
  let input: OpenAIResponsesInput = [];
124
341
  const warnings: Array<SharedV4Warning> = [];
125
342
  const processedApprovalIds = new Set<string>();
343
+ const parallelToolResultGroups =
344
+ hasConversation || hasPreviousResponseId
345
+ ? collectCompleteParallelToolResultGroups({
346
+ prompt,
347
+ providerOptionsName,
348
+ })
349
+ : new Map<string, ParallelToolResultGroup>();
350
+ const emittedParallelToolCalls = new Set<string>();
351
+ const emittedParallelToolResults = new Set<string>();
126
352
 
127
353
  for (const { role, content, providerOptions } of prompt) {
128
354
  switch (role) {
@@ -348,6 +574,49 @@ export async function convertToOpenAIResponsesInput({
348
574
  break;
349
575
  }
350
576
  case 'tool-call': {
577
+ const parallelToolCallMetadata = getParallelToolCallMetadata({
578
+ providerOptions: part.providerOptions,
579
+ providerOptionsName,
580
+ });
581
+ const parallelToolResultGroup =
582
+ parallelToolCallMetadata == null
583
+ ? undefined
584
+ : parallelToolResultGroups.get(
585
+ parallelToolCallMetadata.toolCallId,
586
+ );
587
+
588
+ if (
589
+ parallelToolCallMetadata != null &&
590
+ parallelToolResultGroup != null &&
591
+ hasSameParallelToolCall(
592
+ parallelToolResultGroup.metadata,
593
+ parallelToolCallMetadata,
594
+ )
595
+ ) {
596
+ if (
597
+ !emittedParallelToolCalls.has(
598
+ parallelToolResultGroup.metadata.toolCallId,
599
+ )
600
+ ) {
601
+ emittedParallelToolCalls.add(
602
+ parallelToolResultGroup.metadata.toolCallId,
603
+ );
604
+
605
+ // Conversations already contain the original wrapper item.
606
+ // previousResponseId chains require plain client function
607
+ // calls to be reconstructed in full.
608
+ if (!hasConversation) {
609
+ input.push({
610
+ type: 'function_call',
611
+ call_id: parallelToolResultGroup.metadata.toolCallId,
612
+ name: parallelToolResultGroup.metadata.toolName,
613
+ arguments: parallelToolResultGroup.metadata.input,
614
+ });
615
+ }
616
+ }
617
+ break;
618
+ }
619
+
351
620
  const id = (part.providerOptions?.[providerOptionsName]?.itemId ??
352
621
  (
353
622
  part as {
@@ -918,6 +1187,63 @@ export async function convertToOpenAIResponsesInput({
918
1187
  continue;
919
1188
  }
920
1189
 
1190
+ const parallelToolCallMetadata = getParallelToolCallMetadata({
1191
+ providerOptions: part.providerOptions,
1192
+ providerOptionsName,
1193
+ });
1194
+ const parallelToolResultGroup =
1195
+ parallelToolCallMetadata == null
1196
+ ? undefined
1197
+ : parallelToolResultGroups.get(
1198
+ parallelToolCallMetadata.toolCallId,
1199
+ );
1200
+
1201
+ if (
1202
+ parallelToolCallMetadata != null &&
1203
+ parallelToolResultGroup != null &&
1204
+ hasSameParallelToolCall(
1205
+ parallelToolResultGroup.metadata,
1206
+ parallelToolCallMetadata,
1207
+ )
1208
+ ) {
1209
+ if (
1210
+ !emittedParallelToolResults.has(
1211
+ parallelToolResultGroup.metadata.toolCallId,
1212
+ )
1213
+ ) {
1214
+ emittedParallelToolResults.add(
1215
+ parallelToolResultGroup.metadata.toolCallId,
1216
+ );
1217
+
1218
+ const toolOutputs = await Promise.all(
1219
+ parallelToolResultGroup.results.map(async result =>
1220
+ convertFunctionToolResultOutput({
1221
+ output: result.output,
1222
+ toolName: result.toolName,
1223
+ outputSchemaToolNames,
1224
+ providerOptionsName,
1225
+ warnings,
1226
+ }),
1227
+ ),
1228
+ );
1229
+
1230
+ input.push({
1231
+ type: 'function_call_output',
1232
+ call_id: parallelToolResultGroup.metadata.toolCallId,
1233
+ // The internal wrapper returns one output containing the child
1234
+ // results in the same order as the original tool_uses array.
1235
+ output: toolOutputs
1236
+ .map(output =>
1237
+ typeof output === 'string'
1238
+ ? output
1239
+ : JSON.stringify(output),
1240
+ )
1241
+ .join('\n'),
1242
+ });
1243
+ }
1244
+ continue;
1245
+ }
1246
+
921
1247
  const output = part.output;
922
1248
 
923
1249
  // Skip execution-denied with approvalId - already handled via tool-approval-response
@@ -1152,114 +1478,13 @@ export async function convertToOpenAIResponsesInput({
1152
1478
  continue;
1153
1479
  }
1154
1480
 
1155
- let contentValue: OpenAIResponsesFunctionCallOutput['output'];
1156
- // `output` is always a string, but for functions with output_schema
1157
- // OpenAI parses the contents of that string as JSON. Text-like results
1158
- // therefore need JSON.stringify to become valid JSON string literals.
1159
- const hasOutputSchema = outputSchemaToolNames?.has(part.toolName);
1160
- switch (output.type) {
1161
- case 'text':
1162
- case 'error-text':
1163
- contentValue = hasOutputSchema
1164
- ? JSON.stringify(output.value)
1165
- : output.value;
1166
- break;
1167
- case 'execution-denied': {
1168
- const reason = output.reason ?? 'Tool call execution denied.';
1169
- contentValue = hasOutputSchema ? JSON.stringify(reason) : reason;
1170
- break;
1171
- }
1172
- case 'json':
1173
- case 'error-json':
1174
- contentValue = JSON.stringify(output.value);
1175
- break;
1176
- case 'content':
1177
- contentValue = output.value
1178
- .map(item => {
1179
- const promptCacheBreakpoint = getPromptCacheBreakpoint(
1180
- item.providerOptions,
1181
- providerOptionsName,
1182
- );
1183
- switch (item.type) {
1184
- case 'text': {
1185
- return {
1186
- type: 'input_text' as const,
1187
- text: item.text,
1188
- ...(promptCacheBreakpoint != null && {
1189
- prompt_cache_breakpoint: promptCacheBreakpoint,
1190
- }),
1191
- };
1192
- }
1193
-
1194
- case 'file': {
1195
- const topLevel = getTopLevelMediaType(item.mediaType);
1196
- const imageDetail =
1197
- item.providerOptions?.[providerOptionsName]
1198
- ?.imageDetail;
1199
-
1200
- if (item.data.type === 'data') {
1201
- const fullMediaType = resolveFullMediaType({
1202
- part: item,
1203
- });
1204
- if (topLevel === 'image') {
1205
- return {
1206
- type: 'input_image' as const,
1207
- image_url: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
1208
- detail: imageDetail,
1209
- ...(promptCacheBreakpoint != null && {
1210
- prompt_cache_breakpoint: promptCacheBreakpoint,
1211
- }),
1212
- };
1213
- }
1214
- return {
1215
- type: 'input_file' as const,
1216
- filename: item.filename ?? 'data',
1217
- file_data: `data:${fullMediaType};base64,${convertToBase64(item.data.data)}`,
1218
- ...(promptCacheBreakpoint != null && {
1219
- prompt_cache_breakpoint: promptCacheBreakpoint,
1220
- }),
1221
- };
1222
- }
1223
-
1224
- if (item.data.type === 'url') {
1225
- if (topLevel === 'image') {
1226
- return {
1227
- type: 'input_image' as const,
1228
- image_url: item.data.url.toString(),
1229
- detail: imageDetail,
1230
- ...(promptCacheBreakpoint != null && {
1231
- prompt_cache_breakpoint: promptCacheBreakpoint,
1232
- }),
1233
- };
1234
- }
1235
- return {
1236
- type: 'input_file' as const,
1237
- file_url: item.data.url.toString(),
1238
- ...(promptCacheBreakpoint != null && {
1239
- prompt_cache_breakpoint: promptCacheBreakpoint,
1240
- }),
1241
- };
1242
- }
1243
-
1244
- warnings.push({
1245
- type: 'other',
1246
- message: `unsupported tool content part type: ${item.type} with data type: ${item.data.type}`,
1247
- });
1248
- return undefined;
1249
- }
1250
-
1251
- default: {
1252
- warnings.push({
1253
- type: 'other',
1254
- message: `unsupported tool content part type: ${item.type}`,
1255
- });
1256
- return undefined;
1257
- }
1258
- }
1259
- })
1260
- .filter(isNonNullable);
1261
- break;
1262
- }
1481
+ const contentValue = await convertFunctionToolResultOutput({
1482
+ output,
1483
+ toolName: part.toolName,
1484
+ outputSchemaToolNames,
1485
+ providerOptionsName,
1486
+ warnings,
1487
+ });
1263
1488
 
1264
1489
  const caller = mapToolCaller(
1265
1490
  part.providerOptions?.[providerOptionsName]?.caller as
@@ -0,0 +1,142 @@
1
+ import {
2
+ isJSONObject,
3
+ type LanguageModelV4FunctionTool,
4
+ type LanguageModelV4ToolCall,
5
+ type SharedV4ProviderOptions,
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: SharedV4ProviderOptions | 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<LanguageModelV4FunctionTool>;
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<LanguageModelV4ToolCall, 'toolCallId' | 'toolName' | 'input'>;
78
+ tools: Array<LanguageModelV4FunctionTool>;
79
+ providerOptionsName: string;
80
+ itemId: string;
81
+ }): Promise<Array<LanguageModelV4ToolCall> | 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<LanguageModelV4ToolCall> = [];
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
+ }