@ai-sdk/openai 3.0.28 → 3.0.30

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.
@@ -708,8 +708,8 @@ const result = await generateText({
708
708
 
709
709
  #### Shell Tool
710
710
 
711
- The OpenAI Responses API supports the shell tool for GPT-5.1 models through the `openai.tools.shell` tool.
712
- The shell tool allows allows running bash commands and interacting with a command line.
711
+ The OpenAI Responses API supports the shell tool through the `openai.tools.shell` tool.
712
+ The shell tool allows running bash commands and interacting with a command line.
713
713
  The model proposes shell commands; your integration executes them and returns the outputs.
714
714
 
715
715
  <Note type="warning">
@@ -717,16 +717,18 @@ The model proposes shell commands; your integration executes them and returns th
717
717
  add strict allow-/deny-lists before forwarding a command to the system shell.
718
718
  </Note>
719
719
 
720
+ The shell tool supports three environment modes that control where commands are executed:
721
+
722
+ ##### Local Execution (default)
723
+
724
+ When no `environment` is specified (or `type: 'local'` is used), commands are executed locally via your `execute` callback:
725
+
720
726
  ```ts
721
727
  import { openai } from '@ai-sdk/openai';
722
728
  import { generateText } from 'ai';
723
- import { exec } from 'child_process';
724
- import { promisify } from 'util';
725
-
726
- const execAsync = promisify(exec);
727
729
 
728
730
  const result = await generateText({
729
- model: openai('gpt-5.1'),
731
+ model: openai('gpt-5.2'),
730
732
  tools: {
731
733
  shell: openai.tools.shell({
732
734
  execute: async ({ action }) => {
@@ -739,12 +741,131 @@ const result = await generateText({
739
741
  });
740
742
  ```
741
743
 
742
- Your execute function must return an output array with results for each command:
744
+ ##### Hosted Container (auto)
745
+
746
+ Set `environment.type` to `'containerAuto'` to run commands in an OpenAI-hosted container. No `execute` callback is needed — OpenAI handles execution server-side:
747
+
748
+ ```ts
749
+ const result = await generateText({
750
+ model: openai('gpt-5.2'),
751
+ tools: {
752
+ shell: openai.tools.shell({
753
+ environment: {
754
+ type: 'containerAuto',
755
+ // optional configuration:
756
+ memoryLimit: '4g',
757
+ fileIds: ['file-abc123'],
758
+ networkPolicy: {
759
+ type: 'allowlist',
760
+ allowedDomains: ['example.com'],
761
+ },
762
+ },
763
+ }),
764
+ },
765
+ prompt: 'Install numpy and compute the eigenvalues of a 3x3 matrix.',
766
+ });
767
+ ```
768
+
769
+ The `containerAuto` environment supports:
770
+
771
+ - **fileIds** _string[]_ - File IDs to make available in the container
772
+ - **memoryLimit** _'1g' | '4g' | '16g' | '64g'_ - Memory limit for the container
773
+ - **networkPolicy** - Network access policy:
774
+ - `{ type: 'disabled' }` — no network access
775
+ - `{ type: 'allowlist', allowedDomains: string[], domainSecrets?: Array<{ domain, name, value }> }` — allow specific domains with optional secrets
776
+
777
+ ##### Existing Container Reference
778
+
779
+ Set `environment.type` to `'containerReference'` to use an existing container by ID:
780
+
781
+ ```ts
782
+ const result = await generateText({
783
+ model: openai('gpt-5.2'),
784
+ tools: {
785
+ shell: openai.tools.shell({
786
+ environment: {
787
+ type: 'containerReference',
788
+ containerId: 'cntr_abc123',
789
+ },
790
+ }),
791
+ },
792
+ prompt: 'Check the status of running processes.',
793
+ });
794
+ ```
795
+
796
+ ##### Execute Callback
797
+
798
+ For local execution (default or `type: 'local'`), your execute function must return an output array with results for each command:
743
799
 
744
800
  - **stdout** _string_ - Standard output from the command
745
801
  - **stderr** _string_ - Standard error from the command
746
802
  - **outcome** - Either `{ type: 'timeout' }` or `{ type: 'exit', exitCode: number }`
747
803
 
804
+ ##### Skills
805
+
806
+ [Skills](https://platform.openai.com/docs/guides/tools-skills) are versioned bundles of files with a `SKILL.md` manifest that extend the shell tool's capabilities. They can be attached to both `containerAuto` and `local` environments.
807
+
808
+ **Container skills** support two formats — by reference (for skills uploaded to OpenAI) or inline (as a base64-encoded zip):
809
+
810
+ ```ts
811
+ const result = await generateText({
812
+ model: openai('gpt-5.2'),
813
+ tools: {
814
+ shell: openai.tools.shell({
815
+ environment: {
816
+ type: 'containerAuto',
817
+ skills: [
818
+ // By reference:
819
+ { type: 'skillReference', skillId: 'skill_abc123' },
820
+ // Or inline:
821
+ {
822
+ type: 'inline',
823
+ name: 'my-skill',
824
+ description: 'What this skill does',
825
+ source: {
826
+ type: 'base64',
827
+ mediaType: 'application/zip',
828
+ data: readFileSync('./my-skill.zip').toString('base64'),
829
+ },
830
+ },
831
+ ],
832
+ },
833
+ }),
834
+ },
835
+ prompt: 'Use the skill to solve this problem.',
836
+ });
837
+ ```
838
+
839
+ **Local skills** point to a directory on disk containing a `SKILL.md` file:
840
+
841
+ ```ts
842
+ const result = await generateText({
843
+ model: openai('gpt-5.2'),
844
+ tools: {
845
+ shell: openai.tools.shell({
846
+ execute: async ({ action }) => {
847
+ // ... your local execution implementation ...
848
+ return { output: results };
849
+ },
850
+ environment: {
851
+ type: 'local',
852
+ skills: [
853
+ {
854
+ name: 'my-skill',
855
+ description: 'What this skill does',
856
+ path: resolve('path/to/skill-directory'),
857
+ },
858
+ ],
859
+ },
860
+ }),
861
+ },
862
+ prompt: 'Use the skill to solve this problem.',
863
+ stopWhen: stepCountIs(5),
864
+ });
865
+ ```
866
+
867
+ For more details on creating skills, see the [OpenAI Skills documentation](https://platform.openai.com/docs/guides/tools-skills).
868
+
748
869
  #### Apply Patch Tool
749
870
 
750
871
  The OpenAI Responses API supports the apply patch tool for GPT-5.1 models through the `openai.tools.applyPatch` tool.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/openai",
3
- "version": "3.0.28",
3
+ "version": "3.0.30",
4
4
  "license": "Apache-2.0",
5
5
  "sideEffects": false,
6
6
  "main": "./dist/index.js",
@@ -44,8 +44,8 @@
44
44
  "tsup": "^8",
45
45
  "typescript": "5.8.3",
46
46
  "zod": "3.25.76",
47
- "@ai-sdk/test-server": "1.0.3",
48
- "@vercel/ai-tsconfig": "0.0.0"
47
+ "@vercel/ai-tsconfig": "0.0.0",
48
+ "@ai-sdk/test-server": "1.0.3"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "zod": "^3.25.76 || ^4.1.8"
@@ -133,7 +133,7 @@ export class OpenAIImageModel implements ImageModelV3 {
133
133
  },
134
134
  providerMetadata: {
135
135
  openai: {
136
- images: response.data.map(item => ({
136
+ images: response.data.map((item, index) => ({
137
137
  ...(item.revised_prompt
138
138
  ? { revisedPrompt: item.revised_prompt }
139
139
  : {}),
@@ -142,6 +142,11 @@ export class OpenAIImageModel implements ImageModelV3 {
142
142
  quality: response.quality ?? undefined,
143
143
  background: response.background ?? undefined,
144
144
  outputFormat: response.output_format ?? undefined,
145
+ ...distributeTokenDetails(
146
+ response.usage?.input_tokens_details,
147
+ index,
148
+ response.data.length,
149
+ ),
145
150
  })),
146
151
  },
147
152
  },
@@ -190,7 +195,7 @@ export class OpenAIImageModel implements ImageModelV3 {
190
195
  },
191
196
  providerMetadata: {
192
197
  openai: {
193
- images: response.data.map(item => ({
198
+ images: response.data.map((item, index) => ({
194
199
  ...(item.revised_prompt
195
200
  ? { revisedPrompt: item.revised_prompt }
196
201
  : {}),
@@ -199,6 +204,11 @@ export class OpenAIImageModel implements ImageModelV3 {
199
204
  quality: response.quality ?? undefined,
200
205
  background: response.background ?? undefined,
201
206
  outputFormat: response.output_format ?? undefined,
207
+ ...distributeTokenDetails(
208
+ response.usage?.input_tokens_details,
209
+ index,
210
+ response.data.length,
211
+ ),
202
212
  })),
203
213
  },
204
214
  },
@@ -206,6 +216,40 @@ export class OpenAIImageModel implements ImageModelV3 {
206
216
  }
207
217
  }
208
218
 
219
+ /**
220
+ * Distributes input token details evenly across images, with the remainder
221
+ * assigned to the last image so that summing across all entries gives the
222
+ * exact total.
223
+ */
224
+ function distributeTokenDetails(
225
+ details:
226
+ | { image_tokens?: number | null; text_tokens?: number | null }
227
+ | null
228
+ | undefined,
229
+ index: number,
230
+ total: number,
231
+ ): { imageTokens?: number; textTokens?: number } {
232
+ if (details == null) {
233
+ return {};
234
+ }
235
+
236
+ const result: { imageTokens?: number; textTokens?: number } = {};
237
+
238
+ if (details.image_tokens != null) {
239
+ const base = Math.floor(details.image_tokens / total);
240
+ const remainder = details.image_tokens - base * (total - 1);
241
+ result.imageTokens = index === total - 1 ? remainder : base;
242
+ }
243
+
244
+ if (details.text_tokens != null) {
245
+ const base = Math.floor(details.text_tokens / total);
246
+ const remainder = details.text_tokens - base * (total - 1);
247
+ result.textTokens = index === total - 1 ? remainder : base;
248
+ }
249
+
250
+ return result;
251
+ }
252
+
209
253
  type OpenAIImageEditInput = {
210
254
  /**
211
255
  * Allows to set transparency for the background of the generated image(s).
@@ -302,16 +302,49 @@ export async function convertToOpenAIResponsesInput({
302
302
  break;
303
303
  }
304
304
 
305
+ const resolvedResultToolName = toolNameMapping.toProviderToolName(
306
+ part.toolName,
307
+ );
308
+
309
+ /*
310
+ * Shell tool results are separate output items (shell_call_output)
311
+ * with their own item IDs distinct from the shell_call's item ID.
312
+ * Since the pipeline only preserves the shell_call's item ID in
313
+ * callProviderMetadata, we reconstruct the full shell_call_output
314
+ * instead of using an item_reference with the wrong ID.
315
+ */
316
+ if (hasShellTool && resolvedResultToolName === 'shell') {
317
+ if (part.output.type === 'json') {
318
+ const parsedOutput = await validateTypes({
319
+ value: part.output.value,
320
+ schema: shellOutputSchema,
321
+ });
322
+ input.push({
323
+ type: 'shell_call_output',
324
+ call_id: part.toolCallId,
325
+ output: parsedOutput.output.map(item => ({
326
+ stdout: item.stdout,
327
+ stderr: item.stderr,
328
+ outcome:
329
+ item.outcome.type === 'timeout'
330
+ ? { type: 'timeout' as const }
331
+ : {
332
+ type: 'exit' as const,
333
+ exit_code: item.outcome.exitCode,
334
+ },
335
+ })),
336
+ });
337
+ }
338
+ break;
339
+ }
340
+
305
341
  if (store) {
306
342
  const itemId =
307
343
  (
308
- part as {
309
- providerMetadata?: {
310
- [providerOptionsName]?: { itemId?: string };
311
- };
312
- }
313
- ).providerMetadata?.[providerOptionsName]?.itemId ??
314
- part.toolCallId;
344
+ part.providerOptions?.[providerOptionsName] as
345
+ | { itemId?: string }
346
+ | undefined
347
+ )?.itemId ?? part.toolCallId;
315
348
  input.push({ type: 'item_reference', id: itemId });
316
349
  } else {
317
350
  warnings.push({
@@ -142,8 +142,10 @@ export type OpenAIResponsesShellCall = {
142
142
 
143
143
  export type OpenAIResponsesShellCallOutput = {
144
144
  type: 'shell_call_output';
145
+ id?: string;
145
146
  call_id: string;
146
- max_output_length?: number;
147
+ status?: 'in_progress' | 'completed' | 'incomplete';
148
+ max_output_length?: number | null;
147
149
  output: Array<{
148
150
  stdout: string;
149
151
  stderr: string;
@@ -328,6 +330,52 @@ export type OpenAIResponsesTool =
328
330
  }
329
331
  | {
330
332
  type: 'shell';
333
+ environment?:
334
+ | {
335
+ type: 'container_auto';
336
+ file_ids?: string[];
337
+ memory_limit?: '1g' | '4g' | '16g' | '64g';
338
+ network_policy?:
339
+ | { type: 'disabled' }
340
+ | {
341
+ type: 'allowlist';
342
+ allowed_domains: string[];
343
+ domain_secrets?: Array<{
344
+ domain: string;
345
+ name: string;
346
+ value: string;
347
+ }>;
348
+ };
349
+ skills?: Array<
350
+ | {
351
+ type: 'skill_reference';
352
+ skill_id: string;
353
+ version?: string;
354
+ }
355
+ | {
356
+ type: 'inline';
357
+ name: string;
358
+ description: string;
359
+ source: {
360
+ type: 'base64';
361
+ media_type: 'application/zip';
362
+ data: string;
363
+ };
364
+ }
365
+ >;
366
+ }
367
+ | {
368
+ type: 'container_reference';
369
+ container_id: string;
370
+ }
371
+ | {
372
+ type: 'local';
373
+ skills?: Array<{
374
+ name: string;
375
+ description: string;
376
+ path: string;
377
+ }>;
378
+ };
331
379
  };
332
380
 
333
381
  export type OpenAIResponsesReasoning = {
@@ -486,6 +534,25 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
486
534
  commands: z.array(z.string()),
487
535
  }),
488
536
  }),
537
+ z.object({
538
+ type: z.literal('shell_call_output'),
539
+ id: z.string(),
540
+ call_id: z.string(),
541
+ status: z.enum(['in_progress', 'completed', 'incomplete']),
542
+ output: z.array(
543
+ z.object({
544
+ stdout: z.string(),
545
+ stderr: z.string(),
546
+ outcome: z.discriminatedUnion('type', [
547
+ z.object({ type: z.literal('timeout') }),
548
+ z.object({
549
+ type: z.literal('exit'),
550
+ exit_code: z.number(),
551
+ }),
552
+ ]),
553
+ }),
554
+ ),
555
+ }),
489
556
  ]),
490
557
  }),
491
558
  z.object({
@@ -679,6 +746,25 @@ export const openaiResponsesChunkSchema = lazySchema(() =>
679
746
  commands: z.array(z.string()),
680
747
  }),
681
748
  }),
749
+ z.object({
750
+ type: z.literal('shell_call_output'),
751
+ id: z.string(),
752
+ call_id: z.string(),
753
+ status: z.enum(['in_progress', 'completed', 'incomplete']),
754
+ output: z.array(
755
+ z.object({
756
+ stdout: z.string(),
757
+ stderr: z.string(),
758
+ outcome: z.discriminatedUnion('type', [
759
+ z.object({ type: z.literal('timeout') }),
760
+ z.object({
761
+ type: z.literal('exit'),
762
+ exit_code: z.number(),
763
+ }),
764
+ ]),
765
+ }),
766
+ ),
767
+ }),
682
768
  ]),
683
769
  }),
684
770
  z.object({
@@ -1064,6 +1150,25 @@ export const openaiResponsesResponseSchema = lazySchema(() =>
1064
1150
  commands: z.array(z.string()),
1065
1151
  }),
1066
1152
  }),
1153
+ z.object({
1154
+ type: z.literal('shell_call_output'),
1155
+ id: z.string(),
1156
+ call_id: z.string(),
1157
+ status: z.enum(['in_progress', 'completed', 'incomplete']),
1158
+ output: z.array(
1159
+ z.object({
1160
+ stdout: z.string(),
1161
+ stderr: z.string(),
1162
+ outcome: z.discriminatedUnion('type', [
1163
+ z.object({ type: z.literal('timeout') }),
1164
+ z.object({
1165
+ type: z.literal('exit'),
1166
+ exit_code: z.number(),
1167
+ }),
1168
+ ]),
1169
+ }),
1170
+ ),
1171
+ }),
1067
1172
  ]),
1068
1173
  )
