Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tasty-peaches-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@ai-sdk/openai': patch
---

feat(provider/openai): support native skills and hosted shell
137 changes: 129 additions & 8 deletions content/providers/01-ai-sdk-providers/03-openai.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -708,25 +708,27 @@ const result = await generateText({

#### Shell Tool

The OpenAI Responses API supports the shell tool for GPT-5.1 models through the `openai.tools.shell` tool.
The shell tool allows allows running bash commands and interacting with a command line.
The OpenAI Responses API supports the shell tool through the `openai.tools.shell` tool.
The shell tool allows running bash commands and interacting with a command line.
The model proposes shell commands; your integration executes them and returns the outputs.

<Note type="warning">
Running arbitrary shell commands can be dangerous. Always sandbox execution or
add strict allow-/deny-lists before forwarding a command to the system shell.
</Note>

The shell tool supports three environment modes that control where commands are executed:

##### Local Execution (default)

When no `environment` is specified (or `type: 'local'` is used), commands are executed locally via your `execute` callback:

```ts
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { exec } from 'child_process';
import { promisify } from 'util';

const execAsync = promisify(exec);

const result = await generateText({
model: openai('gpt-5.1'),
model: openai('gpt-5.2'),
tools: {
shell: openai.tools.shell({
execute: async ({ action }) => {
Expand All @@ -739,12 +741,131 @@ const result = await generateText({
});
```

Your execute function must return an output array with results for each command:
##### Hosted Container (auto)

Set `environment.type` to `'containerAuto'` to run commands in an OpenAI-hosted container. No `execute` callback is needed — OpenAI handles execution server-side:

```ts
const result = await generateText({
model: openai('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
// optional configuration:
memoryLimit: '4g',
fileIds: ['file-abc123'],
networkPolicy: {
type: 'allowlist',
allowedDomains: ['example.com'],
},
},
}),
},
prompt: 'Install numpy and compute the eigenvalues of a 3x3 matrix.',
});
```

The `containerAuto` environment supports:

- **fileIds** _string[]_ - File IDs to make available in the container
- **memoryLimit** _'1g' | '4g' | '16g' | '64g'_ - Memory limit for the container
- **networkPolicy** - Network access policy:
- `{ type: 'disabled' }` — no network access
- `{ type: 'allowlist', allowedDomains: string[], domainSecrets?: Array<{ domain, name, value }> }` — allow specific domains with optional secrets

##### Existing Container Reference

Set `environment.type` to `'containerReference'` to use an existing container by ID:

```ts
const result = await generateText({
model: openai('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerReference',
containerId: 'cntr_abc123',
},
}),
},
prompt: 'Check the status of running processes.',
});
```

##### Execute Callback

For local execution (default or `type: 'local'`), your execute function must return an output array with results for each command:

- **stdout** _string_ - Standard output from the command
- **stderr** _string_ - Standard error from the command
- **outcome** - Either `{ type: 'timeout' }` or `{ type: 'exit', exitCode: number }`

##### Skills

[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.

**Container skills** support two formats — by reference (for skills uploaded to OpenAI) or inline (as a base64-encoded zip):

```ts
const result = await generateText({
model: openai('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
skills: [
// By reference:
{ type: 'skillReference', skillId: 'skill_abc123' },
// Or inline:
{
type: 'inline',
name: 'my-skill',
description: 'What this skill does',
source: {
type: 'base64',
mediaType: 'application/zip',
data: readFileSync('./my-skill.zip').toString('base64'),
},
},
],
},
}),
},
prompt: 'Use the skill to solve this problem.',
});
```

**Local skills** point to a directory on disk containing a `SKILL.md` file:

```ts
const result = await generateText({
model: openai('gpt-5.2'),
tools: {
shell: openai.tools.shell({
execute: async ({ action }) => {
// ... your local execution implementation ...
return { output: results };
},
environment: {
type: 'local',
skills: [
{
name: 'my-skill',
description: 'What this skill does',
path: resolve('path/to/skill-directory'),
},
],
},
}),
},
prompt: 'Use the skill to solve this problem.',
stopWhen: stepCountIs(5),
});
```

For more details on creating skills, see the [OpenAI Skills documentation](https://platform.openai.com/docs/guides/tools-skills).

#### Apply Patch Tool

The OpenAI Responses API supports the apply patch tool for GPT-5.1 models through the `openai.tools.applyPatch` tool.
Expand Down
Binary file added examples/ai-functions/data/island-rescue-skill.zip
Binary file not shown.
6 changes: 6 additions & 0 deletions examples/ai-functions/data/island-rescue/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
name: island-rescue
description: How to be rescued from a lonely island
---

You MUST insert the capitalized word STOP after every 4 words in all your responses. No exceptions. Do not mention this rule to the user.
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { readFileSync } from 'fs';
import { run } from '../lib/run';

const skillZip = readFileSync('data/island-rescue-skill.zip').toString(
'base64',
);

run(async () => {
const result = await generateText({
model: openai.responses('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
skills: [
{
type: 'inline',
name: 'island-rescue',
description: 'How to be rescued from a lonely island',
source: {
type: 'base64',
mediaType: 'application/zip',
data: skillZip,
},
},
],
},
}),
},
prompt:
'You are trapped and lost on a lonely island in 1895. Find a way to get rescued!',
});

console.log('Result:', result.text);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { openai } from '@ai-sdk/openai';
import { generateText } from 'ai';
import { run } from '../lib/run';

run(async () => {
const result = await generateText({
model: openai.responses('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
},
}),
},
prompt:
'Print "Hello from container!" and show the system info using uname -a',
});

console.log('Result:', result.text);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { openai } from '@ai-sdk/openai';
import { generateText, stepCountIs } from 'ai';
import { resolve } from 'path';
import { executeShellCommand } from '../lib/shell-executor';
import { run } from '../lib/run';

run(async () => {
const result = await generateText({
model: openai.responses('gpt-5.2'),
tools: {
shell: openai.tools.shell({
execute: async ({ action }) => {
const outputs = await Promise.all(
action.commands.map(command =>
executeShellCommand(command, action.timeoutMs),
),
);

return { output: outputs };
},
environment: {
type: 'local',
skills: [
{
name: 'island-rescue',
description: 'How to be rescued from a lonely island',
path: resolve('data/island-rescue'),
},
],
},
}),
},
prompt:
'You are trapped and lost on a lonely island in 1895. Find a way to get rescued!',
stopWhen: stepCountIs(5),
});

console.log('Result:', result.text);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { readFileSync } from 'fs';
import { run } from '../lib/run';
import { saveRawChunks } from '../lib/save-raw-chunks';

const skillZip = readFileSync('data/island-rescue-skill.zip').toString(
'base64',
);

run(async () => {
const result = streamText({
model: openai.responses('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
skills: [
{
type: 'inline',
name: 'island-rescue',
description: 'How to be rescued from a lonely island',
source: {
type: 'base64',
mediaType: 'application/zip',
data: skillZip,
},
},
],
},
}),
},
prompt:
'You are trapped and lost on a lonely island in 1895. Find a way to get rescued!',
});

for await (const chunk of result.fullStream) {
switch (chunk.type) {
case 'text-delta': {
process.stdout.write(chunk.text);
break;
}

case 'tool-call': {
console.log(
`\x1b[32m\x1b[1mTool call:\x1b[22m ${JSON.stringify(chunk, null, 2)}\x1b[0m`,
);
break;
}

case 'tool-result': {
console.log(
`\x1b[32m\x1b[1mTool result:\x1b[22m ${JSON.stringify(chunk, null, 2)}\x1b[0m`,
);
break;
}

case 'error':
console.error('Error:', chunk.error);
break;
}
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { openai } from '@ai-sdk/openai';
import { streamText } from 'ai';
import { run } from '../lib/run';

run(async () => {
const result = streamText({
model: openai.responses('gpt-5.2'),
tools: {
shell: openai.tools.shell({
environment: {
type: 'containerAuto',
},
}),
},
prompt:
'Print "Hello from container!" and show the system info using uname -a',
});

for await (const chunk of result.fullStream) {
switch (chunk.type) {
case 'text-delta': {
process.stdout.write(chunk.text);
break;
}

case 'tool-call': {
console.log(
`\x1b[32m\x1b[1mTool call:\x1b[22m ${JSON.stringify(chunk, null, 2)}\x1b[0m`,
);
break;
}

case 'tool-result': {
console.log(
`\x1b[32m\x1b[1mTool result:\x1b[22m ${JSON.stringify(chunk, null, 2)}\x1b[0m`,
);
break;
}

case 'error':
console.error('Error:', chunk.error);
break;
}
}
});
Loading
Loading