@ai-sdk/openai 3.0.97 → 3.0.99

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.
@@ -6,6 +6,7 @@ import {
6
6
  type LanguageModelV3CallOptions,
7
7
  type LanguageModelV3Content,
8
8
  type LanguageModelV3FinishReason,
9
+ type LanguageModelV3FunctionTool,
9
10
  type LanguageModelV3GenerateResult,
10
11
  type LanguageModelV3ProviderTool,
11
12
  type LanguageModelV3StreamPart,
@@ -49,6 +50,10 @@ import {
49
50
  type OpenAIResponsesUsage,
50
51
  } from './convert-openai-responses-usage';
51
52
  import { convertToOpenAIResponsesInput } from './convert-to-openai-responses-input';
53
+ import {
54
+ expandParallelToolCall,
55
+ isUndeclaredParallelToolCall,
56
+ } from './expand-parallel-tool-call';
52
57
  import { mapOpenAIResponseFinishReason } from './map-openai-responses-finish-reason';
53
58
  import {
54
59
  openaiResponsesChunkSchema,
@@ -538,6 +543,10 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
538
543
 
539
544
  const content: Array<LanguageModelV3Content> = [];
540
545
  const logprobs: Array<OpenAIResponsesLogprobs> = [];
546
+ const functionTools =
547
+ options.tools?.filter(
548
+ (tool): tool is LanguageModelV3FunctionTool => tool.type === 'function',
549
+ ) ?? [];
541
550
 
542
551
  // flag that checks if there have been client-side tool calls (not executed by openai)
543
552
  let hasFunctionCall = false;
@@ -798,6 +807,22 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
798
807
  case 'function_call': {
799
808
  hasFunctionCall = true;
800
809
 
810
+ const expandedToolCalls = await expandParallelToolCall({
811
+ toolCall: {
812
+ toolCallId: part.call_id,
813
+ toolName: part.name,
814
+ input: part.arguments,
815
+ },
816
+ tools: functionTools,
817
+ providerOptionsName,
818
+ itemId: part.id,
819
+ });
820
+
821
+ if (expandedToolCalls != null) {
822
+ content.push(...expandedToolCalls);
823
+ break;
824
+ }
825
+
801
826
  content.push({
802
827
  type: 'tool-call',
803
828
  toolCallId: part.call_id,
@@ -1105,6 +1130,11 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1105
1130
  const approvalRequestIdToDummyToolCallIdFromPrompt =
1106
1131
  extractApprovalRequestIdToToolCallIdMapping(options.prompt);
1107
1132
 
1133
+ const functionTools =
1134
+ options.tools?.filter(
1135
+ (tool): tool is LanguageModelV3FunctionTool => tool.type === 'function',
1136
+ ) ?? [];
1137
+
1108
1138
  const approvalRequestIdToDummyToolCallIdFromStream = new Map<
1109
1139
  string,
1110
1140
  string
@@ -1131,6 +1161,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1131
1161
  endEmitted: boolean;
1132
1162
  };
1133
1163
  toolSearchExecution?: 'server' | 'client';
1164
+ suppressInputStreaming?: boolean;
1165
+ bufferedInputDeltas?: string[];
1134
1166
  }
1135
1167
  | undefined
1136
1168
  > = {};
@@ -1199,16 +1231,25 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1199
1231
 
1200
1232
  if (isResponseOutputItemAddedChunk(value)) {
1201
1233
  if (value.item.type === 'function_call') {
1234
+ const suppressInputStreaming = isUndeclaredParallelToolCall({
1235
+ toolName: value.item.name,
1236
+ tools: functionTools,
1237
+ });
1238
+
1202
1239
  ongoingToolCalls[value.output_index] = {
1203
1240
  toolName: value.item.name,
1204
1241
  toolCallId: value.item.call_id,
1242
+ suppressInputStreaming,
1243
+ bufferedInputDeltas: suppressInputStreaming ? [] : undefined,
1205
1244
  };
1206
1245
 
1207
- controller.enqueue({
1208
- type: 'tool-input-start',
1209
- id: value.item.call_id,
1210
- toolName: value.item.name,
1211
- });
1246
+ if (!suppressInputStreaming) {
1247
+ controller.enqueue({
1248
+ type: 'tool-input-start',
1249
+ id: value.item.call_id,
1250
+ toolName: value.item.name,
1251
+ });
1252
+ }
1212
1253
  } else if (value.item.type === 'custom_tool_call') {
1213
1254
  const toolName = toolNameMapping.toCustomToolName(
1214
1255
  value.item.name,
@@ -1439,34 +1480,111 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1439
1480
  },
1440
1481
  });
1441
1482
  } else if (value.item.type === 'function_call') {
1483
+ const item = value.item;
1484
+ const ongoingToolCall = ongoingToolCalls[value.output_index];
1442
1485
  ongoingToolCalls[value.output_index] = undefined;
1443
1486
  hasFunctionCall = true;
1444
1487
 
1445
- controller.enqueue({
1446
- type: 'tool-input-end',
1447
- id: value.item.call_id,
1448
- ...(value.item.namespace != null && {
1488
+ const suppressInputStreaming =
1489
+ ongoingToolCall?.suppressInputStreaming ??
1490
+ isUndeclaredParallelToolCall({
1491
+ toolName: item.name,
1492
+ tools: functionTools,
1493
+ });
1494
+
1495
+ const enqueueUnexpandedToolCall = () => {
1496
+ if (suppressInputStreaming) {
1497
+ controller.enqueue({
1498
+ type: 'tool-input-start',
1499
+ id: item.call_id,
1500
+ toolName: item.name,
1501
+ });
1502
+
1503
+ const bufferedInputDeltas =
1504
+ ongoingToolCall?.bufferedInputDeltas ?? [];
1505
+
1506
+ if (bufferedInputDeltas.length > 0) {
1507
+ for (const delta of bufferedInputDeltas) {
1508
+ controller.enqueue({
1509
+ type: 'tool-input-delta',
1510
+ id: item.call_id,
1511
+ delta,
1512
+ });
1513
+ }
1514
+ } else if (item.arguments.length > 0) {
1515
+ controller.enqueue({
1516
+ type: 'tool-input-delta',
1517
+ id: item.call_id,
1518
+ delta: item.arguments,
1519
+ });
1520
+ }
1521
+ }
1522
+
1523
+ controller.enqueue({
1524
+ type: 'tool-input-end',
1525
+ id: item.call_id,
1526
+ ...(item.namespace != null && {
1527
+ providerMetadata: {
1528
+ [providerOptionsName]: {
1529
+ namespace: item.namespace,
1530
+ },
1531
+ },
1532
+ }),
1533
+ });
1534
+
1535
+ controller.enqueue({
1536
+ type: 'tool-call',
1537
+ toolCallId: item.call_id,
1538
+ toolName: item.name,
1539
+ input: item.arguments,
1449
1540
  providerMetadata: {
1450
1541
  [providerOptionsName]: {
1451
- namespace: value.item.namespace,
1542
+ itemId: item.id,
1543
+ ...(item.namespace != null && {
1544
+ namespace: item.namespace,
1545
+ }),
1452
1546
  },
1453
1547
  },
1454
- }),
1455
- });
1548
+ });
1549
+ };
1456
1550
 
1457
- controller.enqueue({
1458
- type: 'tool-call',
1459
- toolCallId: value.item.call_id,
1460
- toolName: value.item.name,
1461
- input: value.item.arguments,
1462
- providerMetadata: {
1463
- [providerOptionsName]: {
1464
- itemId: value.item.id,
1465
- ...(value.item.namespace != null && {
1466
- namespace: value.item.namespace,
1467
- }),
1468
- },
1551
+ if (!suppressInputStreaming) {
1552
+ enqueueUnexpandedToolCall();
1553
+ return;
1554
+ }
1555
+
1556
+ return expandParallelToolCall({
1557
+ toolCall: {
1558
+ toolCallId: item.call_id,
1559
+ toolName: item.name,
1560
+ input: item.arguments,
1469
1561
  },
1562
+ tools: functionTools,
1563
+ providerOptionsName,
1564
+ itemId: item.id,
1565
+ }).then(expandedToolCalls => {
1566
+ if (expandedToolCalls == null) {
1567
+ enqueueUnexpandedToolCall();
1568
+ return;
1569
+ }
1570
+
1571
+ for (const toolCall of expandedToolCalls) {
1572
+ controller.enqueue({
1573
+ type: 'tool-input-start',
1574
+ id: toolCall.toolCallId,
1575
+ toolName: toolCall.toolName,
1576
+ });
1577
+ controller.enqueue({
1578
+ type: 'tool-input-delta',
1579
+ id: toolCall.toolCallId,
1580
+ delta: toolCall.input,
1581
+ });
1582
+ controller.enqueue({
1583
+ type: 'tool-input-end',
1584
+ id: toolCall.toolCallId,
1585
+ });
1586
+ controller.enqueue(toolCall);
1587
+ }
1470
1588
  });
1471
1589
  } else if (value.item.type === 'custom_tool_call') {
1472
1590
  ongoingToolCalls[value.output_index] = undefined;
@@ -1863,11 +1981,15 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1863
1981
  const toolCall = ongoingToolCalls[value.output_index];
1864
1982
 
1865
1983
  if (toolCall != null) {
1866
- controller.enqueue({
1867
- type: 'tool-input-delta',
1868
- id: toolCall.toolCallId,
1869
- delta: value.delta,
1870
- });
1984
+ if (toolCall.suppressInputStreaming) {
1985
+ toolCall.bufferedInputDeltas?.push(value.delta);
1986
+ } else {
1987
+ controller.enqueue({
1988
+ type: 'tool-input-delta',
1989
+ id: toolCall.toolCallId,
1990
+ delta: value.delta,
1991
+ });
1992
+ }
1871
1993
  }
1872
1994
  } else if (isResponseCustomToolCallInputDeltaChunk(value)) {
1873
1995
  const toolCall = ongoingToolCalls[value.output_index];
@@ -2189,6 +2311,26 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
2189
2311
  },
2190
2312
 
2191
2313
  flush(controller) {
2314
+ for (const toolCall of Object.values(ongoingToolCalls)) {
2315
+ if (!toolCall?.suppressInputStreaming) {
2316
+ continue;
2317
+ }
2318
+
2319
+ controller.enqueue({
2320
+ type: 'tool-input-start',
2321
+ id: toolCall.toolCallId,
2322
+ toolName: toolCall.toolName,
2323
+ });
2324
+
2325
+ for (const delta of toolCall.bufferedInputDeltas ?? []) {
2326
+ controller.enqueue({
2327
+ type: 'tool-input-delta',
2328
+ id: toolCall.toolCallId,
2329
+ delta,
2330
+ });
2331
+ }
2332
+ }
2333
+
2192
2334
  const providerMetadata: SharedV3ProviderMetadata = {
2193
2335
  [providerOptionsName]: {
2194
2336
  responseId: responseId,
@@ -15,10 +15,15 @@ import { toolSearchArgsSchema } from '../tool/tool-search';
15
15
  import { webSearchArgsSchema } from '../tool/web-search';
16
16
  import { webSearchPreviewArgsSchema } from '../tool/web-search-preview';
17
17
  import type {
18
+ OpenAIResponsesAllowedTool,
18
19
  OpenAIResponsesFunctionTool,
19
20
  OpenAIResponsesTool,
20
21
  } from './openai-responses-api';
21
22
 
23
+ type AllowedToolResolution =
24
+ | { supported: true; entry: OpenAIResponsesAllowedTool }
25
+ | { supported: false; reason: string };
26
+
22
27
  type OpenAIToolOptions = {
23
28
  deferLoading?: boolean;
24
29
  namespace?: {
@@ -60,7 +65,7 @@ export async function prepareResponsesTools({
60
65
  | {
61
66
  type: 'allowed_tools';
62
67
  mode: 'auto' | 'required';
63
- tools: Array<{ type: 'function'; name: string }>;
68
+ tools: Array<OpenAIResponsesAllowedTool>;
64
69
  };
65
70
  toolWarnings: SharedV3Warning[];
66
71
  }> {
@@ -81,6 +86,35 @@ export async function prepareResponsesTools({
81
86
  const resolvedCustomProviderToolNames =
82
87
  customProviderToolNames ?? new Set<string>();
83
88
 
89
+ const allowedToolResolutions = new Map<string, AllowedToolResolution>();
90
+ const allowedToolAliases = new Map<
91
+ string,
92
+ AllowedToolResolution | 'ambiguous'
93
+ >();
94
+
95
+ const recordAllowedTool = (
96
+ toolName: string,
97
+ resolution: AllowedToolResolution,
98
+ canonicalName: string | undefined,
99
+ ) => {
100
+ allowedToolResolutions.set(toolName, resolution);
101
+
102
+ if (canonicalName == null || canonicalName === toolName) {
103
+ return;
104
+ }
105
+
106
+ const existingAlias = allowedToolAliases.get(canonicalName);
107
+
108
+ if (existingAlias == null) {
109
+ allowedToolAliases.set(canonicalName, resolution);
110
+ } else if (
111
+ existingAlias !== 'ambiguous' &&
112
+ !isSameAllowedTool(existingAlias, resolution)
113
+ ) {
114
+ allowedToolAliases.set(canonicalName, 'ambiguous');
115
+ }
116
+ };
117
+
84
118
  for (const tool of tools) {
85
119
  switch (tool.type) {
86
120
  case 'function': {
@@ -115,9 +149,32 @@ export async function prepareResponsesTools({
115
149
 
116
150
  namespaceTool.tools.push(openaiFunctionTool);
117
151
  }
152
+
153
+ recordAllowedTool(
154
+ tool.name,
155
+ namespace != null
156
+ ? {
157
+ supported: false,
158
+ reason:
159
+ 'tools inside an OpenAI tool namespace are not visible to tool_choice.allowed_tools',
160
+ }
161
+ : openaiOptions?.deferLoading === true
162
+ ? {
163
+ supported: false,
164
+ reason:
165
+ 'deferred tools are not visible to tool_choice.allowed_tools',
166
+ }
167
+ : {
168
+ supported: true,
169
+ entry: { type: 'function', name: tool.name },
170
+ },
171
+ undefined,
172
+ );
118
173
  break;
119
174
  }
120
175
  case 'provider': {
176
+ const openaiToolCountBefore = openaiTools.length;
177
+
121
178
  switch (tool.id) {
122
179
  case 'openai.file_search': {
123
180
  const args = await validateTypes({
@@ -321,6 +378,16 @@ export async function prepareResponsesTools({
321
378
  break;
322
379
  }
323
380
  }
381
+
382
+ if (openaiTools.length > openaiToolCountBefore) {
383
+ const openaiTool = openaiTools[openaiToolCountBefore];
384
+
385
+ recordAllowedTool(
386
+ tool.name,
387
+ toAllowedToolResolution(openaiTool),
388
+ toolNameMapping?.toProviderToolName(tool.name),
389
+ );
390
+ }
324
391
  break;
325
392
  }
326
393
  default:
@@ -333,15 +400,74 @@ export async function prepareResponsesTools({
333
400
  }
334
401
 
335
402
  if (allowedTools != null) {
403
+ const allowedToolEntries: Array<OpenAIResponsesAllowedTool> = [];
404
+ const droppedToolNames: string[] = [];
405
+
406
+ for (const name of allowedTools.toolNames) {
407
+ const directResolution = allowedToolResolutions.get(name);
408
+ const resolution = directResolution ?? allowedToolAliases.get(name);
409
+
410
+ if (directResolution != null && allowedToolAliases.has(name)) {
411
+ toolWarnings.push({
412
+ type: 'unsupported',
413
+ feature: `allowedTools entry "${name}"`,
414
+ details:
415
+ 'this name is both a tool name and the provider tool name of another tool in this request; the tool with this name is allowed',
416
+ });
417
+ }
418
+
419
+ if (resolution === 'ambiguous') {
420
+ toolWarnings.push({
421
+ type: 'unsupported',
422
+ feature: `allowedTools entry "${name}"`,
423
+ details:
424
+ 'several tools in this request share this provider tool name; use the tool name from the tools for this request instead',
425
+ });
426
+ droppedToolNames.push(name);
427
+ continue;
428
+ }
429
+
430
+ if (resolution == null) {
431
+ toolWarnings.push({
432
+ type: 'unsupported',
433
+ feature: `allowedTools entry "${name}"`,
434
+ details:
435
+ 'the tool is not part of the tools for this request and is sent as a function tool',
436
+ });
437
+ allowedToolEntries.push({
438
+ type: 'function',
439
+ name: toolNameMapping?.toProviderToolName(name) ?? name,
440
+ });
441
+ continue;
442
+ }
443
+
444
+ if (!resolution.supported) {
445
+ toolWarnings.push({
446
+ type: 'unsupported',
447
+ feature: `allowedTools entry "${name}"`,
448
+ details: `${resolution.reason}; the tool is removed from the allowed tools`,
449
+ });
450
+ droppedToolNames.push(name);
451
+ continue;
452
+ }
453
+
454
+ allowedToolEntries.push(resolution.entry);
455
+ }
456
+
457
+ if (allowedToolEntries.length === 0) {
458
+ throw new UnsupportedFunctionalityError({
459
+ functionality: `allowedTools with only tools that cannot be allow-listed (${droppedToolNames.join(
460
+ ', ',
461
+ )})`,
462
+ });
463
+ }
464
+
336
465
  return {
337
466
  tools: openaiTools,
338
467
  toolChoice: {
339
468
  type: 'allowed_tools',
340
469
  mode: allowedTools.mode ?? 'auto',
341
- tools: allowedTools.toolNames.map(name => ({
342
- type: 'function',
343
- name: toolNameMapping?.toProviderToolName(name) ?? name,
344
- })),
470
+ tools: allowedToolEntries,
345
471
  },
346
472
  toolWarnings,
347
473
  };
@@ -389,6 +515,61 @@ export async function prepareResponsesTools({
389
515
  }
390
516
  }
391
517
 
518
+ function allowedToolKey(entry: OpenAIResponsesAllowedTool): string {
519
+ switch (entry.type) {
520
+ case 'mcp':
521
+ return `mcp:${entry.server_label}`;
522
+ case 'function':
523
+ case 'custom':
524
+ return `${entry.type}:${entry.name}`;
525
+ default:
526
+ return entry.type;
527
+ }
528
+ }
529
+
530
+ function isSameAllowedTool(
531
+ a: AllowedToolResolution,
532
+ b: AllowedToolResolution,
533
+ ): boolean {
534
+ if (a.supported && b.supported) {
535
+ return allowedToolKey(a.entry) === allowedToolKey(b.entry);
536
+ }
537
+
538
+ if (!a.supported && !b.supported) {
539
+ return a.reason === b.reason;
540
+ }
541
+
542
+ return false;
543
+ }
544
+
545
+ function toAllowedToolResolution(
546
+ tool: OpenAIResponsesTool,
547
+ ): AllowedToolResolution {
548
+ switch (tool.type) {
549
+ case 'custom':
550
+ return { supported: true, entry: { type: 'custom', name: tool.name } };
551
+ case 'mcp':
552
+ return {
553
+ supported: true,
554
+ entry: { type: 'mcp', server_label: tool.server_label },
555
+ };
556
+ case 'file_search':
557
+ case 'web_search':
558
+ case 'web_search_preview':
559
+ case 'image_generation':
560
+ case 'code_interpreter':
561
+ case 'apply_patch':
562
+ case 'shell':
563
+ case 'local_shell':
564
+ return { supported: true, entry: { type: tool.type } };
565
+ default:
566
+ return {
567
+ supported: false,
568
+ reason: `OpenAI does not support ${tool.type} tools in tool_choice.allowed_tools`,
569
+ };
570
+ }
571
+ }
572
+
392
573
  function prepareFunctionTool({
393
574
  tool,
394
575
  options,