1069
1174
  .optional(),
@@ -37,7 +37,7 @@ import { fileSearchOutputSchema } from '../tool/file-search';
37
37
  import { imageGenerationOutputSchema } from '../tool/image-generation';
38
38
  import { localShellInputSchema } from '../tool/local-shell';
39
39
  import { mcpOutputSchema } from '../tool/mcp';
40
- import { shellInputSchema } from '../tool/shell';
40
+ import { shellInputSchema, shellOutputSchema } from '../tool/shell';
41
41
  import { webSearchOutputSchema } from '../tool/web-search';
42
42
  import {
43
43
  convertOpenAIResponsesUsage,
@@ -417,6 +417,16 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
417
417
  toolChoice,
418
418
  });
419
419
 
420
+ const shellToolEnvType = (
421
+ tools?.find(
422
+ tool => tool.type === 'provider' && tool.id === 'openai.shell',
423
+ ) as { args?: { environment?: { type?: string } } } | undefined
424
+ )?.args?.environment?.type;
425
+
426
+ const isShellProviderExecuted =
427
+ shellToolEnvType === 'containerAuto' ||
428
+ shellToolEnvType === 'containerReference';
429
+
420
430
  return {
421
431
  webSearchToolName,
422
432
  args: {
@@ -428,6 +438,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
428
438
  store,
429
439
  toolNameMapping,
430
440
  providerOptionsName,
441
+ isShellProviderExecuted,
431
442
  };
432
443
  }
