@ai-sdk/openai 4.0.42 → 4.0.44

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.
@@ -1403,6 +1403,64 @@ The custom tool can be configured with:
1403
1403
  - **definition** _string_ - (grammar only) The grammar definition string (a regex pattern or Lark grammar).
1404
1404
  - **execute** _function_ (optional) - An async function that receives the raw string input and returns a string result. Enables multi-turn tool calling.
1405
1405
 
1406
+ #### Restricting Callable Tools
1407
+
1408
+ The `allowedTools` provider option restricts which of the tools you declared the model is allowed to
1409
+ call, while still sending the full `tools` list to OpenAI. Because the tools list stays byte-identical
1410
+ across requests, the prompt cache is preserved — unlike `activeTools`, which removes tools from the
1411
+ request and invalidates the cache whenever the allow-list changes.
1412
+
1413
+ `allowedTools` is only supported by the Responses API, and it overrides the request-level `toolChoice`.
1414
+
1415
+ ```ts
1416
+ import { openai } from '@ai-sdk/openai';
1417
+ import { generateText } from 'ai';
1418
+
1419
+ const result = await generateText({
1420
+ model: openai.responses('gpt-5.5'),
1421
+ tools: {
1422
+ weather: weatherTool,
1423
+ cityAttractions: cityAttractionsTool,
1424
+ search: openai.tools.webSearch(),
1425
+ },
1426
+ providerOptions: {
1427
+ openai: {
1428
+ allowedTools: { toolNames: ['weather', 'search'], mode: 'auto' },
1429
+ },
1430
+ },
1431
+ prompt: 'What is the weather in San Francisco?',
1432
+ });
1433
+ ```
1434
+
1435
+ - **toolNames** _string[]_ - The tools the model may call, named as you declared them in `tools`. For
1436
+ provider-defined tools you may also use the canonical OpenAI name (for example `web_search`).
1437
+ - **mode** _'auto' | 'required'_ (optional) - `'auto'` (default) lets the model pick one of the allowed
1438
+ tools or answer with a message; `'required'` forces it to call at least one of them.
1439
+
1440
+ Both function tools and provider-defined tools can be allow-listed, including web search, file search,
1441
+ image generation, code interpreter, computer, apply patch, shell, programmatic tool calling, custom
1442
+ tools, and MCP servers.
1443
+
1444
+ If a name matches both a tool you declared and the canonical OpenAI name of a different tool, the tool
1445
+ you declared under that name wins, and a warning reports the ambiguity. If several tools share the same
1446
+ canonical name — two MCP servers, for example — that name is ambiguous, so it is dropped with a warning;
1447
+ name those tools as you declared them instead.
1448
+
1449
+ <Note>
1450
+ Three kinds of tools cannot be allow-listed, because OpenAI's `allowed_tools`
1451
+ only sees the eagerly-loaded tool list: the [tool search tool](#tool-search),
1452
+ tools marked with `deferLoading`, and tools grouped into a namespace. Naming
1453
+ one of these in `allowedTools` removes it from the allow-list and emits a
1454
+ warning; if that leaves no tools at all, the request fails with an error
1455
+ instead of silently sending no restriction.
1456
+ </Note>
1457
+
1458
+ <Note>
1459
+ An MCP server is allow-listed as a whole: the entry carries the server label,
1460
+ so every tool on that server is callable. To restrict which tools on a server
1461
+ the model may call, use the MCP tool's own `allowedTools` argument.
1462
+ </Note>
1463
+
1406
1464
  #### Image Inputs
1407
1465
 
1408
1466
  The OpenAI Responses API supports Image inputs for appropriate models.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "4.0.42",
3
+ "version": "4.0.44",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -36,15 +36,15 @@
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",
43
43
  "tsup": "^8.5.1",
44
44
  "typescript": "5.8.3",
45
45
  "zod": "3.25.76",
46
- "@ai-sdk/test-server": "2.0.1",
47
- "@vercel/ai-tsconfig": "0.0.0"
46
+ "@vercel/ai-tsconfig": "0.0.0",
47
+ "@ai-sdk/test-server": "2.0.1"
48
48
  },
49
49
  "peerDependencies": {
50
50
  "zod": "^3.25.76 || ^4.1.8"
@@ -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
  });
@@ -485,6 +485,21 @@ export type OpenAIResponsesFunctionTool = {
485
485
  output_schema?: JSONSchema7;
486
486
  };
487
487
 
488
+ /**
489
+ * Entry in `tool_choice.allowed_tools.tools`. OpenAI identifies most built-in
490
+ * tools by type alone; only `function`, `custom` and `mcp` carry an identifier.
491
+ */
492
+ export type OpenAIResponsesAllowedTool =
493
+ | { type: 'function'; name: string }
494
+ | { type: 'custom'; name: string }
495
+ | { type: 'mcp'; server_label: string }
496
+ | {
497
+ type: Exclude<
498
+ OpenAIResponsesTool['type'],
499
+ 'function' | 'custom' | 'mcp' | 'namespace' | 'tool_search'
500
+ >;
501
+ };
502
+
488
503
  export type OpenAIResponsesTool =
