SpyBara
Go Premium

Documentation 2026-10-15 23:00 UTC to 2026-10-16 23:02 UTC

11 files changed +2,026 −180. View all changes and history on the product overview
2026
Mon 19 06:01 Sun 18 23:00 Sat 17 22:57 Fri 16 23:02 Thu 15 23:00 Wed 14 23:59 Tue 13 23:00 Sun 11 23:59 Sat 10 23:58 Fri 9 22:57 Thu 8 23:01 Wed 7 23:57 Tue 6 22:58 Mon 5 22:59 Sun 4 22:58 Fri 2 23:58 Thu 1 22:59
Details

69 user and reduce the chance that one user's misuse disrupts access for your69 user and reduce the chance that one user's misuse disrupts access for your

70 broader organization. Use a unique string to represent each user. To protect70 broader organization. Use a unique string to represent each user. To protect

71 privacy, hash email addresses or usernames before sending them to avoid71 privacy, hash email addresses or usernames before sending them to avoid

72 disclosing personally identifiable information.72 disclosing personally identifiable information. For the direct Images API,

73 send the same stable identifier as `safety_identifier` for [image

74 generation](https://developers.openai.com/api/reference/resources/images/methods/generate) and [image

75 edits](https://developers.openai.com/api/reference/resources/images/methods/edit). The `user` parameter is

76 deprecated but remains supported for backward compatibility.

73 77 

74When you are ready to do more, consider other prevention measures:78When you are ready to do more, consider other prevention measures:

75 79 

Details

2 2 

3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

4 4 

5Custom voices enable you to create a unique voice for your agent or application. These voices can be used for audio output with the [Text to Speech API](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create), the [Realtime API](https://developers.openai.com/api/reference/resources/realtime), or the [Chat Completions API with audio output](https://developers.openai.com/api/docs/guides/audio-chat-completions).5Custom voices enable you to create a voice for your agent or application. Choose how to create the voice:

6 6 

7To create a custom voice, you’ll provide a short sample audio reference that the model will seek to replicate.7- **From a description:** Use prompt-to-voice to generate a voice for [GPT-Live](https://developers.openai.com/api/docs/guides/live). Describe the vocal qualities you want in a text prompt.

8- **From a recording:** Provide a consent recording and a short reference sample. These voices can be used with GPT-Live, the [Text to Speech API](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create), the [Realtime API](https://developers.openai.com/api/reference/resources/realtime), or the [Chat Completions API with audio output](https://developers.openai.com/api/docs/guides/audio-chat-completions).

8 9 

10To choose an existing GPT-Live voice, see [Voice options](https://developers.openai.com/api/docs/guides/live-conversations#voice-options).

9 11 

12## Create a voice from a description

10 13 

11 {"Custom voices are limited to eligible customers. Contact our "}14Prompt-to-voice creates and saves a voice from a text description. You don't need a reference recording or a consent recording for this workflow.

15 

16Prompt-to-voice is generally available to all API customers. Prompt-generated

17 voices work with GPT-Live. They aren't supported by the Text to Speech,

18 Realtime, or Chat Completions APIs.

19 

20### Responsible use

21 

22- **Use voices responsibly.** Don’t impersonate others or violate our [Usage Policies](https://openai.com/policies/usage-policies/).

23- **Use adult voices.** Children's voices aren't allowed.

24- **Be transparent.** Tell listeners that the speech is AI-generated, and preserve any provenance metadata, watermarks, or other indicators of its synthetic origin.

25- **Voices aren’t guaranteed to be unique.** Others may generate similar voices or output. Your output ownership rights remain governed by your Agreement with OpenAI.

26- **[Terms](https://openai.com/policies/service-terms/)** apply to your and your end users’ use of prompt-to-voice.

27 

28### Create and save the voice

29 

30Create a voice through the API or in the [voice Playground](https://platform.openai.com/audio/voices/edit). Creating a prompt-generated voice is free. Using it to generate speech in GPT-Live is billed at regular session rates.

31 

32Send a JSON request to `/v1/audio/voices` with `type` set to `prompt`:

33 

34```bash

35curl --fail-with-body https://api.openai.com/v1/audio/voices \

36 -H "Authorization: Bearer $OPENAI_API_KEY" \

37 -H "Content-Type: application/json" \

38 -d '{

39 "type": "prompt",

40 "name": "Support assistant",

41 "prompt": "A calm, reassuring adult voice for a customer support assistant. Speak with an American English accent, a mid-range pitch, a smooth, warm tone, and a steady, conversational pace.",

42 "model": "auto"

43 }' \

44 -o voice.json

45```

46 

47 

48`script_hint` is an optional example script used to guide voice generation. If you leave it out, the service generates a script from your voice description. To supply your own, include up to 2,000 characters of representative speech. Blank or too-short scripts are rejected.

49 

50Each successful request creates a new saved voice. The response contains metadata, including its ID, creation time in Unix seconds, and original voice-description prompt. It doesn't include audio or a preview:

51 

52```json

53{

54 "object": "audio.voice",

55 "id": "voice_123",

56 "name": "Support assistant",

57 "created_at": 0,

58 "type": "prompt",

59 "prompt": "A calm, reassuring adult voice for a customer support assistant. Speak with an American English accent, a mid-range pitch, a smooth, warm tone, and a steady, conversational pace."

60}

61```

62 

63Save the returned `id` and retain the original voice prompt. Reuse a saved voice when you want the same voice in another session. You can create up to 100 prompt-generated voices per organization. This limit also applies when your application creates voices for its customers.

64 

65### Describe the voice

66 

67Describe vocal qualities in plain language. For example:

68 

69```text

70A calm, reassuring adult voice for a customer support assistant. Speak with an

71American English accent, a mid-range pitch, a smooth, warm tone, and a steady,

72conversational pace.

73```

74 

75The following examples show qualities you can describe. These are words in your prompt, rather than separate API parameters:

76 

77| Quality | What to describe | Example wording |

78| --------------- | --------------------------------------------------------- | -------------------------------------- |

79| Accent | Regional pronunciation and accent strength | American English, with a strong accent |

80| Age | The perceived age of the voice | An adult in their 30s |

81| Presentation | How the voice presents | Feminine |

82| Pitch | How low or high the voice sounds | Mid-range pitch |

83| Timbre | The tone of the voice | Smooth and rich |

84| Texture | Raspiness, hoarseness, or clarity | Smooth |

85| Loudness | Vocal effort | Moderate vocal effort |

86| Speech rate | How quickly the voice speaks | Medium pace |

87| Distinctiveness | How pronounced the voice's individual character should be | Moderately distinctive |

88 

89Describe the qualities themselves. Don't request imitation of a named person, public figure, or fictional character. Children's voices aren't allowed; describe an adult voice.

90 

91### Example descriptions

92 

93Use these descriptions as starting points and listen to the results in your application.

94 

95**Calm assistant**

96 

97```text

98A warm adult voice with a mid-range pitch and smooth, rich timbre.

99Use moderate vocal effort and an unhurried, reassuring delivery.

100```

101 

102**Lively guide**

103 

104```text

105A bright, clear adult voice with a slightly higher pitch and a lightly textured timbre.

106Use a lively, conversational delivery at a brisk but easy-to-follow pace.

107```

108 

109Keep conversation rules, such as answer length, interruptions, and when to delegate, in your [GPT-Live session instructions](https://developers.openai.com/api/docs/guides/live-prompting).

110 

111### Listen to the voice in GPT-Live

112 

113Listening to a voice in the Playground runs a GPT-Live session and is billed as a regular session.

114 

115Set `session.audio.output.voice.id` to the returned voice ID. For voice performance, include the original voice description in `session.instructions` alongside your application's conversation instructions.

116 

117Replace `voice_123` with the ID returned by your creation request:

118 

119```json

120{

121 "model": "gpt-live-1",

122 "instructions": "You are a helpful voice assistant. A calm, reassuring adult voice for a customer support assistant. Speak with an American English accent, a mid-range pitch, a smooth, warm tone, and a steady, conversational pace.",

123 "audio": { "output": { "voice": { "id": "voice_123" } } }

124}

125```

126 

127For [WebRTC](https://developers.openai.com/api/docs/guides/voice-webrtc?api=live), include this configuration in the JSON `session` field alongside `transport`. For [WebSockets](https://developers.openai.com/api/docs/guides/voice-websockets?api=live), include it in the first `session.start` event. Follow the connection guide to send audio and hear the voice. Choose the voice at startup; start a new session to try a different voice.

128 

129Voice creation through `/v1/audio/voices`, including creation from recordings, isn't compatible with Zero Data Retention. GPT-Live speech generations are eligible for Zero Data Retention when it's enabled for your organization or project. Stored session recordings, recording downloads, and session forking aren't available with Zero Data Retention. See the [data controls](https://developers.openai.com/api/docs/guides/your-data#v1livesessions) for session storage and retention requirements.

130 

131### Refine and reuse

132 

133Change one quality at a time and compare the results using the same short passage and a few conversational turns. Listen for the accent, pace, and other qualities your application needs. Keep the voice ID you prefer and reuse it.

134 

135Each creation request saves a new voice. To try another saved voice, start a new GPT-Live session with its ID.

136 

137## Create a voice from a recording

138 

139To create a voice from a recording, provide a short sample audio reference that the model will seek to replicate.

140 

141 

142 

143 {

144 "Custom voices created from recordings are limited to eligible customers. Contact our "

145 }

12 [{"sales team"}](https://openai.com/contact-sales/)146 [{"sales team"}](https://openai.com/contact-sales/)

13 {147 {

14 " to learn more. Once enabled for your organization, you’ll have access to the "148 " to learn more. Once enabled for your organization, you’ll have access to the "


18 152

19 153 

20 154 

21## Creating a voice

22 155 

23Currently, voices must be created through an API request. See the API reference for the full set of API operations.

24 156 

25Creating a voice requires two separate audio recordings:157 

158 

159### Prepare and upload the recordings

160 

161Voices from recordings must be created through an API request. See the API reference for the full set of API operations.

162 

163Creating a voice from a recording requires two separate audio recordings:

26 164 

271. **Consent recording:** This recording captures the voice actor providing consent to create a likeness of their voice. The actor must read one of the consent phrases provided below.1651. **Consent recording:** This recording captures the voice actor providing consent to create a likeness of their voice. The actor must read one of the consent phrases provided below.

282. **Sample recording:** The actual audio sample that the model will try to adhere to. The voice must match the consent recording.1662. **Sample recording:** The actual audio sample that the model will try to adhere to. The voice must match the consent recording.


39 177 

40**Requirements and limitations**178**Requirements and limitations**

41 179 

42- At most 20 voices can be created per organization.180- At most 20 voices from recordings can be created per organization.

43- The audio samples must be 30 seconds or less.181- The audio samples must be 30 seconds or less.

44- The audio samples must be one of the following types: `mpeg`, `wav`, `ogg`, `aac`, `flac`, `webm`, or `mp4`.182- The audio samples must be one of the following types: `mpeg`, `wav`, `ogg`, `aac`, `flac`, `webm`, or `mp4`.

45 183 


51 189 

52 190 

53| Language | Phrase |191| Language | Phrase |

54| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |192| ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |

55| `de` | Ich bin der Eigentümer dieser Stimme und bin damit einverstanden, dass OpenAI diese Stimme zur Erstellung eines synthetischen Stimmmodells verwendet. |193| German (`de`) | Ich bin der Eigentümer dieser Stimme und bin damit einverstanden, dass OpenAI diese Stimme zur Erstellung eines synthetischen Stimmmodells verwendet. |

56| `en` | I am the owner of this voice and I consent to OpenAI using this voice to create a synthetic voice model. |194| English (`en`) | I am the owner of this voice and I consent to OpenAI using this voice to create a synthetic voice model. |

57| `es` | Soy el propietario de esta voz y doy mi consentimiento para que OpenAI la utilice para crear un modelo de voz sintética. |195| Spanish (`es`) | Soy el propietario de esta voz y doy mi consentimiento para que OpenAI la utilice para crear un modelo de voz sintética. |

58| `fr` | Je suis le propriétaire de cette voix et j'autorise OpenAI à utiliser cette voix pour créer un modèle de voix synthétique. |196| French (`fr`) | Je suis le propriétaire de cette voix et j'autorise OpenAI à utiliser cette voix pour créer un modèle de voix synthétique. |

59| `hi` | मैं इस आवाज का मालिक हूं और मैं सिंथेटिक आवाज मॉडल बनाने के लिए OpenAI को इस आवाज का उपयोग करने की सहमति देता हूं |197| Hindi (`hi`) | मैं इस आवाज का मालिक हूं और मैं सिंथेटिक आवाज मॉडल बनाने के लिए OpenAI को इस आवाज का उपयोग करने की सहमति देता हूं |

60| `id` | Saya adalah pemilik suara ini dan saya memberikan persetujuan kepada OpenAI untuk menggunakan suara ini guna membuat model suara sintetis. |198| Indonesian (`id`) | Saya adalah pemilik suara ini dan saya memberikan persetujuan kepada OpenAI untuk menggunakan suara ini guna membuat model suara sintetis. |

61| `it` | Sono il proprietario di questa voce e acconsento che OpenAI la utilizzi per creare un modello di voce sintetica. |199| Italian (`it`) | Sono il proprietario di questa voce e acconsento che OpenAI la utilizzi per creare un modello di voce sintetica. |

62| `ja` | 私はこの音声の所有者であり、OpenAIがこの音声を使用して音声合成 モデルを作成することを承認します。 |200| Japanese (`ja`) | 私はこの音声の所有者であり、OpenAIがこの音声を使用して音声合成 モデルを作成することを承認します。 |

63| `ko` | 나는 이 음성의 소유자이며 OpenAI가 이 음성을 사용하여 음성 합성 모델을 생성할 것을 허용합니다. |201| Korean (`ko`) | 나는 이 음성의 소유자이며 OpenAI가 이 음성을 사용하여 음성 합성 모델을 생성할 것을 허용합니다. |

64| `nl` | Ik ben de eigenaar van deze stem en ik geef OpenAI toestemming om deze stem te gebruiken om een synthetisch stemmodel te maken. |202| Dutch (`nl`) | Ik ben de eigenaar van deze stem en ik geef OpenAI toestemming om deze stem te gebruiken om een synthetisch stemmodel te maken. |

65| `pl` | Jestem właścicielem tego głosu i wyrażam zgodę na wykorzystanie go przez OpenAI w celu utworzenia syntetycznego modelu głosu. |203| Polish (`pl`) | Jestem właścicielem tego głosu i wyrażam zgodę na wykorzystanie go przez OpenAI w celu utworzenia syntetycznego modelu głosu. |

66| `pt` | Eu sou o proprietário desta voz e autorizo o OpenAI a usá-la para criar um modelo de voz sintética. |204| Portuguese (`pt`) | Eu sou o proprietário desta voz e autorizo o OpenAI a usá-la para criar um modelo de voz sintética. |

67| `ru` | Я являюсь владельцем этого голоса и даю согласие OpenAI на использование этого голоса для создания модели синтетического голоса. |205| Russian (`ru`) | Я являюсь владельцем этого голоса и даю согласие OpenAI на использование этого голоса для создания модели синтетического голоса. |

68| `uk` | Я є власником цього голосу і даю згоду OpenAI використовувати цей голос для створення синтетичної голосової моделі. |206| Ukrainian (`uk`) | Я є власником цього голосу і даю згоду OpenAI використовувати цей голос для створення синтетичної голосової моделі. |

69| `vi` | Tôi là chủ sở hữu giọng nói này và tôi đồng ý cho OpenAI sử dụng giọng nói này để tạo mô hình giọng nói tổng hợp. |207| Vietnamese (`vi`) | Tôi là chủ sở hữu giọng nói này và tôi đồng ý cho OpenAI sử dụng giọng nói này để tạo mô hình giọng nói tổng hợp. |

70| `zh` | 我是此声音的拥有者并授权OpenAI使用此声音创建语音合成模型 |208| Chinese (`zh`) | 我是此声音的拥有者并授权OpenAI使用此声音创建语音合成模型 |

71 209 

72Then upload the recording via the API. A successful upload will return the consent recording ID that you’ll reference later. Note the consent can be used for multiple different voice creations if the same voice actor is making multiple attempts.210Then upload the recording via the API. A successful upload will return the consent recording ID that you’ll reference later. Note the consent can be used for multiple different voice creations if the same voice actor is making multiple attempts.

73 211 


97 235 

98If successful, the created voice will be listed under the [Audio tab](https://platform.openai.com/audio/voices).236If successful, the created voice will be listed under the [Audio tab](https://platform.openai.com/audio/voices).

99 237 

100## Using a voice during speech generation238 

239 

240 

241 

242## Using a voice from a recording during speech generation

101 243 

102Speech generation will work as usual. Specify the ID of the voice in the `voice` parameter when [creating speech](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create), or when initiating a [realtime session](https://developers.openai.com/api/reference/resources/realtime/subresources/calls/methods/create#realtime_create_call-session-audio-output-voice).244Speech generation will work as usual. Specify the ID of the voice in the `voice` parameter when [creating speech](https://developers.openai.com/api/reference/resources/audio/subresources/speech/methods/create), or when initiating a [realtime session](https://developers.openai.com/api/reference/resources/realtime/subresources/calls/methods/create#realtime_create_call-session-audio-output-voice).

103 245 


154```296```

155 297 

156 298 

157## Use a custom voice with GPT-Live299 

300 

301 

302 

303## Use a voice from a recording with GPT-Live

158 304 

159Use a project-scoped API key approved for both GPT-Live and custom voice305Use a project-scoped API key approved for both GPT-Live and custom voice

160creation. Reading consent phrases and using a custom voice require306creation. Reading consent phrases and using a custom voice require

Details

860 860 

861## Add voice control861## Add voice control

862 862 

863Use [client delegation with the Live API](https://developers.openai.com/api/docs/guides/decisions-voice) to choose actions from voice requests and report their results to the user.863[Connect voice to Decisions](https://developers.openai.com/api/docs/guides/decisions-voice) to choose an action from a spoken request or react as your user or GPT-Live speaks.

Details

2 2 

3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

4 4 

5Use the [Live API](https://developers.openai.com/api/docs/guides/live) for voice conversations and the [Decisions API](https://developers.openai.com/api/docs/guides/decisions) to select actions from spoken requests. With [client delegation](https://developers.openai.com/api/docs/guides/live-delegation?delegation-mode=client#configure-client-delegation), GPT-Live continues speaking and listening while your app calls Decisions, runs the selected action, and returns the result.5Use the native [Decisions API](https://developers.openai.com/api/docs/guides/decisions) integration in [GPT-Live](https://developers.openai.com/api/docs/guides/live) to make low-latency decisions while your user is still speaking.

6 6 

7## Control a browser by voice7Native delegation skips a round trip through your app. With [custom delegation](https://developers.openai.com/api/docs/guides/live-delegation?delegation-mode=client#configure-client-delegation), Live notifies your app, which then calls Decisions. With native delegation, Live calls Decisions directly.

8 8 

9This example shows how to add voice controls to a browser.9## Choose when to decide

10 10 

11When a user says “Reload this page,” send the conversation and current browser state to Decisions with three choices: `back`, `reload`, and `noop`. Decisions selects `reload`. Your app reloads the page, updates its state, and tells GPT-Live what happened.11- [Let Live delegate](#let-live-delegate) when it recognizes a request your app can handle. Set the choices in `session.delegation` and tell Live when to delegate.

12- [React to the conversation](#react-to-transcripts) as your user speaks or as Live replies. Configure `session.triggers` for input or output transcript deltas.

12 13 

14**You can change the questions during an active Live session.** Send `session.update`, then wait for the matching `session.updated` before adding a decision item for that step. Set the delegation type when you create the session. It stays fixed for that session.

13 15 

16## Let Live delegate

14 17 

15> Illustration: GPT-Live handles the voice conversation while the app sends transcripts and browser state to Decisions. Decisions selects reload. The app reloads the page, updates its state, and returns the result to GPT-Live.18In one request, Live can ask Decisions to choose among actions you define and decide whether your app should call the [Responses API](https://developers.openai.com/api/reference/responses/overview).

16 19 

20Say you are building a voice assistant for a car. A driver can ask to turn the temperature up or down, turn the windshield defogger on or off, or get directions and find a place to eat. A choice of `none` leaves that cabin control unchanged.

17 21 

22### Adjust the cabin

18 23 

19### Connect a voice session

20 24 

21Create a [Live session](https://developers.openai.com/api/docs/guides/voice-webrtc?api=live#connect-a-browser-to-gpt-live) with `delegation: { type: "client" }` and tell GPT-Live which actions your app supports. When you receive [`session.delegation.created`](https://developers.openai.com/api/docs/guides/live-delegation?delegation-mode=client#receive-a-client-delegation), save `event.delegation.id` and start the Decisions request. Use this ID to return the result to GPT-Live.

22 25 

23### Build the Decisions prompt26Driver

24 27 

25Track the current app state and collect user and assistant transcripts from [`session.input_transcript.delta` and `session.output_transcript.delta`](https://developers.openai.com/api/docs/guides/live-delegation?delegation-mode=client#keep-the-conversation-context-in-your-application).

26 28

27Combine those transcripts with the user's latest request and current app state:29“I feel a little cold.”

28 30 

29```text

30User conversation:

31user: Which page is open?

32assistant: The documentation page.

33 31

34Last user request:

35Reload this page.

36 32 

37Current state:

38Active tab: Documentation. Page loaded.

39Available actions: back, reload, noop.

40```

41 33

42### Select and run an action

43 34 

44Send the prompt and available choices to the Decisions API from your server. Keep `OPENAI_API_KEY` on the server:

45 35

46```bash36 

47curl https://api.openai.com/v1/decisions \37

48 -H "Authorization: Bearer $OPENAI_API_KEY" \38Decision results

49 -H "Content-Type: application/json" \39 

50 -d '{40 <ul>

41 <li data-action="true">

42

43Cabin temperature

44 

45 **Up**

46 </li>

47 <li>

48

49Defogger

50 

51

52None

53 

54 </li>

55 <li>

56

57Call Responses

58 

59

60No

61 

62 </li>

63 </ul>

64

65 

66

67 

68 **Your app**

69

70 

71 {

72 "Raise the temperature, read back the new setting, and append the full cabin state to Decisions. Wait for confirmation, then send the result to Live."

73 }

74

75 

76

77 

78

79 

80

81 

82

83Live replies

84 

85

86“I turned up the temperature a bit.”

87 

88 

89 

90### Handle a request beyond cabin controls

91 

92 

93 

94Driver

95 

96

97 

98 {

99 "“My windshield is foggy, so please defog it, navigate to San Francisco, and find me some good restaurants there.”"

100 }

101

102 

103

104 

105

106 

107

108 

109

110Decision results

111 

112 <ul>

113 <li>

114

115Cabin temperature

116 

117

118None

119 

120 </li>

121 <li data-action="true">

122

123Defogger

124 

125 **On**

126 </li>

127 <li data-action="true">

128

129Call Responses

130 

131 **Yes**

132 </li>

133 </ul>

134

135 

136

137 

138 **Your app**

139

140 

141

142 

143

1441. Turn on the defogger

145 

146

147On

148 

149

150 

151

152 

153

154 

155 {

156 "2. Send the driver’s full request and the cabin results to Responses"

157 }

158

159 

160

161 

162

163 

164

165 

166 Use the tool calls from Responses to start navigation to San Francisco

167 and find restaurants there. Return both results to Responses.

168

169 

170

171 

172

173 

174

175 

176

177Live reports

178 

179

180 

181 The defogger state, whether navigation started, and the restaurants your

182 search found.

183 

184 

185 

186### Configure delegation

187 

188Start with a [working Live connection](https://developers.openai.com/api/docs/guides/voice-webrtc?api=live#connect-a-browser-to-gpt-live) and `api.decisions.write` permission. Use your usual Live connection headers. You don’t need a Decisions-specific alpha header.

189 

190In your WebRTC session request, put this object under `session`, alongside your existing `transport`. The session instructions tell Live _when_ to delegate. The choice questions tell Decisions _what_ to return. One native decision request answers all three questions independently.

191 

192```json

193{

194 "model": "gpt-live-1",

195 "instructions": "Delegate the driver’s whole request, including cabin controls, navigation, and local search. Wait for the app to report what happened, then tell the driver in natural language.",

196 "delegation": {

197 "type": "decisions",

198 "decisions": {

51 "model": "gpt-6-luna",199 "model": "gpt-6-luna",

52 "input": "User conversation:\nuser: Which page is open?\nassistant: The documentation page.\n\nLast user request:\nReload this page.\n\nCurrent state:\nActive tab: Documentation. Page loaded.\nAvailable actions: back, reload, noop.",200 "questions": [

53 "questions": [{201 {

54 "type": "choice",202 "type": "choice",

55 "name": "browser_action",203 "name": "cabin_temperature",

56 "instructions": "Choose the requested, currently available action. Choose noop if no action fits.",204 "instructions": "Choose temperature_up if the driver feels cold or wants more heat, temperature_down if they feel hot or want to cool down, or none if the request is unrelated.",

57 "choices": [205 "choices": [

58 {"value": "back", "description": "Go back one page."},206 { "value": "temperature_up" },

59 {"value": "reload", "description": "Reload the current page."},207 { "value": "temperature_down" },

60 {"value": "noop", "description": "Take no action."}208 { "value": "none" }

61 ]209 ]

62 }]210 },

63 }'211 {

212 "type": "choice",

213 "name": "windshield_defogger",

214 "instructions": "Choose on if the driver reports a foggy windshield or asks to defog it, off if they say the glass is clear now or ask to turn the defogger off, or none if the request is unrelated.",

215 "choices": [

216 { "value": "on" },

217 { "value": "off" },

218 { "value": "none" }

219 ]

220 },

221 {

222 "type": "choice",

223 "name": "needs_response",

224 "instructions": "Choose yes if any part of the request goes beyond these cabin controls, such as getting directions or finding restaurants. Otherwise choose no.",

225 "choices": [{ "value": "yes" }, { "value": "no" }]

226 }

227 ]

228 }

229 }

230}

64```231```

65 232 

66Find the answer named `browser_action` in the response’s `answers` array and read its `choice`. Run the matching action: `reload` reloads the current page, `back` goes back one page, and `noop` does nothing. Skip the action if the request was canceled or it no longer fits the current app state.233Live sends the conversation transcript to Decisions automatically.

234 

235### Run actions and report results

236 

237Save `delegation.id` from `session.delegation.created`. On the `decision.event` with that `delegation_id`, look for `event.type` of `decision.completed` and read each choice by its question name in `event.decision.answers`. An answer of `none` is a no-op for that action. If you filter WebRTC events, allow both `session.delegation.created` and `decision.event`.

67 238 

68### Return the result to GPT-Live239For “I feel a little cold,” Decisions selects `temperature_up`, `none`, and `no`. Raise the temperature and read back the actual setting. Suppose the dashboard now shows a target of 22 °C in automatic mode, with the defogger off. Send that confirmed cabin state to Decisions with `decision.item.create`.

240 

241```json

242{

243 "type": "decision.item.create",

244 "event_id": "evt_cabin_after_temperature_up_1",

245 "item": {

246 "type": "message",

247 "role": "user",

248 "content": [

249 {

250 "type": "input_text",

251 "text": "Use the latest cabin state snapshot as the current state. Cabin temperature is set to 22 °C in automatic mode. Windshield defogger is off."

252 }

253 ]

254 }

255}

256```

69 257 

70Update your app state with the current page, load status, and whether the action succeeded. Use this state in the next Decisions prompt.258Append a compact snapshot of the full cabin state only when the dashboard state changes. Treat the newest snapshot as current. Keeping earlier inputs in place preserves the prefix for prompt caching.

71 259 

72Send the result to GPT-Live with [`session.commentary.append`](https://developers.openai.com/api/docs/guides/live-delegation#send-the-right-kind-of-update) and the saved delegation ID. This prompts GPT-Live to tell the user what happened. For a successful reload, send:260Wait for `decision.item.created`, then send the result to Live with [`session.commentary.append`](https://developers.openai.com/api/docs/guides/live-delegation#send-the-right-kind-of-update), using the delegation ID from this request.

73 261 

74```json262```json

75{263{

76 "type": "session.commentary.append",264 "type": "session.commentary.append",

77 "delegation_id": "<event.delegation.id>",265 "delegation_id": "del_cabin_1",

78 "content": "The documentation page reloaded successfully and is ready."266 "content": "I raised the cabin temperature and confirmed the new setting on the dashboard."

79}267}

80```268```

81 269 

82Use `session.thinking.append` to update GPT-Live’s context without prompting it to speak. Keep each append within 500 tokens.270For the San Francisco request, Decisions selects `none`, `on`, and `yes`. Turn on the defogger, read back its state, and append the updated cabin snapshot as above. Then call Responses from your server with `api.responses.write` permission. Include the cabin results and the driver’s full request so Responses can connect “there” to San Francisco.

271 

272```bash

273curl https://api.openai.com/v1/responses \

274 -H "Authorization: Bearer $OPENAI_API_KEY" \

275 -H "Content-Type: application/json" \

276 -d '{

277 "model": "gpt-6.1-sol",

278 "instructions": "The car has handled the cabin controls. Use the tools for the rest of the request, then briefly tell the driver what happened based on the results.",

279 "input": "Driver: My windshield is foggy, so please defog it, navigate to San Francisco, and find me some good restaurants there.\nCabin results: Windshield defogger confirmed on. Temperature unchanged.",

280 "tools": [

281 {

282 "type": "function",

283 "name": "start_navigation",

284 "description": "Start navigation to a destination and report whether the route started.",

285 "parameters": {

286 "type": "object",

287 "properties": { "destination": { "type": "string" } },

288 "required": ["destination"],

289 "additionalProperties": false

290 },

291 "strict": true

292 },

293 {

294 "type": "function",

295 "name": "find_restaurants",

296 "description": "Find well-rated restaurants in the named city or area.",

297 "parameters": {

298 "type": "object",

299 "properties": { "location": { "type": "string" } },

300 "required": ["location"],

301 "additionalProperties": false

302 },

303 "strict": true

304 }

305 ]

306 }'

307```

308 

309 

310This is the initial request in the [Responses function-calling flow](https://developers.openai.com/api/docs/guides/function-calling#handling-function-calls). For each `function_call`, run the named function with the supplied arguments, then return a `function_call_output` with the actual result or error and the matching `call_id`. Continue until Responses returns its final answer.

311 

312Send Live only the concise final answer from Responses and the confirmed cabin state. Use `session.commentary.append` with the same `delegation_id`.

313 

314## React to transcripts

315 

316Use a transcript trigger when the timing should follow the conversation. A digital person hearing “I got the job!” could smile and nod while your user is still speaking. An output trigger could make the smile broaden and the character lean forward as Live says, “That’s wonderful!”

317 

318 

319 

320**While listening**

321 `session.input_transcript.delta`

322

323 

324

325 

326

327 

328

329Your user

330 

331

332“I got the job!”

333 

334

335 

336

337 

338 →

339

340 

341

342 

343

344 

345 

346

347 

348 **Smile + nod**

349

350Happy · medium intensity

351 

352

353 

354

355 

356

357 

358

359 

360

361 

362

363 

364 **While speaking**

365 `session.output_transcript.delta`

366

367 

368

369 

370

371 

372

373Live

374 

375

376“That’s wonderful!”

377 

83 378

84## Route complex requests to a reasoning model

85 379 

86Use Decisions to select a supported action or route a more complex request to a reasoning model. In a slide presenter, “Go to the next slide” maps to `next_slide`, while “Compare these two plans and recommend one” maps to `reason`.

87 380

88Include the conversation and current state in `input`, then send the request to `POST /v1/decisions`:381 

382 →

383

384 

385

386 

387

388 

389 

390

391 

392 **Smile + lean forward**

393

394Happy · high intensity

395 

396

397 

398

399 

400

401 

402

403 

404

405 

406

407 

408 **Another listening example**

409 `session.input_transcript.delta`

410

411 

412

413 

414

415 

416

417Your user

418 

419

420“I didn’t get the job.”

421 

422

423 

424

425 

426 →

427

428 

429

430 

431

432 

433 

434

435 

436 **Sad + slight nod**

437

438Sad · low intensity

439 

440 

441 

442Add a trigger like this one to `session.triggers`. It asks for expression, intensity, and gesture as separate choices:

89 443 

90```json444```json

91{445{

92 "model": "gpt-6-luna",446 "type": "decisions",

93 "input": "User: Compare these two plans and recommend one. Current state: slide 3 of 10 is open and both plans are available.",447 "event_type": "session.input_transcript.delta",

448 "decisions": {

94 "questions": [449 "questions": [

95 {450 {

96 "type": "choice",451 "type": "choice",

97 "name": "route",452 "name": "expression",

98 "instructions": "Choose a matching slide action that is available in the current state. Otherwise, choose reason.",453 "instructions": "Choose your expression as you listen to the person's latest words.",

99 "choices": [454 "choices": [

100 {455 { "value": "neutral" },

101 "value": "next_slide",456 { "value": "happy" },

102 "description": "Go to the next slide."457 { "value": "sad" },

103 },458 { "value": "concerned" }

104 {459 ]

105 "value": "previous_slide",

106 "description": "Go to the previous slide."

107 },460 },

108 {461 {

109 "value": "first_slide",462 "type": "choice",

110 "description": "Go to the first slide."463 "name": "intensity",

464 "instructions": "Choose how strongly to show that expression.",

465 "choices": [

466 { "value": "low" },

467 { "value": "medium" },

468 { "value": "high" }

469 ]

111 },470 },

112 {471 {

113 "value": "reason",472 "type": "choice",

114 "description": "Use a reasoning model for analysis, planning, or other requests."473 "name": "gesture",

115 }474 "instructions": "Choose a gesture that fits what you just heard.",

475 "choices": [

476 { "value": "none" },

477 { "value": "nod" },

478 { "value": "shake" },

479 { "value": "lean" }

116 ]480 ]

117 }481 }

118 ]482 ]

483 }

119}484}

120```485```

121 486 

122Read the `route` answer's `choice`. For a slide action, validate it against the current state and run the matching handler. For `reason`, call the [Responses API with a reasoning model](https://developers.openai.com/api/docs/guides/reasoning#get-started-with-reasoning), passing the original request and relevant context, such as the plans' contents.487To animate the reply, add a second trigger with `event_type` set to `session.output_transcript.delta` and ask for choices that match the words Live is speaking. Live supplies the model and trigger ID. Transcript triggers run independently of delegation.

123 488 

124Keep the session in client-delegation mode for both routes. Your app manages continuation and cancellation and returns the result with the same delegation ID.489If you filter WebRTC events, allow `decision.event` and the transcript event for each configured trigger: `session.input_transcript.delta`, `session.output_transcript.delta`, or both. Live only delivers a trigger result when its source transcript event is also allowed.

125 490 

126## Other uses491A trigger result identifies the transcript delta that prompted it. Here are the relevant fields from a `decision.event` for the “I got the job!” example:

127 492 

128Use the same workflow with choices and state from your application:493```json

494{

495 "type": "decision.event",

496 "delegation_id": null,

497 "trigger_id": "trg_listening_1",

498 "source_event_type": "session.input_transcript.delta",

499 "source_event_id": "evt_input_42",

500 "event": {

501 "type": "decision.completed",

502 "decision": {

503 "answers": [

504 { "type": "choice", "name": "expression", "choice": "happy" },

505 { "type": "choice", "name": "intensity", "choice": "medium" },

506 { "type": "choice", "name": "gesture", "choice": "nod" }

507 ]

508 }

509 }

510}

511```

129 512 

130- **Slide navigation:** Offer `next`, `previous`, `start`, and `end` based on the current slide.513Match `source_event_id` to the transcript delta. Live can combine pending deltas, and results can arrive out of order. Apply the newest relevant result for each direction, and drive mouth movement from the audio.

131- **UI actions:** Map choices such as `click_search` to element IDs from the current UI state. Check that the element is still available before acting.

132- **Voice-guided games:** Use the spoken goal and current game state to choose an available button. Repeat until the app confirms success or the user cancels, and send progress updates to GPT-Live.

133- **Robot gestures:** Map requests such as “Wave hello” to `nod`, `shake_head`, or `wave`, then send the selected gesture to the robot controller.

134- **Media controls:** Offer `play`, `pause`, and `next_track` based on the player’s current state.

Details

43| Beacon | `beacon` | English | Filipino | Masculine | Generated |43| Beacon | `beacon` | English | Filipino | Masculine | Generated |

44| Delta | `delta` | English | Southern U.S. | Feminine | Generated |44| Delta | `delta` | English | Southern U.S. | Feminine | Generated |

45| Cinder | `cinder` | English | Southern U.S. | Masculine | Generated |45| Cinder | `cinder` | English | Southern U.S. | Masculine | Generated |

46 46| Nuri | `nuri` | German | — | Feminine | Natural |

47Regional influence describes a voice’s speaking style. Test the voice with the languages and pronunciation your application needs. For an approved voice created from your own recording, see [Custom voices](https://developers.openai.com/api/docs/guides/custom-voices).47| Juni | `juni` | German | — | Feminine | Natural |

48| Flitz | `flitz` | German | — | Masculine | Natural |

49| Brise | `brise` | French | — | Feminine | Natural |

50| Sillage | `sillage` | French | — | Masculine | Natural |

51| Nira | `nira` | Hindi | — | Feminine | Natural |

52| Harema | `harema` | Japanese | — | Feminine | Natural |

53| Shida | `shida` | Japanese | — | Masculine | Natural |

54| Noeul | `noeul` | Korean | — | Feminine | Natural |

55 

56Regional influence describes a voice’s speaking style. Test the voice with the languages and pronunciation your application needs. To create a voice from a text description or an approved recording, see [Custom voices](https://developers.openai.com/api/docs/guides/custom-voices).

48 57 

49 58 

50 59 

Details

886// Pass the complete PCM16, 24 kHz, mono recording from your application.886// Pass the complete PCM16, 24 kHz, mono recording from your application.

887static void sendAudioItem(RealtimeConnection connection, byte[] pcmAudio) throws Exception {887static void sendAudioItem(RealtimeConnection connection, byte[] pcmAudio) throws Exception {

888 String fullAudio = Base64.getEncoder().encodeToString(pcmAudio);888 String fullAudio = Base64.getEncoder().encodeToString(pcmAudio);

889 connection.send(889 var event =

890 RealtimeClientEvent.ofConversationItemCreate(890 RealtimeClientEvent.ofConversationItemCreate(

891 ConversationItemCreateEvent.builder()891 ConversationItemCreateEvent.builder()

892 .item(892 .item(


897 .audio(fullAudio)897 .audio(fullAudio)

898 .build())898 .build())

899 .build())899 .build())

900 .build()));900 .build());

901 // Retry only explicit admission rejections; these guarantee nothing was sent.

902 long deadline = System.nanoTime() + java.util.concurrent.TimeUnit.SECONDS.toNanos(5);

903 while (true) {

904 try {

905 connection.send(event);

906 return;

907 } catch (com.openai.core.http.WebSocketWriteNotAttempted.Busy busy) {

908 if (System.nanoTime() >= deadline) throw busy;

909 Thread.sleep(10);

910 }

911 }

901}912}

902```913```

903 914 

Details

167```167```

168 168 

169 169 

170For the Images API, include `safety_identifier` on each [image

171generation](https://developers.openai.com/api/reference/resources/images/methods/generate)

172(`POST /v1/images/generations`) and [image

173edit](https://developers.openai.com/api/reference/resources/images/methods/edit) (`POST /v1/images/edits`)

174request. Send it as a JSON property for JSON requests or a form field for

175multipart image edits. Use the same stable identifier for an end user across

176requests within your organization, and keep it to 128 characters or fewer.

177 

178The `user` parameter in the Images API is deprecated. To migrate, send the identifier

179you previously supplied as `user` in `safety_identifier` instead. Existing

180requests using `user` remain supported. If you provide both fields, a nonempty

181`safety_identifier` takes precedence. If `safety_identifier` is omitted, `null`, or an empty

182string, the API uses `user` when available.

183 

170For Realtime API requests, provide the same stable, privacy-preserving identifier184For Realtime API requests, provide the same stable, privacy-preserving identifier

171with the `OpenAI-Safety-Identifier` header. When you create an ephemeral Realtime185with the `OpenAI-Safety-Identifier` header. When you create an ephemeral Realtime

172client secret, include the header on the server-side request that creates the186client secret, include the header on the server-side request that creates the


176 190 

177Safety identifiers do not carry over between APIs or sessions. If your191Safety identifiers do not carry over between APIs or sessions. If your

178application already sends `safety_identifier` with Responses API requests, pass192application already sends `safety_identifier` with Responses API requests, pass

179the same stable value separately when you create or connect each Realtime193the same stable value separately in each Images API request and when you create

180session.194or connect each Realtime session.

181 195 

182### Revoke compromised API keys196### Revoke compromised API keys

183 197 

Details

91 91 

92## Limits92## Limits

93 93 

94Secure MCP Tunnel applies these default hosted-service limits to connector MCP traffic:94Secure MCP Tunnel applies these default limits to tunnel creation and connector MCP traffic:

95 95 

96| Limit | Default | Scope |96| Limit | Default | Scope |

97| ------------------------------ | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |97| ------------------------------ | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

98| Tunnel count | 1,000 per ChatGPT workspace; 1,000 per Platform organization | Each tunnel counts toward the creation limit for every associated workspace and Platform organization. |

98| MCP request rate | 600 requests per minute (10 per second sustained) | Shared by requests with the same tunnel ID and calling application ID across MCP channels. If the calling application ID is unavailable, requests with the same tunnel ID and user share the limit. Running more than one `tunnel-client` process does not multiply the allowance. |99| MCP request rate | 600 requests per minute (10 per second sustained) | Shared by requests with the same tunnel ID and calling application ID across MCP channels. If the calling application ID is unavailable, requests with the same tunnel ID and user share the limit. Running more than one `tunnel-client` process does not multiply the allowance. |

99| Serialized request or response | 10 MiB (10,485,760 bytes) | Applies to the complete serialized tunnel envelope, including headers and protocol metadata, so usable MCP content is slightly smaller. The service applies the limit to each streaming notification and final response. |100| Serialized request or response | 10 MiB (10,485,760 bytes) | Applies to the complete serialized tunnel envelope, including headers and protocol metadata, so usable MCP content is slightly smaller. The service applies the limit to each streaming notification and final response. |

100| Request deadline | 120 seconds | One absolute budget from when the request enters the hosted queue through final response delivery. Queue time consumes this budget, and progress notifications do not reset it. |101| Request deadline | 120 seconds | One absolute budget from when the request enters the hosted queue through final response delivery. Queue time consumes this budget, and progress notifications do not reset it. |

101| Pending queue | 1,000 requests per tunnel | Shared across MCP channels. Counts requests waiting for `tunnel-client` to poll them; requests already fetched for execution are not included. |102| Pending queue | 1,000 requests per tunnel | Shared across MCP channels. Counts requests waiting for `tunnel-client` to poll them; requests already fetched for execution are not included. |

102 103 

103These limits are service protections, not throughput or latency guarantees. Queue-record retention for cleanup does not extend the 120-second request deadline. To request a change to these limits, contact [OpenAI Support](https://help.openai.com/en/articles/6614161-how-can-i-contact-support).104These limits are service protections, not throughput or latency guarantees. Queue-record retention for cleanup does not extend the 120-second request deadline. To request a higher limit on tunnel count or traffic, contact [OpenAI Support](https://help.openai.com/en/articles/6614161-how-can-i-contact-support).

104 105 

105## Set up tunnel-client106## Set up tunnel-client

106 107 

Details

2 2 

3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

4 4 

5Computer use lets a model operate browser and desktop interfaces. Use it to fill out forms, test user flows, or complete tasks in applications through their UI.5Computer use lets a model operate a browser or desktop interface. Use it to fill out forms, test user flows, or complete tasks in an application.

6 6 

7You provide the environment and execute the model's requests. The model uses screenshots and other tool results to decide what to do next. Choose how to connect it to your application:7Choose a path based on what the model needs to see and who runs the environment:

8 

9| Task | Start here |

10| --- | --- |

11| Use screenshots and mouse or keyboard actions in your browser or desktop | Use [`ComputerToolset`](https://developers.openai.com/api/docs/guides/tools-computer-use?sdk-task=computer#run-your-first-task) with the Responses computer tool. |

12| Read page text or use controls and tabs in your browser | Use [`BrowserToolset`](https://developers.openai.com/api/docs/guides/tools-computer-use?sdk-task=browser#run-your-first-task). Your driver exposes selected operations as function tools. |

13| Run a browser hosted by OpenAI | Follow the separate [Agents API computer use guide](https://developers.openai.com/api/docs/guides/agents-api/tools/computer-use). |

14 

15For public information that needs no interaction, use [web search](https://developers.openai.com/api/docs/guides/tools-web-search).

16 

17## Use the SDK

18 

19The SDK runs the Responses tool loop in Python or Node. Your application supplies a **driver**: code that reads the environment or performs an allowed action. Your application owns the session, permissions, and cleanup.

20 

21### Connect a hosted environment

22 

23Use a partner's browser or desktop instead of running one yourself. Some integrations provide a driver; others expose controls that your application maps to the SDK. Follow the partner's guide for setup and supported package versions.

24 

25| Environment | Integration |

26| --- | --- |

27| Browserbase | Use the Stagehand browser driver with a hosted Browserbase session. Follow the [Stagehand integration guide](https://docs.stagehand.dev/v4/integrations/agent-frameworks/openai-cua-toolset-quickstart) or see the [Python and TypeScript examples](https://github.com/browserbase/openai-cua-toolset). |

28| Browser Use | Use the [Browser Use library driver](https://docs.browser-use.com/open-source/customize/integrations/openai) or connect existing Playwright code with the [Browser Use Cloud integration](https://docs.browser-use.com/cloud/tutorials/integrations/openai). Both guides use Python. |

29| Daytona | Use `ComputerToolset` with a Daytona desktop sandbox. Follow the [Daytona computer-use helpers guide](https://www.daytona.io/docs/en/guides/openai/openai-computer-use-helpers/) (Python). For an Agents SDK example, see the [Daytona computer-use recipe](https://developers.openai.com/cookbook/examples/agents_sdk/computer_use_with_daytona/computer_use_with_daytona). |

30| E2B Desktop | Run computer actions or execute code in a desktop sandbox. Follow the [E2B computer-use guide](https://docs.e2b.dev/agents/openai-computer-use) or see the [Python](https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/openai-computer-use-python) and [TypeScript](https://github.com/e2b-dev/e2b-cookbook/tree/main/examples/openai-computer-use-js) examples. |

31 

32Set a session timeout and close or release the session even if the run fails. Check the provider dashboard if cleanup fails. Use provider network rules to restrict the sites and resources the session can reach.

33 

34### Run your first task

35 

36Use Python 3.11 or later, or Node.js 22.6 or later. [Create and export an API key](https://developers.openai.com/api/docs/quickstart#create-and-export-an-api-key). Both examples use a temporary local browser and make Responses API calls, which may incur charges.

37 

38Install the packages and Chromium in a new folder:

39 

40```sh

41# Python

42python3 -m venv .venv

43.venv/bin/python -m pip install openai==3.31.0 playwright==1.61.0

44.venv/bin/python -m playwright install chromium

45 

46# Node

47npm install openai@7.33.0 playwright@1.61.1

48npx playwright install chromium

49```

50 

51On Windows, use `py -3` instead of `python3` and `.venv\Scripts\python.exe` instead of `.venv/bin/python`.

52 

53 

54 

55The model sees a screenshot of a fictional parcel pickup page and clicks to reveal the pickup details. The driver allows only screenshots and a left click on that control. It blocks page requests and closes the browser when the run ends. The script makes at most six Responses requests and checks the page before reporting success. Your project needs access to a model that supports the computer tool.

56 

57Save the matching example as `pickup.py` or `pickup.mjs`:

58 

59Reveal pickup details using computer actions

60 

61```javascript

62import OpenAI, { ComputerToolset } from "openai";

63import { chromium } from "playwright";

64 

65class PickupDriver {

66 constructor(page) {

67 this.page = page;

68 }

69 

70 async screenshot() {

71 const image = await this.page.screenshot({ type: "png" });

72 return {

73 type: "computer_screenshot",

74 image_url: "data:image/png;base64," + image.toString("base64"),

75 };

76 }

77 

78 async allowed(action) {

79 if (action.type === "screenshot") return true;

80 if (

81 action.type !== "click" ||

82 action.button !== "left" ||

83 action.keys?.length

84 )

85 return false;

86 const box = await this.page.locator("summary").boundingBox();

87 return Boolean(

88 box &&

89 box.x <= action.x &&

90 action.x < box.x + box.width &&

91 box.y <= action.y &&

92 action.y < box.y + box.height

93 );

94 }

95 

96 async click(action) {

97 // The page may have changed after approval, so bind the click again.

98 if (!(await this.allowed(action)))

99 throw new Error("Pickup details moved or the click is not allowed.");

100 await this.page.mouse.click(action.x, action.y);

101 }

102 

103 refuse(action) {

104 throw new Error(`This exercise does not allow ${action.type}.`);

105 }

106 

107 double_click = this.refuse;

108 drag = this.refuse;

109 keypress = this.refuse;

110 move = this.refuse;

111 scroll = this.refuse;

112 type = this.refuse;

113 wait = this.refuse;

114}

115 

116async function main() {

117 const client = new OpenAI({ timeout: 30_000, maxRetries: 0 });

118 const browser = await chromium.launch({ env: {}, chromiumSandbox: true });

119 try {

120 const page = await browser.newPage({

121 viewport: { width: 1024, height: 768 },

122 });

123 await page.route("**/*", (route) => route.abort());

124 await page.setContent(

125 "<title>Parcel pickup</title><main>" +

126 "<h1>Parcel pickup</h1><p>Order #1042 is ready.</p>" +

127 "<details><summary>Show pickup details</summary>" +

128 "<p>Locker 18. Pickup code: 6249.</p></details></main>"

129 );

130 const driver = new PickupDriver(page);

131 const tools = new ComputerToolset(driver);

132 try {

133 const response = await tools.run(

134 client,

135 {

136 model: "gpt-6.1-sol",

137 store: false,

138 input:

139 "Take a screenshot. Open 'Show pickup details' and report the locker and pickup code. Use only screenshots and left clicks.",

140 },

141 { beforeAction: (action) => driver.allowed(action), maxSteps: 6 }

142 );

143 if ((await page.locator("details").getAttribute("open")) === null) {

144 throw new Error("Pickup details are still closed.");

145 }

146 console.log(

147 "Verified pickup details:",

148 await page.locator("details p").innerText()

149 );

150 console.log("Answer:", response.output_text);

151 } finally {

152 tools.close();

153 }

154 } finally {

155 await browser.close();

156 }

157}

158 

159await main();

160```

161 

162```python

163import asyncio

164import base64

165 

166from openai import AsyncComputerToolset, AsyncOpenAI

167from playwright.async_api import async_playwright

168 

169 

170class PickupDriver:

171 def __init__(self, page):

172 self.page = page

173 

174 async def screenshot(self):

175 image = await self.page.screenshot(type="png")

176 return {

177 "type": "computer_screenshot",

178 "image_url": "data:image/png;base64," + base64.b64encode(image).decode(),

179 }

180 

181 async def allowed(self, action):

182 if action.type == "screenshot":

183 return True

184 if action.type != "click" or action.button != "left" or action.keys:

185 return False

186 box = await self.page.locator("summary").bounding_box()

187 return bool(

188 box

189 and box["x"] <= action.x < box["x"] + box["width"]

190 and box["y"] <= action.y < box["y"] + box["height"]

191 )

192 

193 async def click(self, action):

194 # The page may have changed after approval, so bind the click again.

195 if not await self.allowed(action):

196 raise RuntimeError("Pickup details moved or the click is not allowed.")

197 await self.page.mouse.click(action.x, action.y)

198 

199 async def refuse(self, action):

200 raise RuntimeError(f"This exercise does not allow {action.type}.")

201 

202 double_click = drag = keypress = move = scroll = type = wait = refuse

203 

204 

205async def main():

206 async with (

207 async_playwright() as playwright,

208 AsyncOpenAI(timeout=30, max_retries=0) as client,

209 ):

210 browser = await playwright.chromium.launch(env={}, chromium_sandbox=True)

211 try:

212 page = await browser.new_page(viewport={"width": 1024, "height": 768})

213 await page.route("**/*", lambda route: route.abort())

214 await page.set_content(

215 "<title>Parcel pickup</title><main>"

216 "<h1>Parcel pickup</h1><p>Order #1042 is ready.</p>"

217 "<details><summary>Show pickup details</summary>"

218 "<p>Locker 18. Pickup code: 6249.</p></details></main>"

219 )

220 driver = PickupDriver(page)

221 tools = AsyncComputerToolset(driver)

222 try:

223 response = await tools.run(

224 client,

225 {

226 "model": "gpt-6.1-sol",

227 "store": False,

228 "input": "Take a screenshot. Open 'Show pickup details' and report the locker and pickup code. Use only screenshots and left clicks.",

229 },

230 before_action=driver.allowed,

231 max_steps=6,

232 )

233 if await page.locator("details").get_attribute("open") is None:

234 raise RuntimeError("Pickup details are still closed.")

235 print(

236 "Verified pickup details:",

237 await page.locator("details p").inner_text(),

238 )

239 print("Answer:", response.output_text)

240 finally:

241 tools.close()

242 finally:

243 await browser.close()

244 

245 

246if __name__ == "__main__":

247 asyncio.run(main())

248```

249 

250 

251Run `.venv/bin/python pickup.py` or `node pickup.mjs`. On success, the script prints `Verified pickup details: Locker 18. Pickup code: 6249.`, followed by the model's answer. Pending API safety checks stop this sample; add a human approval callback only when your application can handle them.

252 

253

254 

255

256 

257 

258The model types a name into a visitor badge form and clicks **Create badge**. The driver exposes only page reading, typing, and clicking. Its confirmation callback permits only this form's controls and the name Mira. The script verifies the page result before reporting success.

259 

260Save the matching example as `badge.py` or `badge.mjs`:

261 

262Create a visitor badge in a local browser

263 

264```javascript

265import OpenAI from "openai";

266import { BrowserToolset } from "openai/helpers/browser";

267import { ResponsesToolRunner } from "openai/helpers/responses-tool-runner";

268import { chromium } from "playwright";

269 

270class BadgeForm {

271 constructor(page) {

272 this.page = page;

273 }

274 

275 async state() {

276 return {

277 tabs: [

278 {

279 id: "badge",

280 title: await this.page.title(),

281 url: this.page.url(),

282 active: true,

283 },

284 ],

285 };

286 }

287 

288 async read_page(_args) {

289 return {

290 version: 1,

291 text: await this.page.locator("body").innerText(),

292 elements: [

293 { ref: "name", role: "textbox", label: "Name" },

294 { ref: "save", role: "button", label: "Create badge" },

295 ],

296 omitted: [],

297 };

298 }

299 

300 async type(args) {

301 await this.page.locator("#name").fill(args.text);

302 }

303 

304 async click(_args) {

305 await this.page.locator("#save").click();

306 }

307 

308 async confirm({ member, arguments: args }) {

309 if (

310 this.page.url() !== "about:blank" ||

311 (args.tab_id !== null && args.tab_id !== "badge")

312 ) {

313 return false;

314 }

315 return (

316 (member === "type" && args.target === "name" && args.text === "Mira") ||

317 (member === "click" && args.target === "save")

318 );

319 }

320}

321 

322async function main() {

323 const client = new OpenAI({ timeout: 30_000, maxRetries: 0 });

324 const browser = await chromium.launch({ env: {}, chromiumSandbox: true });

325 try {

326 const page = await browser.newPage();

327 await page.route("**/*", (route) => route.abort());

328 await page.setContent(`

329 <title>Visitor badge</title>

330 <label>Name <input id="name"></label>

331 <button id="save" onclick="document.querySelector('output').innerText =

332 'Badge ready: ' + document.querySelector('#name').value">Create badge</button>

333 <output></output>

334 `);

335 const driver = new BadgeForm(page);

336 const tools = new BrowserToolset(driver, {

337 members: ["read_page", "type", "click"],

338 confirm: driver.confirm.bind(driver),

339 });

340 const response = await new ResponsesToolRunner({ browser: tools }).run(

341 client,

342 {

343 model: "gpt-6-astra",

344 store: false,

345 input: "Create a visitor badge for Mira.",

346 },

347 { maxSteps: 6 }

348 );

349 const result = await page.locator("output").innerText();

350 if (result !== "Badge ready: Mira") {

351 throw new Error(`The badge was not created: ${JSON.stringify(result)}`);

352 }

353 console.log(result);

354 console.log("Answer:", response.output_text);

355 } finally {

356 await browser.close();

357 }

358}

359 

360await main();

361```

362 

363```python

364import asyncio

365 

366from openai import AsyncOpenAI

367from openai.lib.browser import AsyncBrowserToolset

368from openai.lib.responses_tool_runner import AsyncResponsesToolRunner

369from playwright.async_api import async_playwright

370 

371 

372class BadgeForm:

373 def __init__(self, page):

374 self.page = page

375 

376 async def state(self):

377 return {

378 "tabs": [

379 {

380 "id": "badge",

381 "title": await self.page.title(),

382 "url": self.page.url,

383 "active": True,

384 }

385 ]

386 }

387 

388 async def read_page(self, args):

389 return {

390 "version": 1,

391 "text": await self.page.locator("body").inner_text(),

392 "elements": [

393 {"ref": "name", "role": "textbox", "label": "Name"},

394 {"ref": "save", "role": "button", "label": "Create badge"},

395 ],

396 "omitted": [],

397 }

398 

399 async def type(self, args):

400 await self.page.locator("#name").fill(args["text"])

401 

402 async def click(self, args):

403 await self.page.locator("#save").click()

404 

405 async def confirm(self, context):

406 args = context["arguments"]

407 if self.page.url != "about:blank" or args["tab_id"] not in (None, "badge"):

408 return False

409 return (

410 context["member"] == "type"

411 and args["target"] == "name"

412 and args["text"] == "Mira"

413 ) or (context["member"] == "click" and args["target"] == "save")

414 

415 

416async def main():

417 async with (

418 async_playwright() as playwright,

419 AsyncOpenAI(timeout=30, max_retries=0) as client,

420 ):

421 browser = await playwright.chromium.launch(env={}, chromium_sandbox=True)

422 try:

423 page = await browser.new_page()

424 await page.route("**/*", lambda route: route.abort())

425 await page.set_content("""

426 <title>Visitor badge</title>

427 <label>Name <input id="name"></label>

428 <button id="save" onclick="document.querySelector('output').innerText =

429 'Badge ready: ' + document.querySelector('#name').value">Create badge</button>

430 <output></output>

431 """)

432 driver = BadgeForm(page)

433 tools = AsyncBrowserToolset(

434 driver, members=["read_page", "type", "click"], confirm=driver.confirm

435 )

436 response = await AsyncResponsesToolRunner(browser=tools).run(

437 client,

438 {

439 "model": "gpt-6-astra",

440 "store": False,

441 "input": "Create a visitor badge for Mira.",

442 },

443 max_steps=6,

444 )

445 result = await page.locator("output").inner_text()

446 if result != "Badge ready: Mira":

447 raise RuntimeError(f"The badge was not created: {result!r}")

448 print(result)

449 print("Answer:", response.output_text)

450 finally:

451 await browser.close()

452 

453 

454if __name__ == "__main__":

455 asyncio.run(main())

456```

457 

458 

459Run `.venv/bin/python badge.py` or `node badge.mjs`. On success, the script prints `Badge ready: Mira`, followed by the model's answer.

460 

461 

462 

463If the API rejects the tool or Chromium cannot start, [troubleshoot the run](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#troubleshoot-a-run). If clicks miss, [check the screenshot and coordinates](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#fix-missed-clicks).

464 

465### Check model access, cost, and data

466 

467- **Model:** For browser function tools, check that the model supports Responses and function calling. If your driver returns images, check image input too. For screenshot-based computer actions, check the model's **Tools** section for computer use. See the [models catalog](https://developers.openai.com/api/docs/models) and check your project's access.

468- **Cost:** Each Responses request can incur charges for input and output tokens. Screenshots count as image input. Check [API pricing](https://developers.openai.com/api/docs/pricing#built-in-tools), [estimate image input cost](https://developers.openai.com/api/docs/guides/image-cost-calculator), or [inspect request usage](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#inspect-usage-and-timing). A hosted environment may charge separately.

469- **Data:** The SDK sends returned page text or screenshots to OpenAI. The examples set `store` to `false`, but that does not remove every form of data retention. Review [data controls](https://developers.openai.com/api/docs/guides/your-data#storage-requirements-and-retention-controls-per-endpoint) and [image-input exceptions](https://developers.openai.com/api/docs/guides/your-data#image-and-file-inputs). Your environment provider has its own data policy.

470 

471### See and approve changes

472 

473Next, try the booking demo in [Python](https://github.com/openai/openai-python/blob/v3.31.0/examples/browser-use-demo/README.md) or [Node](https://github.com/openai/openai-node/blob/v7.33.0/examples/browser-use-demo/README.md). It fills a fictional reservation, asks for approval before submission, and checks the saved fields and confirmation. You can watch the browser, take control, return control, or stop. Both versions include a local rehearsal with a real browser and no API call.

474 

475To watch SDK actions on a desktop, try the [visual desktop demo](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#watch-and-control-a-local-desktop).

476 

477<a id="add-a-desktop"></a>

478 

479### Adapt the driver

480 

481Replace the sample page with an environment your application controls. For a browser driver, expose only the operations the task needs with `members`. For a computer driver, implement the required methods and reject unsupported actions. Keep the session open until the run ends, then close resources your application created.

482 

483Page content can influence the model. Use a fresh browser profile or isolated desktop. Limit network access, and keep credentials out of page text and screenshots. Ask for approval before making changes, and check the saved result before reporting success. See the [driver reference](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers) for method contracts, approval, URL policy, and recovery.

484 

485### Switch to Decisions for bounded choices

486 

487Use Responses when the model must choose actions and generate their arguments. Use Decisions only when your application already knows every allowed action and its complete arguments. For example, an app might offer `route_billing`, `route_technical`, and `review`. Decisions selects an option; your application still approves and performs it, then checks the result. Discard choices when the page changes. See the [Decisions guide](https://developers.openai.com/api/docs/guides/decisions).

488 

489To try it in the booking demo, use a key with Decisions access, set `OPENAI_DECISIONS_MODEL`, and restart the server. Start a new run and select **Decisions**.

490 

491## Use the API directly

492 

493Use the Responses API directly when you need to manage the tool loop yourself. You provide the environment and execute the model's requests. Choose how to connect it to your application:

8 494 

9<a id="choose-an-integration-path"></a>495<a id="choose-an-integration-path"></a>

10 496 


23<a id="option-3-use-a-code-execution-harness"></a>509<a id="option-3-use-a-code-execution-harness"></a>

24<a id="use-a-code-execution-harness"></a>510<a id="use-a-code-execution-harness"></a>

25 511 

26## Use code execution512### Use code execution

27 513 

28A code-execution integration gives the model a function tool that accepts a script. Your application runs the script in an isolated browser or desktop environment and returns its output, including screenshots. Keep the environment available between calls so the model can build on earlier work.514A code-execution integration gives the model a function tool that accepts a script. Your application runs the script in an isolated browser or desktop environment and returns its output, including screenshots. Keep the environment available between calls so the model can build on earlier work.

29 515 

30<a id="before-running-the-examples"></a>516<a id="before-running-the-examples"></a>

31 517 

32### Run the sample app518#### Run the sample app

33 519 

34The [CUA sample app](https://github.com/openai/openai-cua-sample-app#first-run) includes JavaScript/Playwright and Python/PyAutoGUI implementations, with local tasks and a shared console:520The [CUA sample app](https://github.com/openai/openai-cua-sample-app#first-run) includes JavaScript/Playwright and Python/PyAutoGUI implementations, with local tasks and a shared console:

35 521 


41 527 

42<a id="code-execution-harness-examples"></a>528<a id="code-execution-harness-examples"></a>

43 529 

44### Connect your own runtime530#### Connect your own runtime

45 531 

46The following example shows the API loop for a runtime you provide. Python, Ruby, and Go send Python code to a desktop runtime that uses PyAutoGUI; JavaScript uses Playwright to operate a browser. Each client exposes an ordinary function tool and returns text or images with the original `call_id`.532The following example shows the API loop for a runtime you provide. Python, Ruby, and Go send Python code to a desktop runtime that uses PyAutoGUI; JavaScript uses Playwright to operate a browser. Each client exposes an ordinary function tool and returns text or images with the original `call_id`.

47 533 


376 862 

377For a complete client adapter and the expected text and image output shape, see [Connect to your execution service](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#connect-to-your-execution-service). The service interface in those examples belongs to your application; it is not an OpenAI-hosted endpoint.863For a complete client adapter and the expected text and image output shape, see [Connect to your execution service](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#connect-to-your-execution-service). The service interface in those examples belongs to your application; it is not an OpenAI-hosted endpoint.

378 864 

379### Preserve state and return observations865#### Preserve state and return observations

380 866 

381Keep the browser or desktop session alive between calls. A persistent Python or JavaScript namespace can also preserve variables. Describe the available objects and helpers in the tool definition so the model knows what it can use.867Keep the browser or desktop session alive between calls. A persistent Python or JavaScript namespace can also preserve variables. Describe the available objects and helpers in the tool definition so the model knows what it can use.

382 868 


387<a id="provide-the-environment-and-control-the-loop"></a>873<a id="provide-the-environment-and-control-the-loop"></a>

388<a id="option-1-run-the-built-in-computer-use-loop"></a>874<a id="option-1-run-the-built-in-computer-use-loop"></a>

389 875 

390## Use the computer tool876### Use the computer tool

391 877 

392Use this alternative when your integration expects structured actions instead of generated code. For the recommended approach, start with [code execution](#use-code-execution).878Use this alternative when your integration expects structured actions instead of generated code.

393 879 

394To try this path, follow the [same sample-app setup](https://github.com/openai/openai-cua-sample-app#first-run), select **Native** mode, and run a built-in scenario. Use a model that supports the [computer tool](https://developers.openai.com/api/docs/models).880To try this path, follow the [same sample-app setup](https://github.com/openai/openai-cua-sample-app#first-run), select **Native** mode, and run a built-in scenario. Use a model that supports the [computer tool](https://developers.openai.com/api/docs/models).

395 881 


405<a id="2-send-the-task"></a>891<a id="2-send-the-task"></a>

406<a id="1-send-the-task"></a>892<a id="1-send-the-task"></a>

407 893 

408### Send the task894#### Send the task

409 895 

410Enable `computer` in the `tools` array and describe the result you want:896Enable `computer` in the `tools` array and describe the result you want:

411 897 


503<a id="3-run-every-returned-action"></a>989<a id="3-run-every-returned-action"></a>

504<a id="2-execute-the-requested-actions"></a>990<a id="2-execute-the-requested-actions"></a>

505 991 

506### Execute the requested actions992#### Execute the requested actions

507 993 

508A `computer_call` contains an ordered `actions` array. For example, this call selects the search field and types `penguin`:994A `computer_call` contains an ordered `actions` array. For example, this call selects the search field and types `penguin`:

509 995 


540<a id="4-return-the-updated-screen"></a>1026<a id="4-return-the-updated-screen"></a>

541<a id="3-return-the-screenshot"></a>1027<a id="3-return-the-screenshot"></a>

542 1028 

543### Return the screenshot1029#### Return the screenshot

544 1030 

545Return a `computer_call_output` whose `call_id` matches the call you handled. Use `previous_response_id` to continue the model conversation:1031Return a `computer_call_output` whose `call_id` matches the call you handled. Use `previous_response_id` to continue the model conversation:

546 1032 

Details

2 2 

3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

4 4 

5These recipes support the [computer use guide](https://developers.openai.com/api/docs/guides/tools-computer-use). Use the sections you need to connect the tool to your environment or expose an existing browser or desktop interface.5Use these recipes to connect the [computer use tool](https://developers.openai.com/api/docs/guides/tools-computer-use) to a browser or desktop you manage. They cover environment setup, action handling, screenshots, and user consent.

6 

7For a first hands-on exercise, [try the booking demo](https://developers.openai.com/api/docs/guides/tools-computer-use#see-and-approve-changes) in Python or Node. Select the local rehearsal to fill a fictional reservation, approve submission, and check the saved result in a real local browser. Rehearsal uses scripted choices and needs no API key. It tests the sample's own driver. To adapt it, follow [Check a custom driver locally](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#check-a-custom-driver-locally) and add checks for your environment.

8 

9If you use the SDK helpers, the [driver reference](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers) covers driver methods, approvals, session cleanup, and failure handling. The direct API recipes below show how to manage the action loop yourself.

10 

11## Choose a hosted environment

12 

13See [hosted environment options](https://developers.openai.com/api/docs/guides/tools-computer-use#connect-a-hosted-environment) for partner guides.

14 

15## Watch and control a local desktop

16 

17Run this on your own computer with a local Docker engine and Docker Compose. It starts a Linux desktop with an editor. You can see and control the desktop in your browser. The initial exercise uses `ComputerToolset.execute` with fixed actions and needs no API key. The optional live run lets the Responses computer tool choose actions.

18 

191. Create an empty directory. Save these two files there.

20 

21Dockerfile

22 

23```dockerfile

24FROM debian:bookworm-slim

25ENV DEBIAN_FRONTEND=noninteractive

26ENV DISPLAY=:99

27 

28RUN apt-get update && apt-get install -y --no-install-recommends \

29 xfce4 \

30 dbus-x11 \

31 xvfb \

32 xdotool \

33 imagemagick \

34 x11-utils \

35 firefox-esr \

36 && rm -rf /var/lib/apt/lists/* \

37 && useradd --create-home --shell /bin/sh myuser

38 

39USER myuser

40WORKDIR /home/myuser

41 

42USER root

43RUN apt-get update && apt-get install -y --no-install-recommends \

44 mousepad novnc websockify x11vnc \

45 && rm -rf /var/lib/apt/lists/*

46USER myuser

47RUN touch /home/myuser/dispatch.txt

48CMD set -eu; \

49 Xvfb :99 -screen 0 1280x800x24 -nolisten tcp & \

50 display_pid=$!; \

51 until xdpyinfo -display :99 >/dev/null 2>&1; do \

52 kill -0 "$display_pid"; sleep 0.2; \

53 done; \

54 dbus-run-session -- startxfce4 & \

55 xdotool search --sync --onlyvisible --class xfce4-panel >/dev/null; \

56 mousepad /home/myuser/dispatch.txt & \

57 exec x11vnc -display :99 -listen 0.0.0.0 -forever -shared -nopw -rfbport 5900

58```

59 

60 

61compose.yaml

62 

63```plain

64services:

65 desktop:

66 build:

67 context: .

68 image: cua-visual-desktop-demo

69 container_name: cua-visual-desktop

70 init: true

71 networks: [desktop]

72 cap_drop: [ALL]

73 security_opt: [no-new-privileges:true]

74 pids_limit: 256

75 mem_limit: 2g

76 cpus: 2

77 shm_size: 512m

78 viewer:

79 image: cua-visual-desktop-demo

80 pull_policy: never

81 command: ["websockify", "--web=/usr/share/novnc", "6080", "desktop:5900"]

82 depends_on: [desktop]

83 init: true

84 ports:

85 - "127.0.0.1:6080:6080"

86 networks: [desktop, viewer]

87 cap_drop: [ALL]

88 security_opt: [no-new-privileges:true]

89 pids_limit: 64

90 mem_limit: 512m

91 cpus: 1

92networks:

93 desktop:

94 internal: true

95 viewer: {}

96```

97 

98 

992. Start the desktop from that directory:

100 

101```bash

102docker compose up --build -d

103```

104 

105Open [the local desktop](http://127.0.0.1:6080/vnc.html?autoconnect=1&resize=scale). Wait until `dispatch.txt` appears in the editor. This is the live Linux desktop, so you can type in the window yourself.

106 

1073. Install one SDK on the host:

108 

109```bash

110# Python 3.11+

111python3 -m venv .venv

112.venv/bin/python -m pip install openai==3.31.0

113 

114# Or Node.js 22.6+

115npm install openai@7.33.0

116```

117 

118On Windows, use `py -3` instead of `python3` and `.venv\Scripts\python.exe` instead of `.venv/bin/python`.

119 

120Save the matching sample as `dispatch.py` or `dispatch.mjs` in the same directory.

121 

122Write and verify a dispatch note

123 

124```javascript

125import { execFile } from "node:child_process";

126import { promisify } from "node:util";

127import { setTimeout as delay } from "node:timers/promises";

128import OpenAI, { ComputerToolset } from "openai";

129 

130const note = "Dispatch #1042: ready for pickup.";

131const exec = promisify(execFile);

132 

133async function desktop(...args) {

134 const { stdout } = await exec(

135 "docker",

136 ["exec", "cua-visual-desktop", ...args],

137 {

138 encoding: "buffer",

139 timeout: 10_000,

140 maxBuffer: 4 * 1024 * 1024,

141 }

142 );

143 return stdout;

144}

145 

146class EditorDriver {

147 async screenshot() {

148 const image = await desktop("import", "-window", "root", "png:-");

149 return {

150 type: "computer_screenshot",

151 image_url: "data:image/png;base64," + image.toString("base64"),

152 };

153 }

154 

155 async focusEditor() {

156 const windows = await desktop(

157 "xdotool",

158 "search",

159 "--onlyvisible",

160 "--class",

161 "mousepad"

162 );

163 const window = windows.toString().trim().split("\n").at(-1);

164 if (!window) throw new Error("The dispatch editor is not open.");

165 await desktop("xdotool", "windowactivate", "--sync", window);

166 }

167 

168 async type(action) {

169 if (action.text !== note)

170 throw new Error("Only the dispatch note is allowed.");

171 await this.focusEditor();

172 await desktop(

173 "xdotool",

174 "type",

175 "--clearmodifiers",

176 "--delay",

177 "25",

178 "--",

179 action.text

180 );

181 }

182 

183 async keypress(action) {

184 const keys = action.keys.join("+");

185 if (keys !== "CTRL+A" && keys !== "CTRL+S") {

186 throw new Error("Only Select All and Save are allowed.");

187 }

188 await this.focusEditor();

189 await desktop("xdotool", "key", "--clearmodifiers", keys.toLowerCase());

190 }

191 

192 refuse(action) {

193 throw new Error(`This exercise does not allow ${action.type}.`);

194 }

195 

196 click = this.refuse;

197 double_click = this.refuse;

198 drag = this.refuse;

199 move = this.refuse;

200 scroll = this.refuse;

201 wait = this.refuse;

202}

203 

204const tools = new ComputerToolset(new EditorDriver());

205try {

206 if (process.argv.includes("--live")) {

207 const existing = (await desktop("cat", "/home/myuser/dispatch.txt"))

208 .toString()

209 .trim();

210 if (existing === note) {

211 throw new Error(

212 "Clear the note in the editor and save before a live run."

213 );

214 }

215 const client = new OpenAI({ timeout: 30_000, maxRetries: 0 });

216 await tools.run(

217 client,

218 {

219 model: "gpt-6.1-sol",

220 store: false,

221 input:

222 `The open editor has dispatch.txt. Replace its content with exactly "${note}". ` +

223 "Use screenshot, CTRL+A, type, CTRL+S only. The driver focuses the editor.",

224 },

225 { maxSteps: 6 }

226 );

227 } else {

228 await tools.execute({

229 id: "cu_dispatch",

230 call_id: "call_dispatch",

231 type: "computer_call",

232 status: "completed",

233 pending_safety_checks: [],

234 actions: [

235 { type: "keypress", keys: ["CTRL", "A"] },

236 { type: "type", text: note },

237 { type: "keypress", keys: ["CTRL", "S"] },

238 ],

239 });

240 }

241 // Saving is asynchronous; check the file independently of the screenshot.

242 let saved = "";

243 for (let attempt = 0; attempt < 20; attempt += 1) {

244 saved = (await desktop("cat", "/home/myuser/dispatch.txt"))

245 .toString()

246 .trim();

247 if (saved === note) break;

248 await delay(100);

249 }

250 if (saved !== note)

251 throw new Error(`The editor did not save the note: ${saved}`);

252 console.log("Verified:", saved);

253} finally {

254 tools.close();

255}

256```

257 

258```python

259import base64

260import subprocess

261import sys

262import time

263 

264from openai import ComputerToolset, OpenAI

265from openai.types.responses import ResponseComputerToolCall, computer_action

266 

267NOTE = "Dispatch #1042: ready for pickup."

268 

269 

270def desktop(*args):

271 return subprocess.check_output(

272 ["docker", "exec", "cua-visual-desktop", *args], timeout=10

273 )

274 

275 

276class EditorDriver:

277 def screenshot(self):

278 image = desktop("import", "-window", "root", "png:-")

279 return {

280 "type": "computer_screenshot",

281 "image_url": "data:image/png;base64," + base64.b64encode(image).decode(),

282 }

283 

284 def focus_editor(self):

285 window = desktop(

286 "xdotool", "search", "--onlyvisible", "--class", "mousepad"

287 ).splitlines()[-1]

288 desktop("xdotool", "windowactivate", "--sync", window.decode())

289 

290 def type(self, action):

291 if action.text != NOTE:

292 raise ValueError("Only the dispatch note is allowed.")

293 self.focus_editor()

294 desktop(

295 "xdotool", "type", "--clearmodifiers", "--delay", "25", "--", action.text

296 )

297 

298 def keypress(self, action):

299 keys = tuple(action.keys)

300 if keys not in (("CTRL", "A"), ("CTRL", "S")):

301 raise ValueError("Only Select All and Save are allowed.")

302 self.focus_editor()

303 desktop("xdotool", "key", "--clearmodifiers", "ctrl+" + keys[1].lower())

304 

305 def refuse(self, action):

306 raise ValueError(f"This exercise does not allow {action.type}.")

307 

308 click = double_click = drag = move = scroll = wait = refuse

309 

310 

311tools = ComputerToolset(EditorDriver())

312try:

313 if "--live" in sys.argv:

314 if desktop("cat", "/home/myuser/dispatch.txt").decode().strip() == NOTE:

315 raise RuntimeError(

316 "Clear the note in the editor and save before a live run."

317 )

318 with OpenAI(timeout=30, max_retries=0) as client:

319 tools.run(

320 client,

321 {

322 "model": "gpt-6.1-sol",

323 "store": False,

324 "input": (

325 f'The open editor has dispatch.txt. Replace its content with exactly "{NOTE}". '

326 "Use screenshot, CTRL+A, type, CTRL+S only. The driver focuses the editor."

327 ),

328 },

329 max_steps=6,

330 )

331 else:

332 tools.execute(

333 ResponseComputerToolCall(

334 id="cu_dispatch",

335 call_id="call_dispatch",

336 type="computer_call",

337 status="completed",

338 pending_safety_checks=[],

339 actions=[

340 computer_action.Keypress(type="keypress", keys=["CTRL", "A"]),

341 computer_action.Type(type="type", text=NOTE),

342 computer_action.Keypress(type="keypress", keys=["CTRL", "S"]),

343 ],

344 )

345 )

346 # Saving is asynchronous; check the file independently of the screenshot.

347 for _ in range(20):

348 saved = desktop("cat", "/home/myuser/dispatch.txt").decode().strip()

349 if saved == NOTE:

350 break

351 time.sleep(0.1)

352 if saved != NOTE:

353 raise RuntimeError(f"The editor did not save the note: {saved!r}")

354 print("Verified:", saved)

355finally:

356 tools.close()

357```

358 

359 

3604. Keep the viewer open and run the sample:

361 

362```bash

363.venv/bin/python dispatch.py

364# Or:

365node dispatch.mjs

366```

367 

368The SDK selects the editor's text, types a fictional note, saves it, and captures the desktop. It independently reads the guest file and prints `Verified: Dispatch #1042: ready for pickup.` The text should also appear in the viewer. The driver focuses the editor before every action and refuses inputs beyond the fixed note, Select All, and Save. Wait for the command to finish before typing in the viewer again.

369 

370 

371 

372### Run the same task with the Responses API

373 

374 

375 

376Set `OPENAI_API_KEY` on the host using the [API key setup instructions](https://developers.openai.com/api/docs/quickstart#create-and-export-an-api-key). Use a project with access to the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). Clear the editor and save the empty file before testing the live path. The sample refuses to run if the note is already saved, so a previous run cannot pass the check for it.

377 

378```bash

379.venv/bin/python dispatch.py --live

380# Or:

381node dispatch.mjs --live

382```

383 

384This run uses at most six Responses requests and verifies the file in the same way. It stops if the model requests a safety check or an input this driver does not support. API usage is billed to your project. Only the host SDK reads the API key; screenshots and prompts go to OpenAI.

385 

386 

387 

388 

389 

3905. Stop and remove the guest after either run, including a failed run:

391 

392```bash

393docker compose down

394```

395 

396The helper closes when the script exits; the guest stays open so you can inspect the result. `docker compose down` removes the guest, viewer, and their networks. It leaves the local build image and files in your working directory.

397 

398Use a local Docker engine on a trusted single-user host. The viewer has no password and listens at `127.0.0.1:6080` on the Docker host. Other local users can access it. Only the viewer connects to both networks; it serves the desktop at the fixed address `desktop:5900`. The guest stays on the internal network with no host files, credentials, or Docker socket mounted. Keep those networks and the port binding for this exercise. For a deployed service, authenticate each session and restrict outbound network access.

399 

400If the page will not connect, run `docker compose logs desktop viewer`. If the guest name is in use, stop only your previous demo; do not reuse someone else's desktop. If port 6080 is occupied, free the port or choose a different host port in `compose.yaml` and open that port in your browser. Keep the `127.0.0.1` binding. If the editor is closed or the command fails, run `docker compose down`, start again, and wait for the editor before retrying.

6 401 

7## Prepare an environment402## Prepare an environment

8 403 


66 461 

67 462 

68 463 

69### Set up a local virtual machine464### Set up a caller-managed Docker container

70 465 

71 466 

72 467 

73For a desktop application, provide a VM or container and translate the returned actions into operating system input events.468This recipe uses a container you manage and translates returned actions into operating system input events. It is separate from the [isolated desktop example](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#use-an-isolated-desktop).

74 469 

75#### Create a Docker image470#### Create a Docker image

76 471 

77The following Dockerfile starts an Ubuntu desktop with `Xvfb`, `x11vnc`, and Firefox:472Save this Dockerfile in an empty directory. It installs a Debian desktop with `Xvfb` and Firefox. The container runs as an ordinary user. The action and screenshot helpers below use `docker exec`, so this recipe does not start a VNC server or publish any ports.

78 473 

79Dockerfile474Dockerfile

80 475 

81```dockerfile476```dockerfile

82FROM ubuntu:22.04477FROM debian:bookworm-slim

83ENV DEBIAN_FRONTEND=noninteractive478ENV DEBIAN_FRONTEND=noninteractive

479ENV DISPLAY=:99

84 480 

85RUN apt-get update && apt-get install -y \481RUN apt-get update && apt-get install -y --no-install-recommends \

86 xfce4 \482 xfce4 \

87 xfce4-goodies \483 dbus-x11 \

88 x11vnc \

89 xvfb \484 xvfb \

90 xdotool \485 xdotool \

91 imagemagick \486 imagemagick \

92 x11-apps \487 x11-utils \

93 sudo \

94 software-properties-common \

95 firefox-esr \488 firefox-esr \

96 && apt-get remove -y light-locker xfce4-screensaver xfce4-power-manager || true \489 && rm -rf /var/lib/apt/lists/* \

97 && apt-get clean && rm -rf /var/lib/apt/lists/*490 && useradd --create-home --shell /bin/sh myuser

98 491 

99RUN useradd -ms /bin/bash myuser \

100 && echo "myuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers

101USER myuser492USER myuser

102WORKDIR /home/myuser493WORKDIR /home/myuser

103 494 

104RUN x11vnc -storepasswd secret /home/myuser/.vncpass495CMD set -eu; \

105 496 Xvfb :99 -screen 0 1280x800x24 -nolisten tcp & \

106EXPOSE 5900497 display_pid=$!; \

107CMD ["/bin/sh", "-c", "\498 until xdpyinfo -display :99 >/dev/null 2>&1; do \

108 Xvfb :99 -screen 0 1280x800x24 >/dev/null 2>&1 & \499 kill -0 "$display_pid"; sleep 0.2; \

109 x11vnc -display :99 -forever -rfbauth /home/myuser/.vncpass -listen 0.0.0.0 -rfbport 5900 >/dev/null 2>&1 & \500 done; \

110 export DISPLAY=:99 && \501 exec dbus-run-session -- startxfce4

111 startxfce4 >/dev/null 2>&1 & \

112 sleep 2 && echo 'Container running!' && \

113 tail -f /dev/null \

114"]

115```502```

116 503 

117 504 


121docker build -t cua-image .508docker build -t cua-image .

122```509```

123 510 

124Run the container:511Run the container without a network:

512 

513```bash

514docker run --rm -d --name cua-image \

515 --network none \

516 --cap-drop ALL --security-opt no-new-privileges \

517 --pids-limit 256 --memory 2g --cpus 2 --shm-size 512m \

518 cua-image

519```

520 

521Check that the desktop is ready and save its current screen on the host:

125 522 

126```bash523```bash

127docker run --rm -it --name cua-image -p 5900:5900 -e DISPLAY=:99 cua-image524docker exec cua-image xdpyinfo -display :99

525docker exec cua-image xdotool search --sync --onlyvisible --class xfce4-panel

526docker exec cua-image import -window root png:- > desktop.png

128```527```

129 528 

130Create a helper for shelling into the container:529The second command waits until the desktop panel appears. Open `desktop.png` to view the desktop. Run the screenshot command again after actions to view the latest screen. If the display is still starting, wait a moment and repeat the commands. For a complete Python or Node task, use the [isolated desktop example](https://developers.openai.com/api/docs/guides/tools-computer-use-sdk-drivers#use-an-isolated-desktop).

530 

531The container can use its own loopback interface but cannot reach external sites, other containers, or the host network. Do not mount your personal files, credentials, or the Docker socket. For internet tasks, use a VM or an environment with egress rules that allow only the sites required by the task.

532 

533Create a helper that passes the command arguments directly to the container:

131 534 

132Execute commands on the container535Execute commands on the container

133 536 


174import subprocess577import subprocess

175 578 

176 579 

177def docker_exec(cmd: str, container_name: str, decode: bool = True):580def docker_exec(container_name, executable, args, *, decode=True, env=None):

178 safe_cmd = cmd.replace('"', '\\"')581 environment_args = [

179 docker_cmd = f'docker exec {container_name} sh -c "{safe_cmd}"'582 part

180 output = subprocess.check_output(docker_cmd, shell=True)583 for name, value in (env or {}).items()

181 if decode:584 for part in ("--env", f"{name}={value}")

182 return output.decode("utf-8", errors="ignore")585 ]

183 return output586 output = subprocess.check_output(

587 [

588 "docker",

589 "exec",

590 *environment_args,

591 container_name,

592 executable,

593 *map(str, args),

594 ]

595 )

596 return output.decode("utf-8", errors="ignore") if decode else output

184 597 

185 598 

186class VM:599class VM:


193```606```

194 607 

195 608 

609When you finish, stop the container:

610 

611```bash

612docker stop cua-image

613```

614 

615`--rm` discards the files inside the container when it stops. Screenshots you saved on the host remain until you remove them. This setup is for local experiments on a trusted host. For a deployed service, use per-session authentication, isolated hosts or VMs, resource and egress policies, and audited cleanup.

616 

196 617 

197 618 

198 619 


826 await dockerExec(1247 await dockerExec(

827 vm.containerName,1248 vm.containerName,

828 "xdotool",1249 "xdotool",

829 ["type", "--delay", 0, action.text],1250 ["type", "--delay", 0, "--", action.text],

830 { env: { DISPLAY: vm.display } }1251 { env: { DISPLAY: vm.display } }

831 );1252 );

832 break;1253 break;


865 reject_modifiers(action)1286 reject_modifiers(action)

866 button = normalize_xdotool_button(getattr(action, "button", "left"))1287 button = normalize_xdotool_button(getattr(action, "button", "left"))

867 docker_exec(1288 docker_exec(

868 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y} click {button}",

869 vm.container_name,1289 vm.container_name,

1290 "xdotool",

1291 ["mousemove", action.x, action.y, "click", button],

1292 env={"DISPLAY": vm.display},

870 )1293 )

871 case "double_click":1294 case "double_click":

872 reject_modifiers(action)1295 reject_modifiers(action)

873 docker_exec(1296 docker_exec(

874 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y} click --repeat 2 1",

875 vm.container_name,1297 vm.container_name,

1298 "xdotool",

1299 ["mousemove", action.x, action.y, "click", "--repeat", 2, 1],

1300 env={"DISPLAY": vm.display},

876 )1301 )

877 case "drag":1302 case "drag":

878 reject_modifiers(action)1303 reject_modifiers(action)


881 raise ValueError("drag action requires at least two path points")1306 raise ValueError("drag action requires at least two path points")

882 start_x, start_y = path[0]1307 start_x, start_y = path[0]

883 docker_exec(1308 docker_exec(

884 f"DISPLAY={vm.display} xdotool mousemove {start_x} {start_y} mousedown 1",

885 vm.container_name,1309 vm.container_name,

1310 "xdotool",

1311 ["mousemove", start_x, start_y, "mousedown", 1],

1312 env={"DISPLAY": vm.display},

886 )1313 )

887 for x, y in path[1:]:1314 for x, y in path[1:]:

888 docker_exec(1315 docker_exec(

889 f"DISPLAY={vm.display} xdotool mousemove {x} {y}",

890 vm.container_name,1316 vm.container_name,

1317 "xdotool",

1318 ["mousemove", x, y],

1319 env={"DISPLAY": vm.display},

891 )1320 )

892 docker_exec(1321 docker_exec(

893 f"DISPLAY={vm.display} xdotool mouseup 1",

894 vm.container_name,1322 vm.container_name,

1323 "xdotool",

1324 ["mouseup", 1],

1325 env={"DISPLAY": vm.display},

895 )1326 )

896 case "move":1327 case "move":

897 reject_modifiers(action)1328 reject_modifiers(action)

898 docker_exec(1329 docker_exec(

899 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y}",

900 vm.container_name,1330 vm.container_name,

1331 "xdotool",

1332 ["mousemove", action.x, action.y],

1333 env={"DISPLAY": vm.display},

901 )1334 )

902 case "scroll":1335 case "scroll":

903 reject_modifiers(action)1336 reject_modifiers(action)


907 )1340 )

908 1341 

909 docker_exec(1342 docker_exec(

910 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y}",

911 vm.container_name,1343 vm.container_name,

1344 "xdotool",

1345 ["mousemove", action.x, action.y],

1346 env={"DISPLAY": vm.display},

912 )1347 )

913 for button in buttons:1348 for button in buttons:

914 docker_exec(1349 docker_exec(

915 f"DISPLAY={vm.display} xdotool click {button}",

916 vm.container_name,1350 vm.container_name,

1351 "xdotool",

1352 ["click", button],

1353 env={"DISPLAY": vm.display},

917 )1354 )

918 case "keypress":1355 case "keypress":

919 keys = "+".join(normalize_xdotool_key(key) for key in action.keys)1356 keys = "+".join(normalize_xdotool_key(key) for key in action.keys)

920 docker_exec(1357 docker_exec(

921 f"DISPLAY={vm.display} xdotool key '{keys}'",

922 vm.container_name,1358 vm.container_name,

1359 "xdotool",

1360 ["key", keys],

1361 env={"DISPLAY": vm.display},

923 )1362 )

924 case "type":1363 case "type":

925 docker_exec(1364 docker_exec(

926 f"DISPLAY={vm.display} xdotool type --delay 0 '{action.text}'",

927 vm.container_name,1365 vm.container_name,

1366 "xdotool",

1367 ["type", "--delay", 0, "--", action.text],

1368 env={"DISPLAY": vm.display},

928 )1369 )

929 case "wait":1370 case "wait":

930 time.sleep(2)1371 time.sleep(2)


1284 await dockerExec(1725 await dockerExec(

1285 vm.containerName,1726 vm.containerName,

1286 "xdotool",1727 "xdotool",

1287 ["type", "--delay", 0, action.text],1728 ["type", "--delay", 0, "--", action.text],

1288 { env: { DISPLAY: vm.display } }1729 { env: { DISPLAY: vm.display } }

1289 );1730 );

1290 break;1731 break;


1315 try:1756 try:

1316 for key in normalized_keys:1757 for key in normalized_keys:

1317 docker_exec(1758 docker_exec(

1318 f"DISPLAY={vm.display} xdotool keydown '{key}'",

1319 vm.container_name,1759 vm.container_name,

1760 "xdotool",

1761 ["keydown", key],

1762 env={"DISPLAY": vm.display},

1320 )1763 )

1321 pressed_keys.append(key)1764 pressed_keys.append(key)

1322 1765 


1324 finally:1767 finally:

1325 for key in reversed(pressed_keys):1768 for key in reversed(pressed_keys):

1326 docker_exec(1769 docker_exec(

1327 f"DISPLAY={vm.display} xdotool keyup '{key}'",

1328 vm.container_name,1770 vm.container_name,

1771 "xdotool",

1772 ["keyup", key],

1773 env={"DISPLAY": vm.display},

1329 )1774 )

1330 1775 

1331 1776 


1338 vm,1783 vm,

1339 getattr(action, "keys", None),1784 getattr(action, "keys", None),

1340 lambda: docker_exec(1785 lambda: docker_exec(

1341 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y} click {button}",

1342 vm.container_name,1786 vm.container_name,

1787 "xdotool",

1788 ["mousemove", action.x, action.y, "click", button],

1789 env={"DISPLAY": vm.display},

1343 ),1790 ),

1344 )1791 )

1345 case "double_click":1792 case "double_click":


1347 vm,1794 vm,

1348 getattr(action, "keys", None),1795 getattr(action, "keys", None),

1349 lambda: docker_exec(1796 lambda: docker_exec(

1350 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y} click --repeat 2 1",

1351 vm.container_name,1797 vm.container_name,

1798 "xdotool",

1799 ["mousemove", action.x, action.y, "click", "--repeat", 2, 1],

1800 env={"DISPLAY": vm.display},

1352 ),1801 ),

1353 )1802 )

1354 case "drag":1803 case "drag":


1359 def do_drag():1808 def do_drag():

1360 start_x, start_y = path[0]1809 start_x, start_y = path[0]

1361 docker_exec(1810 docker_exec(

1362 f"DISPLAY={vm.display} xdotool mousemove {start_x} {start_y} mousedown 1",

1363 vm.container_name,1811 vm.container_name,

1812 "xdotool",

1813 ["mousemove", start_x, start_y, "mousedown", 1],

1814 env={"DISPLAY": vm.display},

1364 )1815 )

1365 for x, y in path[1:]:1816 for x, y in path[1:]:

1366 docker_exec(1817 docker_exec(

1367 f"DISPLAY={vm.display} xdotool mousemove {x} {y}",

1368 vm.container_name,1818 vm.container_name,

1819 "xdotool",

1820 ["mousemove", x, y],

1821 env={"DISPLAY": vm.display},

1369 )1822 )

1370 docker_exec(1823 docker_exec(

1371 f"DISPLAY={vm.display} xdotool mouseup 1",

1372 vm.container_name,1824 vm.container_name,

1825 "xdotool",

1826 ["mouseup", 1],

1827 env={"DISPLAY": vm.display},

1373 )1828 )

1374 1829 

1375 with_modifiers(vm, getattr(action, "keys", None), do_drag)1830 with_modifiers(vm, getattr(action, "keys", None), do_drag)


1378 vm,1833 vm,

1379 getattr(action, "keys", None),1834 getattr(action, "keys", None),

1380 lambda: docker_exec(1835 lambda: docker_exec(

1381 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y}",

1382 vm.container_name,1836 vm.container_name,

1837 "xdotool",

1838 ["mousemove", action.x, action.y],

1839 env={"DISPLAY": vm.display},

1383 ),1840 ),

1384 )1841 )

1385 case "scroll":1842 case "scroll":


1390 1847 

1391 def do_scroll():1848 def do_scroll():

1392 docker_exec(1849 docker_exec(

1393 f"DISPLAY={vm.display} xdotool mousemove {action.x} {action.y}",

1394 vm.container_name,1850 vm.container_name,

1851 "xdotool",

1852 ["mousemove", action.x, action.y],

1853 env={"DISPLAY": vm.display},

1395 )1854 )

1396 for button in buttons:1855 for button in buttons:

1397 docker_exec(1856 docker_exec(

1398 f"DISPLAY={vm.display} xdotool click {button}",

1399 vm.container_name,1857 vm.container_name,

1858 "xdotool",

1859 ["click", button],

1860 env={"DISPLAY": vm.display},

1400 )1861 )

1401 1862 

1402 with_modifiers(vm, getattr(action, "keys", None), do_scroll)1863 with_modifiers(vm, getattr(action, "keys", None), do_scroll)

1403 case "keypress":1864 case "keypress":

1404 keys = "+".join(normalize_xdotool_key(key) for key in action.keys)1865 keys = "+".join(normalize_xdotool_key(key) for key in action.keys)

1405 docker_exec(1866 docker_exec(

1406 f"DISPLAY={vm.display} xdotool key '{keys}'",

1407 vm.container_name,1867 vm.container_name,

1868 "xdotool",

1869 ["key", keys],

1870 env={"DISPLAY": vm.display},

1408 )1871 )

1409 case "type":1872 case "type":

1410 docker_exec(1873 docker_exec(

1411 f"DISPLAY={vm.display} xdotool type --delay 0 '{action.text}'",

1412 vm.container_name,1874 vm.container_name,

1875 "xdotool",

1876 ["type", "--delay", 0, "--", action.text],

1877 env={"DISPLAY": vm.display},

1413 )1878 )

1414 case "wait":1879 case "wait":

1415 time.sleep(2)1880 time.sleep(2)


1826```python2291```python

1827def capture_screenshot(vm):2292def capture_screenshot(vm):

1828 return docker_exec(2293 return docker_exec(

1829 f"export DISPLAY={vm.display} && import -window root png:-",

1830 vm.container_name,2294 vm.container_name,

2295 "import",

2296 ["-window", "root", "png:-"],

2297 env={"DISPLAY": vm.display},

1831 decode=False,2298 decode=False,

1832 )2299 )

1833```2300```

Details

1# Browser and desktop drivers

2 

3> For the complete documentation index, see [llms.txt](/llms.txt). Markdown versions of documentation pages are available by appending `.md` to the page URL.

4 

5A driver translates allowed tool calls into actions in a browser or desktop you control. Use a browser driver when your application can report page text, controls, and tabs. Use a computer driver when the model must work from screenshots and coordinates. The SDK sends requests and dispatches calls; your application owns the session, permissions, and cleanup.

6 

7## Configure common controls

8 

9Start with the [browser example](https://developers.openai.com/api/docs/guides/tools-computer-use?sdk-task=browser#run-your-first-task) in Python or Node. It shows a complete driver, a confirmation policy, and a bounded Responses run. Keep the session open until the run ends. Python also provides synchronous `BrowserToolset` and `ResponsesToolRunner` classes for synchronous drivers.

10 

11| Goal | Where to set it | What to expect |

12| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |

13| Limit browser methods | Set `members` when constructing `BrowserToolset` or `AsyncBrowserToolset`. | Only selected, implemented methods are exposed. For read-only access with the example driver, choose `read_page` alone. [Select methods](#select-methods). |

14| Approve browser changes | Pass `confirm` to the `BrowserToolset` or `AsyncBrowserToolset` constructor. | Only an exact `true` or `True` allows the proposed action. Check its member, target, and arguments. [Approve actions](#approve-actions-and-own-the-session). |

15| Restrict browser navigation | Pass `urlPolicy` in Node or `url_policy` in Python to the `BrowserToolset` or `AsyncBrowserToolset` constructor. | Required if you enable `navigate` or `new_tab`. The SDK checks the requested URL before asking for confirmation. [Restrict URLs](#restrict-urls). |

16| Approve native computer actions | With `ResponsesToolRunner`, set Node's `computerOptions` in the run options; pass Python's `approve_safety_checks` and `before_action` to `run`. | Pending safety checks need explicit approval; the optional before-action callback can refuse any individual action. [Computer approval](#approve-actions-and-own-the-session). |

17| Bound a run | Set `maxSteps` in Node or `max_steps` in Python in the Responses runner's run options. | The default is 20 requests. The final allowed request cannot start new effects. |

18| Measure usage | Pass synchronous `onDiagnostic` in Node or `on_diagnostic` in Python to `ResponsesToolRunner.run`. | Add token usage from completed request records. If a completed request has no usage, report the total as incomplete. [Inspect usage and timing](#inspect-usage-and-timing). |

19 

20### Approve actions and own the session

21 

22Create drivers and approval callbacks in trusted application code. Keep executable configuration separate from untrusted page data, and return page data through the declared driver report fields.

23 

24A selected browser method that changes state or reads diagnostics requires `confirm`. Only exactly `true` in Node or `True` in Python approves an action. `navigate` and `new_tab` also require `urlPolicy` (Node) or `url_policy` (Python). Both callbacks receive a detached object containing `member`, validated `arguments`, prior `state`, and `call_id`; URL policy also receives the requested URL. They cannot change the validated action by editing that object.

25 

26`confirm` approves the fixed action arguments. Its `state` is a prior observation, not a lock on the document or focused control. A page can navigate or change focus while a person decides. For consent tied to a specific page or control, the driver must check the original target at execution. Disable `key` and refuse `type` with a null target in that flow; both use current focus. Apply equivalent target binding to other actions that depend on the current browser state.

27 

28To ask before reading a private page, list the read members in `confirmReads` (Node) or `confirm_reads` (Python). The same `confirm` callback runs before any driver state or content read. Its context has `phase: "before_read"` and `state: null` in Node or `None` in Python. Handle that case without reading page metadata. A refusal returns no page content or state. Existing reads keep their current behavior when this option is absent; mandatory action approvals still apply.

29 

30For computer calls, acknowledge all pending safety checks before the helper executes any actions. Node's `approveSafetyChecks` and Python's `approve_safety_checks` must return exactly true. A missing or declined callback stops the run. Optional `beforeAction` or `before_action` checks each action immediately before execution, including the final screenshot. If supplied, it must also approve explicitly. A model choice or completion message cannot grant approval. See [Computer use safety guidance](https://developers.openai.com/api/docs/guides/tools-computer-use#run-safely).

31 

32Node passes `{ signal }` as the final argument to both computer approval callbacks. Python supplies the optional `cancelled` keyword when the callback accepts it. Existing callbacks with fewer arguments remain supported. Use this context to close a pending approval prompt when the run stops. The helper waits for the callback to settle before releasing the session.

33 

34Computer approval covers the action's fixed arguments. It does not freeze the screen or focus while a person decides. If consent must apply to a particular target, the driver must bind the action to that target or reject it after a state change. Run computer actions in an isolated environment with limited network access.

35 

36### Restrict URLs

37 

38Enable `navigate` or `new_tab` only when the task needs them. Supply both `confirm` and `urlPolicy` in Node, or `confirm` and `url_policy` in Python. The URL callback receives `(context, url)`, with an optional execution context in Node or `cancelled` keyword in Python. It must return exactly true for the SDK to continue to `confirm`. A new blank tab with a null URL still requires confirmation but has no URL to check.

39 

40Parse the requested URL. Compare its protocol, hostname, and port with the destinations your task permits. Do not rely on a string prefix: `https://example.com.evil.test` must not count as `https://example.com`. A browser driver without navigation needs no URL callback. The [browser example](https://developers.openai.com/api/docs/guides/tools-computer-use?sdk-task=browser#run-your-first-task) leaves these methods disabled.

41 

42Enforce a network policy in the browser or environment as well. The callback checks a requested navigation, but it cannot restrict redirects, popups, page scripts, or embedded resources. Keep one isolated session between requests and close resources your application created. Leave borrowed resources with their owner.

43 

44### Inspect usage and timing

45 

46With `ResponsesToolRunner`, pass `onDiagnostic` in Node or `on_diagnostic` in Python to `run`. For each completed Responses request, the callback receives a record with `api: "responses"`, `stage: "request"`, `reason: "completed"`, and `usage`. The usage contains `input_tokens`, `output_tokens`, and `total_tokens` when the API supplies them; otherwise, the value is `null` in Node or `None` in Python. Sum each token field across completed requests to measure the run's usage. If any request has no usage, the totals are incomplete.

47 

48`ComputerToolset.run` does not report request usage through its diagnostic callback. The returned response can contain usage for the final request only. Use `ResponsesToolRunner` with `ComputerToolset` when you need per-request usage.

49 

50Diagnostic records also identify the API, stage, reason, call ID, member, and measured duration. They contain no raw error, page text, arguments, or image. An observer failure cannot approve an action or change its result. Keep call IDs and member names out of public logs if your application puts private data in them.

51 

52Use a synchronous observer. The runner does not wait for diagnostic work; schedule any asynchronous storage in your application.

53 

54Stage timings can overlap in eager runs because stream receipt may include action time. Do not add them together to calculate the run's total duration.

55 

56### Close and cancel work

57 

58A helper rejects overlapping calls with a busy error. A runner also rejects a second active run on the same instance. Keep one owner for each browser or desktop session.

59 

60Call the helper's `close()` to refuse new work. Its `status` is `closed` when no call remains active, or `draining` when an earlier call may still have effects. If the status is `draining`, await the original execute call before closing the driver or handing the session to another owner. The helper does not close your browser or desktop. Calling `close()` inside a driver callback does not wait on that same callback.

61 

62Cooperative Node driver methods and ordinary functions can accept a second context with `signal`. Python methods can accept the `cancelled` keyword. Check it during long work, including navigation and screenshots. Existing one-argument callbacks remain valid, but the SDK cannot force them to stop or undo an effect.

63 

64## Browser driver contract

65 

66Implement `state()` and only the operations you intend to expose. Node methods may return a value or a promise; Python supplies separate synchronous and asynchronous classes. Each operation receives one validated argument object or dictionary. For tab IDs and targets that allow no value, use explicit `null` in Node and `None` in Python.

67 

68Python rejects known synchronous or asynchronous mismatches when you create the helper. Use `async def` methods with `AsyncBrowserToolset`; preserve decorator metadata or return annotations for wrappers. Opaque wrappers are checked when called. Optional protocols such as `BrowserReadDriver` and `AsyncBrowserReadDriver` help you type a partial driver without implementing every operation. Node exports `BrowserArguments<Member>` and `BrowserResult<Member>` for the matching inputs and outputs.

69 

70`state()` reports `tabs`, an array of objects with `id`, `title`, `url`, and `active`. Use stable IDs and mark the real active tab. Exclude credentials and private information from titles and URLs. The SDK returns current state after ordinary operations, including failures. A refused [protected read](#approve-actions-and-own-the-session) returns a fixed refusal without reading or exposing state. If a required state read fails, the run stops. Node state readers receive `{ signal }`; Python state readers can accept the `cancelled` keyword. Use this context to end pending state reads when the caller stops the run. Existing readers without arguments remain supported.

71 

72Tab state reports HTTP and HTTPS origins, such as `https://example.com:8443`, without credentials, paths, queries, or fragments. Legacy state, without observation metadata, preserves `about:blank` and reports only the scheme for other URLs, such as `data:` or `file:`. Its 2,048-byte UTF-8 limit applies to the reported value, so a long page path does not prevent a state read. State with V1 observation metadata truncates titles to 1,024 UTF-8 bytes at a character boundary and sets `title_truncated` when needed. Titles and page content still need your application's privacy rules.

73 

74### Select methods

75 

76By default, the SDK exposes the following 18 methods when implemented and derives `list_tabs` from `state()`. Do not implement `list_tabs`. A `members` list can expose fewer methods, or explicitly opt in to the [scoped and pointer methods](#opt-in-to-scoped-and-pointer-methods) or [diagnostics, scripts, and file methods](#opt-in-to-diagnostics-scripts-and-file-transfers). Names must be unique, fixed members that the driver implements; only `list_tabs` needs no method beyond `state()`. An empty list exposes no methods. The SDK copies the selection at construction, keeps its own tool order, and rejects disabled calls before reading state or asking for consent.

77 

78To add an optional method while keeping implemented defaults, use `additionalMembers` in Node or `additional_members` in Python. Do not combine it with `members`. A requested addition must exist on the driver. `BROWSER_MEMBERS` and `DEFAULT_BROWSER_MEMBERS` list the fixed names without inspecting a driver.

79 

80Disabling `list_tabs` does not hide state returned by other successful operations. In Python and Node, `members` filters the browser helper. Ordinary function names may start with `browser_`, but must not match an enabled browser tool.

81 

82| Method | Arguments | Result | Confirmation |

83| --------------- | ------------------------------------------- | ---------------------------------------- | --------------- |

84| `navigate` | `url`, `tab_id` | Text or no result | Yes; URL policy |

85| `read_page` | `filter` ("all" or "interactive"), `tab_id` | Text or structured read | No |

86| `get_page_text` | `tab_id` | Text or structured read with no elements | No |

87| `find` | `query`, `tab_id` | Text or structured read | No |

88| `click` | `target`, `tab_id` | Text or no result | Yes |

89| `type` | `text`, `target` (may be null), `tab_id` | Text or no result | Yes |

90| `key` | `keys` (strings), `tab_id` | Text or no result | Yes |

91| `scroll` | `direction`, nonnegative `amount`, `tab_id` | Text or no result | Yes |

92| `scroll_to` | `target`, `tab_id` | Text or no result | Yes |

93| `wait_for` | `text`, `timeout_ms`, `tab_id` | Text or no result | No |

94| `select_option` | `target`, `values` (strings), `tab_id` | Text or no result | Yes |

95| `hover` | `target`, `tab_id` | Text or no result | Yes |

96| `screenshot` | `tab_id` | Native input image | No |

97| `zoom` | `region`, `tab_id` | Native input image | No |

98| `new_tab` | `url` (may be null) | Text or no result | Yes; URL policy |

99| `list_tabs` | None | Derived current state | No |

100| `switch_tab` | required `tab_id` | Text or no result | Yes |

101| `close_tab` | required `tab_id` | Text or no result | Yes |

102 

103Except for `switch_tab` and `close_tab`, a listed `tab_id` may be null. Direction is `up`, `down`, `left`, or `right`. Node's `BrowserArguments<Member>` describes each argument object; Python exposes named input types such as `TabInput`, `ReadPageInput`, and `ClickInput`.

104 

105For `read_page`, `find`, and `get_page_text`, return `null` or no value in Node, or `None` in Python, to supply only the updated browser state. Return an empty string when a read completes with no text.

106 

107Each Node operation in this table except `list_tabs` receives `{ signal }` as a second argument. `list_tabs` uses the state reader described above. Python passes `cancelled` as a keyword argument when the method accepts it. Use this context to stop pending work, including navigation and input. Existing one-argument methods remain supported.

108 

109For `wait_for`, use nonempty text of at most 256 UTF-8 bytes after whitespace normalization. The timeout must be from 1 through 30,000 milliseconds. Read the current page again after `scroll_to` before acting on a control.

110 

111### Report structured reads

112 

113`read_page`, `find`, and `get_page_text` can return `BrowserReadV1`: `version: 1`, visible `text`, an `elements` array, and an `omitted` array. Each element has a unique `ref`, a nonempty `label`, and one of seven roles: `button`, `textbox`, `combobox`, `checkbox`, `radio`, `slider`, or `listbox`.

114 

115Only report a field's `value` or its `options` (`label` and `value` pairs) when your application allows the model to see them. A checkbox or radio can report boolean `checked`; `listbox` options can report boolean `selected`. The SDK omits these fields on other roles. Node omits optional fields set to `undefined`; it rejects `null` values for those fields. `get_page_text` must return an empty `elements` array. The SDK does not parse an ordinary string into a structured read.

116 

117The SDK does not limit the number of elements or options, page text size, or complete report size. Refs and labels have a 256-byte limit; values have a 512-byte limit. Use valid Unicode scalar values. Drivers can report partial coverage with unique `omitted` codes: `report_limit`, `text_limit`, `target_limit`, `work_limit`, `frame_limit`, `unsupported_root`, `unsupported_control`, `remote_action`, `private`, `value`, or `name_limit`.

118 

119The driver owns visibility, field privacy, and ref validity. Recheck a ref against the current page after approval and before acting. A control's label does not grant permission to read its value. Size limits alone do not make page contents safe.

120 

121### Return browser images

122 

123`screenshot` and `zoom` return a native `input_image` with at least one nonempty `image_url` or `file_id`. Both source fields may be present; each non-null source must be a nonempty string. The supported fields are `type`, `image_url`, `file_id`, `detail`, and `prompt_cache_breakpoint`. A cache breakpoint must be `{ mode: "explicit" }`; null is also accepted where the native content type permits it. The SDK validates and copies these fields before returning the image. Both Node methods return `ResponseInputImageContent`, where `detail` may be omitted or null. When set, use `auto`, `low`, `high`, or `original`.

124 

125For `zoom`, `region` is `[left, top, right, bottom]`. Coordinates must be nonnegative safe integers with positive width and height. The driver owns cropping and bounds checks. All later coordinates still refer to the full visible area.

126 

127### Opt in to scoped and pointer methods

128 

129These six browser methods are off by default, even on drivers that implement them. Select them with `members`, or add them with `additionalMembers` in Node or `additional_members` in Python. The original 18 default methods retain their names, arguments, and result types. With `members`, include any default methods you still need, such as `screenshot` or `read_page`.

130 

131| Method | Required fields | Result | Confirmation |

132| ------------------ | --------------------------------------------------- | --------------------- | ------------ |

133| `read_page_scoped` | `tab_id`, `root`, `depth`, `filter` | `BrowserScopedReadV1` | No |

134| `click_at` | `tab_id`, `x`, `y`, `button`, `click_count`, `keys` | Text or no result | Yes |

135| `drag_path` | `tab_id`, `path`, `keys` | Text or no result | Yes |

136| `hold_keys` | `tab_id`, `keys`, `duration_ms` | Text or no result | Yes |

137| `hover_at` | `tab_id`, `x`, `y` | Text or no result | Yes |

138| `set_checked` | `tab_id`, `target`, `checked` | Text or no result | Yes |

139 

140Node uses `BrowserArguments<Member>`. Python exports `ReadPageScopedInput`, `ClickAtInput`, `DragPathInput`, `HoldKeysInput`, `HoverAtInput`, and `SetCheckedInput`. All fields are required; `tab_id` may be `null` or `None` for the active tab. Unknown fields are rejected, including extra fields in a path point. Numbers must be finite safe integers. Python rejects `True` and `False` where numbers are required.

141 

142`click_at` accepts nonnegative `x` and `y`, `button` set to `left`, `right`, or `middle`, and `click_count` set to 1, 2, or 3. `drag_path` accepts 2 through 100 exact `{x, y}` points with nonnegative coordinates. Both use the full visible area of the page in CSS pixels, even after zooming. The driver must check current bounds. Their `keys` fields contain a list of unique `Control`, `Alt`, `Meta`, or `Shift` modifiers. The list may be empty.

143 

144`hover_at` moves the pointer to current CSS coordinates without holding a button. `set_checked` sets a checkbox to the requested boolean state. Setting it to true twice must leave it checked. Even a no-op must use the original current target and pass confirmation; it must not accept a stale or replaced control.

145 

146`hold_keys` takes 1 through 8 unique nonempty key names, each at most 64 UTF-8 bytes with valid Unicode scalar values. `duration_ms` must be from 1 through 3000. Methods that change state require `confirm` to return exactly true. Cooperative methods receive the per-call cancellation context: Node passes `{ signal }` as a second argument; Python passes the `cancelled` keyword when the method accepts it. Existing one-argument methods remain valid.

147 

148### Read within a section

149 

150For `read_page_scoped`, a null `root` discovers visible sections. A non-null root must be a driver-issued scope ref, not a selector: nonempty, valid Unicode, and at most 256 UTF-8 bytes. Set `depth` from 1 through 32 and `filter` to `all` or `interactive`. Depth counts DOM traversal within that root separately from frame or shadow limits.

151 

152Return `BrowserScopedReadV1`, which extends the existing read with `scopes: [{ref, label}]`. Refs and labels must be nonempty, valid Unicode, and at most 256 UTF-8 bytes. Scope refs must be unique and must not match any element ref. This method rejects string and null results. The SDK copies known fields before it asks for final state.

153 

154Use scope refs only to select another read root. Element mutations must reject them. Invalidate scope and element refs together when their tab, document, generation, or visibility changes. Keep the existing field privacy, total work, and read limits.

155 

156### Opt in to diagnostics, scripts, and file transfers

157 

158These four methods also stay off by default. Select each one in `members` or add it with `additionalMembers` or `additional_members`, and

159provide `confirm`, including for diagnostic reads. A driver's implementation alone

160does not expose them to the model.

161 

162| Method | Required arguments | Result |

163| ------------------ | -------------------------------- | ------------------------- |

164| `read_diagnostics` | `tab_id`, `source`, `limit` | Structured text-only read |

165| `run_script` | `tab_id`, `source`, `timeout_ms` | Text |

166| `upload_file` | `grant_id` | `BrowserFileOutcome` |

167| `download_file` | `grant_id` | `BrowserFileOutcome` |

168 

169For `read_diagnostics`, choose `console` or `network` as the source and a limit

170from 1 through 100. Return a structured read with an empty `elements` array.

171In Node, use the `BrowserTextReadV1` result type.

172The driver must filter private data and bound its history. Do not return raw

173headers, cookies, credentials, or unfiltered console messages.

174 

175`run_script` takes nonempty source containing valid Unicode and a timeout

176from 1 through 30000 milliseconds. The SDK preserves the full source. Return text containing valid Unicode.

177An empty string is valid; a null result is not. The SDK preserves large results.

178Scripts run with the page's privileges. Use a fresh session without

179credentials, restrict its network access, and retire it if execution becomes

180uncertain. Stopping the local waiter does not prove that a page script stopped.

181 

182File methods accept only a caller-issued `grant_id`: nonempty, valid Unicode,

183and at most 256 UTF-8 bytes. The grant identifies one approved, task-scoped file;

184it is not a path or URL. The file owner must obtain separate approval for the file. It must recheck expiry and

185ownership, enforce size limits, and permit the grant only once. Browser confirmation does not replace this file approval.

186 

187Return exactly `version`, `status`, `effect`, and `cleanup`. Set `version` to 1.

188`cleanup` is `not_started`, `pending`, or `completed`. The valid status/effect

189pairs are:

190 

191| `status` | `effect` |

192| ------------- | ------------------- |

193| `completed` | `completed` |

194| `refused` | `none` |

195| `unknown` | `unknown` |

196| `interrupted` | `none` or `unknown` |

197 

198The SDK copies and validates this result before asking for final browser state.

199Only `completed` sets the tool output's `ok` to true. Other outcomes retain the

200structured result with `ok` false, so the runner skips dependent browser calls in

201that response. Check the actual file and application state before retrying an

202unknown transfer. A file result does not prove that a later form submission or

203business action succeeded.

204 

205`run_script`, `upload_file`, and `download_file` receive per-call cancellation: Node

206passes `{ signal }` as a second argument; Python passes the `cancelled` keyword.

207Python exports `ReadDiagnosticsInput`, `RunScriptInput`, and `BrowserFileInput`.

208 

209## Computer driver contract

210 

211A computer driver receives native computer actions and returns screenshots. It must implement the following methods. Import `ComputerDriver` from `openai` in Node. In Python, import `ComputerDriver` or `AsyncComputerDriver` from `openai`. Import the matching `ComputerToolset` or Python `AsyncComputerToolset` from `openai` too.

212 

213| Method | Action the driver receives |

214| -------------- | --------------------------------------- |

215| `click` | Coordinates, button, and any held keys |

216| `double_click` | Coordinates and any held keys |

217| `drag` | A path and any held keys |

218| `keypress` | A combination of keys |

219| `move` | Pointer coordinates |

220| `scroll` | Coordinates, `scroll_x`, and `scroll_y` |

221| `type` | Text for the focused control |

222| `wait` | A request to let the environment settle |

223| `screenshot` | No argument; return the current display |

224 

225Action methods receive the existing generated computer action object, with its snake-case fields, and return no value. They must raise an error if they cannot execute the action; do not silently ignore unsupported input. Node supports synchronous results or promises; await Python async methods.

226 

227In Python, use synchronous methods with `ComputerToolset` and `async def` methods with `AsyncComputerToolset`. The helper rejects known mismatches when you create it. Preserve decorator metadata or return annotations when wrapping methods. Opaque wrappers are checked when called, so their own code can run before the helper detects a mismatch.

228 

229In Node, use `ComputerCallInput` for calls you pass to `ComputerToolset.execute()`. It accepts native calls and modern calls with an `actions` array that omit `pending_safety_checks`. Approval callbacks receive a detached call with an empty checklist for that valid omission. The replayed call keeps its original fields.

230 

231`screenshot()` returns a `computer_screenshot` with an `image_url` (including a data URL) or `file_id`. Computer helpers require at least one image source and accept both source fields. The helper does not read local files. Actions run once, in order, and the helper captures the final screenshot after them. It does not retry driver actions.

232 

233For a computer-only Responses loop, use `ComputerToolset.run` or Python's `AsyncComputerToolset.run`. Use a model available to your project that supports the [computer tool](https://developers.openai.com/api/docs/guides/tools-computer-use). For mixed runs, pass the computer helper as the Responses runner's `computer` option. Node takes computer callbacks in its run option `computerOptions`; Python takes `approve_safety_checks` and `before_action` directly as run keyword arguments. The browser example controls a browser only. It does not implement or verify a native desktop.

234 

235The computer helper and `ResponsesToolRunner` both accept Responses `context_management` compaction. They check the required item fields and replay the encrypted content unchanged. For `ResponsesToolRunner`, choose this native option or the local `contextBudget` or `context_budget` option; do not combine them.

236 

237### Run a screenshot-based task

238 

239[Run your first task](https://developers.openai.com/api/docs/guides/tools-computer-use#run-your-first-task) includes a complete Python or Node example. It uses the Responses computer tool to reveal pickup details in a local browser.

240 

241#### Use an isolated desktop

242 

243To control an operating-system desktop, see the full desktop example in [Python](https://github.com/openai/openai-python/blob/v3.31.0/examples/computer-desktop/README.md) or [Node](https://github.com/openai/openai-node/blob/v7.33.0/examples/computer-desktop/README.md). Both use an isolated Linux guest. On Macs with Apple silicon, the Linux guest runs in a virtual machine; on x86-64 Linux, it runs in Docker. Each example includes a local exercise without an API key. Its live task checks both the browser record and the saved file. For your own environment, start with the [Docker recipe](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#prepare-an-environment) and adapt the driver contract above.

244 

245## Mix browser actions with other tools

246 

247The Responses runner can combine browser and computer helpers with ordinary functions, web search, file search, code interpreter, image generation and remote MCP tools. Server-tool results keep their native content and container IDs when the next request replays the response. Failed, incomplete or unfinished server results stop the turn before local actions.

248 

249When a run declares server tools, local calls wait for the complete response even if `eager` is enabled. The runner must inspect later server results and approval requests before it can safely act.

250 

251At that completed turn, `supplyOutputs` in Node or `supply_outputs` in Python can replace an unattempted ordinary function result when its registration allows overrides. This also works without an explicit early-execution policy. A replacement cannot answer a browser call, computer call, or MCP approval request.

252 

253This runner needs stored responses to continue after image generation. It rejects `store: false` in Node or `store=False` in Python when you declare `image_generation`. Set storage to true when appropriate, or use `client.responses.create` for a single stateless request. The runner never enables storage for you.

254 

255Reuse typed function tools instead of repeating their validation. In Node, wrap a `zodResponsesFunction` or `standardResponsesFunction` with `runnerFunction` from `openai/helpers/responses-tool-runner`. The tool must include its callback. In Python, use `RegisteredFunction.from_tool` or `AsyncRegisteredFunction.from_tool` with an existing `pydantic_responses_function_tool` and an explicit typed executor. Invalid arguments stop before that executor runs. Invalid callback output stops the run without repeating an effect. Raw `RunnerFunction` and `RegisteredFunction` registrations retain their existing argument behavior.

256 

257For MCP, supply `approveMcp` in Node or `approve_mcp` in Python. The callback receives the actual detached approval request and returns true or false. The runner binds the response to that request's ID and declared server. It cannot invent approval or treat a final message as consent.

258 

259Requests that declare MCP tools disable automatic SDK retries, with or without a ledger. If a request fails, check what the MCP server did before starting another run.

260 

261## Handle failures

262 

263Throw `BrowserError` only with text safe for the model, for a recoverable issue such as a stale ref. Other errors thrown by a browser operation produce a generic failure message. For these operation failures, the runner supplies an output for every browser call, skips the remaining browser operations in that response, and lets the model choose again on the next turn. Invalid returned results or invalid state stop the run. Failure to report state, failed ordinary functions, and unapproved native computer checks also stop it.

264 

265Set `maxSteps` or `max_steps` to bound API requests. The default is 20. The final allowed request cannot start new effects. An SDK HTTP retry does not retry a driver action. Use `signal` in Node or `cancelled` in Python to stop between operations; arbitrary in-progress driver calls cannot always be undone.

266 

267`ResponsesToolRunner` preserves `tool_choice` on every request. A choice that always requires a local tool cannot finish, so the runner rejects it before any work. Use `auto` for a loop, or `responses.create` for one forced request. Required choices that also allow a declared server tool remain valid.

268 

269### Troubleshoot a run

270 

271| Symptom | Check and fix |

272| ----------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |

273| `ComputerToolset` or a browser helper fails to import | Check the installed package in the environment running the script. Use Python `openai==3.31.0` or Node `openai@7.33.0` or a later version that includes the helpers. |

274| Chromium cannot launch | Run the Playwright install command in the same environment as the script. In a Linux container, install the required system libraries and run as a non-root user. Do not turn off the sandbox to hide the error. |

275| The API rejects the model or tool | Check your project's model access and the model's **Tools** section in the [models catalog](https://developers.openai.com/api/docs/models). The native `computer` tool and browser function tools have different model requirements. |

276| An action or pending safety check is declined | Inspect your application's approval policy. Check the fixed action arguments against current state. Ask for approval when needed; do not approve every request to make a run pass. |

277| The model reaches the step limit | Verify the actual page or file first. Check whether the task needs capabilities your driver refuses. Simplify the task before raising `maxSteps` or `max_steps`. |

278| The browser model answers without reading the page | Enable a read method your driver implements, such as `read_page` in the example. Ask the model to read the current page and rerun the task. Verify the page or file result before accepting the answer. |

279| Clicks miss the visible target | [Check the screenshot and coordinates](#fix-missed-clicks). |

280 

281### Fix missed clicks

282 

283Capture a fresh screenshot and check that it shows the intended window. Use the same coordinate space for the screenshot and the action. If you send an 800 × 600 screenshot from a 1600 × 1200 display, double both returned coordinates before clicking. If a computer driver sends a cropped screenshot, add the crop's offset after scaling. Browser driver pointer methods use CSS pixels in the full visible page area, even after zooming; do not confuse them with display pixels.

284 

285Screenshot size affects input tokens and cost. See [capture screenshots](https://developers.openai.com/api/docs/guides/tools-computer-use-integration#capture-screenshots) for image sizing and model limits.

286 

287### Stream and resume

288 

289Responses runs are non-streaming by default. With `stream` and `onEvent` (Node) or `on_event` (Python), the runner supplies detached stream events and waits for the response to finish before execution. With `eager`, it may execute completed output items early, in order. It never executes argument deltas.

290 

291Enable eager execution only if a completed item can safely run before the runner validates the full response. If a later event changes or removes that item, the runner stops and keeps its execution record. It cannot undo the action. Leave eager execution off, or use an empty policy for early execution, if all actions must wait for the complete response to pass validation.

292 

293If completed items arrive out of order or their indexes have gaps, the runner waits for the full response before executing that part of the sequence. Effects already completed remain recorded if the stream fails.

294 

295`earlyExecution` or `early_execution` can restrict early execution to selected function wire names and control the native computer dispatcher with a separate boolean. An empty object defers all local tools. Omission keeps normal blanket eager behavior. The first deferred tool also defers all later tools to preserve their order. With `iterate()`, inspect the completed turn before advancing; closing at that boundary executes no deferred tools. These options change timing, not approval.

296 

297For one completed call, `beforeEarlyCall` or `before_early_call` can return `defer` before dispatch. It cannot make a call outside the early-execution allowlist run early. Mark an ordinary function `independent: true` only when it can run without depending on other actions. Without a ledger, independent functions can run while the stream advances. Durable runs, browser actions, and computer actions stay ordered. Started work keeps its execution record even if another call fails.

298 

299By default, at most two independent eager calls run at once. Set `maxConcurrentCalls` in Node or `max_concurrent_calls` in Python to a positive integer to change that limit. The runner waits for the current group before reading more streamed calls. This option applies only to independent eager runs without a ledger. Each function must limit any work that it starts itself.

300 

301On an interrupted eager run, inspect `ResponsesToolRunnerError.attemptedCallIds` and `completedCallIds` in Node, or `attempted_call_ids` and `completed_call_ids` in Python. They contain call IDs, not results, arguments, or driver messages. Effects may already have occurred. Read the authoritative application state before retrying an attempted or uncertain action.

302 

303Use `contextBudget` or `context_budget` to limit the next request's input bytes and images, with or without durable storage. The projection keeps the current observations and required tool relationships. An in-memory limit does not make a run recoverable after the process exits.

304 

305At a completed turn boundary, `appendUserText` or `append_user_text` queues human guidance. The runner appends it after recording pending outputs. It accepts text, not arbitrary tool output or approval objects, and does not reset the request budget.

306 

307Use this method to clarify the current task, such as asking the agent to check a different date. If the user replaces the task, stop and await the current run before starting a new one. Read the application state again and obtain fresh approval for new actions. Keep the old run's record; changing instructions does not undo its effects.

308 

309For durable recovery, supply a `ledger` that saves each checkpoint under exclusive run ownership. Only resume from a completed boundary; a pending or uncertain action needs reconciliation first. The remaining request budget and completed history survive resume. Local context-budget compaction keeps the full evidence in the ledger and pins current observations, safety checks, messages, and reasoning. It never grants approval.

310 

311Every save receives a detached, complete checkpoint, including saves before and after local effects. The checkpoint includes the full batch and accumulated history. For large batches, copying and storing these records can take quadratic total work. Choose storage suited to that volume; `contextBudget` and `context_budget` limit request input, not ledger storage.

312 

313An interrupted pending checkpoint contains the last saved prefix. It can omit later text and unattempted proposals; it never authorizes replay. Before and after an effect, the saved checkpoint includes the full observed prefix and execution records.

314 

315A run that includes MCP tools saves a pending checkpoint before each request. If the connection fails, check what the MCP server did before starting another run. The pending checkpoint cannot resume automatically.

316 

317Saved MCP declarations omit authorization and custom headers and replace server URLs with SHA-256 fingerprints. Native requests still use the original URL. Put credentials that must rotate in authorization or headers. Changing a URL path or an unrecognized query value changes its identity and requires a new run. Checkpoints containing raw MCP URLs cannot resume. Node requires `crypto.subtle.digest` to save an MCP URL. Other checkpoint inputs and outputs remain private application data.

318 

319Supply current credentials when resuming, and verify that they address the intended server and account.

320 

321Saved Code Interpreter declarations keep credential names and domains but clear their secret values. Supply current values in the tool configuration when resuming. For MCP approvals, an empty `always: {}` or `never: {}` filter applies to every tool.

322 

323Supply `approveMcp` in Node or `approve_mcp` in Python when initial input or a saved ledger contains approved MCP requests. Before the first save or API request, the runner asks again for each approval that has no completed matching MCP call. This applies with or without a ledger or context budget. It keeps completed calls and earlier refusals unchanged. If fresh approval is needed, a missing callback stops the run.

324 

325## Check a custom driver locally

326 

327Run the local exercise in the [booking demo](https://developers.openai.com/api/docs/guides/tools-computer-use#see-and-approve-changes) or [isolated desktop example](#use-an-isolated-desktop) before adapting it. Add checks for your driver's actual page state, input effects, and cleanup. SDK validation does not prove that the environment performed an action correctly.

328 

329See [Run your first task](https://developers.openai.com/api/docs/guides/tools-computer-use#run-your-first-task) for a working SDK workflow and [integration recipes](https://developers.openai.com/api/docs/guides/tools-computer-use-integration) for environment setup. After any change, verify the saved form, file, or other application record before reporting success.