433
444
 
@@ -440,6 +451,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
440
451
  webSearchToolName,
441
452
  toolNameMapping,
442
453
  providerOptionsName,
454
+ isShellProviderExecuted,
443
455
  } = await this.getArgs(options);
444
456
  const url = this.config.url({
445
457
  path: '/responses',
@@ -556,6 +568,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
556
568
  commands: part.action.commands,
557
569
  },
558
570
  } satisfies InferSchema<typeof shellInputSchema>),
571
+ ...(isShellProviderExecuted && { providerExecuted: true }),
559
572
  providerMetadata: {
560
573
  [providerOptionsName]: {
561
574
  itemId: part.id,
@@ -566,6 +579,28 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
566
579
  break;
567
580
  }
568
581
 
582
+ case 'shell_call_output': {
583
+ content.push({
584
+ type: 'tool-result',
585
+ toolCallId: part.call_id,
586
+ toolName: toolNameMapping.toCustomToolName('shell'),
587
+ result: {
588
+ output: part.output.map(item => ({
589
+ stdout: item.stdout,
590
+ stderr: item.stderr,
591
+ outcome:
592
+ item.outcome.type === 'exit'
593
+ ? {
594
+ type: 'exit' as const,
595
+ exitCode: item.outcome.exit_code,
596
+ }
597
+ : { type: 'timeout' as const },
598
+ })),
599
+ } satisfies InferSchema<typeof shellOutputSchema>,
600
+ });
601
+ break;
602
+ }
603
+
569
604
  case 'message': {
570
605
  for (const contentPart of part.content) {
571
606
  if (
@@ -910,6 +945,7 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
910
945
  toolNameMapping,
911
946
  store,
912
947
  providerOptionsName,
948
+ isShellProviderExecuted,
913
949
  } = await this.getArgs(options);
914
950
 
915
951
  const { responseHeaders, value: response } = await postJsonToApi({
@@ -1160,6 +1196,8 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1160
1196
  toolName: toolNameMapping.toCustomToolName('shell'),
1161
1197
  toolCallId: value.item.call_id,
1162
1198
  };
1199
+ } else if (value.item.type === 'shell_call_output') {
1200
+ // shell_call_output is handled in output_item.done
1163
1201
  } else if (value.item.type === 'message') {
1164
1202
  ongoingAnnotations.splice(0, ongoingAnnotations.length);
1165
1203
  controller.enqueue({
@@ -1469,10 +1507,40 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV3 {
1469
1507
  commands: value.item.action.commands,
1470
1508
  },
1471
1509
  } satisfies InferSchema<typeof shellInputSchema>),
1510
+ ...(isShellProviderExecuted && {
1511
+ providerExecuted: true,
1512
+ }),
1472
1513
  providerMetadata: {
1473
1514
  [providerOptionsName]: { itemId: value.item.id },
1474
1515
  },
1475
1516
  });
1517
+ } else if (value.item.type === 'shell_call_output') {
1518
+ controller.enqueue({
1519
+ type: 'tool-result',
1520
+ toolCallId: value.item.call_id,
1521
+ toolName: toolNameMapping.toCustomToolName('shell'),
1522
+ result: {
1523
+ output: value.item.output.map(
1524
+ (item: {
1525
+ stdout: string;
1526
+ stderr: string;
1527
+ outcome:
1528
+ | { type: 'exit'; exit_code: number }
1529
+ | { type: 'timeout' };
1530
+ }) => ({
1531
+ stdout: item.stdout,
1532
+ stderr: item.stderr,
1533
+ outcome:
1534
+ item.outcome.type === 'exit'
1535
+ ? {
1536
+ type: 'exit' as const,
1537
+ exitCode: item.outcome.exit_code,
1538
+ }
1539
+ : { type: 'timeout' as const },
1540
+ }),
1541
+ ),
1542
+ } satisfies InferSchema<typeof shellOutputSchema>,
1543
+ });
1476
1544
  } else if (value.item.type === 'reasoning') {
1477
1545
  const activeReasoningPart = activeReasoning[value.item.id];
1478
1546