489
504
  | OpenAIResponsesFunctionTool
490
505
  | {
@@ -22,10 +22,15 @@ import { toolSearchArgsSchema } from '../tool/tool-search';
22
22
  import { webSearchArgsSchema } from '../tool/web-search';
23
23
  import { webSearchPreviewArgsSchema } from '../tool/web-search-preview';
24
24
  import type {
25
+ OpenAIResponsesAllowedTool,
25
26
  OpenAIResponsesFunctionTool,
26
27
  OpenAIResponsesTool,
27
28
  } from './openai-responses-api';
28
29
 
30
+ type AllowedToolResolution =
31
+ | { supported: true; entry: OpenAIResponsesAllowedTool }
32
+ | { supported: false; reason: string };
33
+
29
34
  export type OpenAIToolOptions = {
30
35
  allowedCallers?: Array<'direct' | 'programmatic'>;
31
36
  deferLoading?: boolean;
@@ -73,7 +78,7 @@ export async function prepareResponsesTools({
73
78
  | {
74
79
  type: 'allowed_tools';
75
80
  mode: 'auto' | 'required';
76
- tools: Array<{ type: 'function'; name: string }>;
81
+ tools: Array<OpenAIResponsesAllowedTool>;
77
82
  };
78
83
  toolWarnings: SharedV4Warning[];
79
84
  }> {
@@ -94,6 +99,35 @@ export async function prepareResponsesTools({
94
99
  const resolvedCustomProviderToolNames =
95
100
  customProviderToolNames ?? new Set<string>();
96
101
 
102
+ const allowedToolResolutions = new Map<string, AllowedToolResolution>();
103
+ const allowedToolAliases = new Map<
104
+ string,
105
+ AllowedToolResolution | 'ambiguous'
106
+ >();
107
+
108
+ const recordAllowedTool = (
109
+ toolName: string,
110
+ resolution: AllowedToolResolution,
111
+ canonicalName: string | undefined,
112
+ ) => {
113
+ allowedToolResolutions.set(toolName, resolution);
114
+
115
+ if (canonicalName == null || canonicalName === toolName) {
116
+ return;
117
+ }
118
+
119
+ const existingAlias = allowedToolAliases.get(canonicalName);
120
+
121
+ if (existingAlias == null) {
122
+ allowedToolAliases.set(canonicalName, resolution);
123
+ } else if (
124
+ existingAlias !== 'ambiguous' &&
125
+ !isSameAllowedTool(existingAlias, resolution)
126
+ ) {
127
+ allowedToolAliases.set(canonicalName, 'ambiguous');
128
+ }
129
+ };
130
+
97
131
  for (const tool of tools) {
98
132
  switch (tool.type) {
99
133
  case 'function': {
@@ -131,9 +165,32 @@ export async function prepareResponsesTools({
131
165
 
132
166
  namespaceTool.tools.push(openaiFunctionTool);
133
167
  }
168
+
169
+ recordAllowedTool(
170
+ tool.name,
171
+ namespace != null
172
+ ? {
173
+ supported: false,
174
+ reason:
175
+ 'tools inside an OpenAI tool namespace are not visible to tool_choice.allowed_tools',
176
+ }
177
+ : openaiOptions?.deferLoading === true
178
+ ? {
179
+ supported: false,
180
+ reason:
181
+ 'deferred tools are not visible to tool_choice.allowed_tools',
182
+ }
183
+ : {
184
+ supported: true,
185
+ entry: { type: 'function', name: tool.name },
186
+ },
187
+ undefined,
188
+ );
134
189
  break;
135
190
  }
136
191
  case 'provider': {
192
+ const openaiToolCountBefore = openaiTools.length;
193
+
137
194
  switch (tool.id) {
138
195
  case 'openai.file_search': {
139
196
  const args = await validateTypes({
@@ -349,6 +406,16 @@ export async function prepareResponsesTools({
349
406
  break;
350
407
  }
351
408
  }
409
+
410
+ if (openaiTools.length > openaiToolCountBefore) {
411
+ const openaiTool = openaiTools[openaiToolCountBefore];
412
+
413
+ recordAllowedTool(
414
+ tool.name,
415
+ toAllowedToolResolution(openaiTool),
416
+ toolNameMapping?.toProviderToolName(tool.name),
417
+ );
418
+ }
352
419
  break;
353
420
  }
354
421
  default:
@@ -361,15 +428,74 @@ export async function prepareResponsesTools({
361
428
  }
362
429
 
363
430
  if (allowedTools != null) {
431
+ const allowedToolEntries: Array<OpenAIResponsesAllowedTool> = [];
432
+ const droppedToolNames: string[] = [];
433
+
434
+ for (const name of allowedTools.toolNames) {
435
+ const directResolution = allowedToolResolutions.get(name);
436
+ const resolution = directResolution ?? allowedToolAliases.get(name);
437
+
438
+ if (directResolution != null && allowedToolAliases.has(name)) {
439
+ toolWarnings.push({
440
+ type: 'unsupported',
441
+ feature: `allowedTools entry "${name}"`,
442
+ details:
443
+ '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',
444
+ });
445
+ }
446
+
447
+ if (resolution === 'ambiguous') {
448
+ toolWarnings.push({
449
+ type: 'unsupported',
450
+ feature: `allowedTools entry "${name}"`,
451
+ details:
452
+ 'several tools in this request share this provider tool name; use the tool name from the tools for this request instead',
453
+ });
454
+ droppedToolNames.push(name);
455
+ continue;
456
+ }
457
+
458
+ if (resolution == null) {
459
+ toolWarnings.push({
460
+ type: 'unsupported',
461
+ feature: `allowedTools entry "${name}"`,
462
+ details:
463
+ 'the tool is not part of the tools for this request and is sent as a function tool',
464
+ });
465
+ allowedToolEntries.push({
466
+ type: 'function',
467
+ name: toolNameMapping?.toProviderToolName(name) ?? name,
468
+ });
469
+ continue;
470
+ }
471
+
472
+ if (!resolution.supported) {
473
+ toolWarnings.push({
474
+ type: 'unsupported',
475
+ feature: `allowedTools entry "${name}"`,
476
+ details: `${resolution.reason}; the tool is removed from the allowed tools`,
477
+ });
478
+ droppedToolNames.push(name);
479
+ continue;
480
+ }
481
+
482
+ allowedToolEntries.push(resolution.entry);
483
+ }
484
+
485
+ if (allowedToolEntries.length === 0) {
486
+ throw new UnsupportedFunctionalityError({
487
+ functionality: `allowedTools with only tools that cannot be allow-listed (${droppedToolNames.join(
488
+ ', ',
489
+ )})`,
490
+ });
491
+ }
492
+
364
493
  return {
365
494
  tools: openaiTools,
366
495
  toolChoice: {
367
496
  type: 'allowed_tools',
368
497
  mode: allowedTools.mode ?? 'auto',
369
- tools: allowedTools.toolNames.map(name => ({
370
- type: 'function',
371
- name: toolNameMapping?.toProviderToolName(name) ?? name,
372
- })),
498
+ tools: allowedToolEntries,
373
499
  },
374
500
  toolWarnings,
375
501
  };
@@ -419,6 +545,63 @@ export async function prepareResponsesTools({
419
545
  }
420
546
  }
421
547
 
548
+ function allowedToolKey(entry: OpenAIResponsesAllowedTool): string {
549
+ switch (entry.type) {
550
+ case 'mcp':
551
+ return `mcp:${entry.server_label}`;
552
+ case 'function':
553
+ case 'custom':
554
+ return `${entry.type}:${entry.name}`;
555
+ default:
556
+ return entry.type;
557
+ }
558
+ }
559
+
560
+ function isSameAllowedTool(
561
+ a: AllowedToolResolution,
562
+ b: AllowedToolResolution,
563
+ ): boolean {
564
+ if (a.supported && b.supported) {
565
+ return allowedToolKey(a.entry) === allowedToolKey(b.entry);
566
+ }
567
+
568
+ if (!a.supported && !b.supported) {
569
+ return a.reason === b.reason;
570
+ }
571
+
572
+ return false;
573
+ }
574
+
575
+ function toAllowedToolResolution(
576
+ tool: OpenAIResponsesTool,
577
+ ): AllowedToolResolution {
578
+ switch (tool.type) {
579
+ case 'custom':
580
+ return { supported: true, entry: { type: 'custom', name: tool.name } };
581
+ case 'mcp':
582
+ return {
583
+ supported: true,
584
+ entry: { type: 'mcp', server_label: tool.server_label },
585
+ };
586
+ case 'file_search':
587
+ case 'web_search':
588
+ case 'web_search_preview':
589
+ case 'image_generation':
590
+ case 'code_interpreter':
591
+ case 'computer':
592
+ case 'apply_patch':
593
+ case 'shell':
594
+ case 'local_shell':
595
+ case 'programmatic_tool_calling':
596
+ return { supported: true, entry: { type: tool.type } };
597
+ default:
598
+ return {
599
+ supported: false,
600
+ reason: `OpenAI does not support ${tool.type} tools in tool_choice.allowed_tools`,
601
+ };
602
+ }
603
+ }
604
+
422
605
  function prepareFunctionTool({
423
606
  tool,
424
607
  options,