Skip to the page

OpenAI computer use

Run OpenAI's computer tool on a SpecStream browser session. The Responses API returns clicks and keystrokes, Playwright runs them in a real Chromium over the session's connect_url, and you send back a screenshot. A session costs $0.10 an hour, charged by the second, so a 5 minute task is under 1 cent of browser time.

SpecStream for OpenAI computer useAgent frameworks & builders

What you need

  • Python 3.10 or newer.
  • An OpenAI API key and a model that supports the computer tool. The script uses gpt-6.1-sol, the model in OpenAI's own example.
  • A SpecStream account. New accounts start with $10 of credit, no card. Create one.
  1. 1. Install the packages

    The OpenAI SDK calls the Responses API, Playwright drives the browser, and httpx calls SpecStream. Playwright connects to the session's browser, so there is no local browser to download and you can skip playwright install.

    Terminal
    pip install openai playwright httpx
  2. 2. Create a SpecStream key and set both keys

    In the SpecStream workspace, open Agents & API, choose API keys, then Create API key. A key that opens a browser needs the finds:spend scope and a spending limit above 0. The limit is the most the key can spend in total, so a small one is enough for a test.

    Set it next to your model key in the shell that runs the script:

    Terminal
    export SPECSTREAM_API_KEY="YOUR_SPECSTREAM_API_KEY"
    export OPENAI_API_KEY="YOUR_OPENAI_API_KEY"
  3. 3. Save the agent

    Save this file as agent.py. It opens a session, connects Playwright to it, opens the start page, and runs OpenAI's three steps in a loop: send the task, run the returned actions, return a screenshot.

    The screenshots show the page only, with no address bar, so the script opens START_URL itself before the first request.

    agent.py
    import base64
    import os
    import time
    import httpx
    from openai import OpenAI
    from playwright.sync_api import sync_playwright
    API = "https://api.specstream.ai/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['SPECSTREAM_API_KEY']}"}
    MODEL = "gpt-6.1-sol"
    TOOLS = [{"type": "computer"}]
    START_URL = "https://news.ycombinator.com"
    TASK = "Tell me the title of the top story on this page. Use the computer tool for UI interaction."
    KEYS = {
    "ENTER": "Enter", "RETURN": "Enter", "ESC": "Escape", "ESCAPE": "Escape", "TAB": "Tab",
    "SPACE": "Space", "BACKSPACE": "Backspace", "DELETE": "Delete", "DEL": "Delete",
    "HOME": "Home", "END": "End", "PAGEUP": "PageUp", "PAGEDOWN": "PageDown",
    "UP": "ArrowUp", "DOWN": "ArrowDown", "LEFT": "ArrowLeft", "RIGHT": "ArrowRight",
    "ARROWUP": "ArrowUp", "ARROWDOWN": "ArrowDown", "ARROWLEFT": "ArrowLeft",
    "ARROWRIGHT": "ArrowRight", "CTRL": "Control", "CONTROL": "Control", "SHIFT": "Shift",
    "OPTION": "Alt", "ALT": "Alt", "META": "Meta", "CMD": "Meta", "COMMAND": "Meta",
    }
    BUTTONS = {"left": "left", "right": "right", "wheel": "middle"}
    def key(name):
    return KEYS.get(name.upper(), name)
    def handle(page, actions):
    """Run the actions of one computer_call on the page, in order."""
    for action in actions:
    kind = action.type
    if kind == "screenshot":
    continue # one is taken after every batch
    if kind == "keypress":
    page.keyboard.press("+".join(key(k) for k in action.keys))
    continue
    if kind == "type":
    page.keyboard.type(action.text)
    continue
    if kind == "wait":
    time.sleep(2)
    continue
    # The mouse actions: keys holds the modifiers for this one action.
    held = [key(k) for k in (getattr(action, "keys", None) or [])]
    for k in held:
    page.keyboard.down(k)
    try:
    if kind == "click":
    button = BUTTONS[getattr(action, "button", "left")]
    page.mouse.click(action.x, action.y, button=button)
    elif kind == "double_click":
    page.mouse.dblclick(action.x, action.y)
    elif kind == "move":
    page.mouse.move(action.x, action.y)
    elif kind == "scroll":
    page.mouse.move(action.x, action.y)
    page.mouse.wheel(action.scroll_x, action.scroll_y)
    elif kind == "drag":
    path = [(point.x, point.y) for point in action.path]
    page.mouse.move(*path[0])
    page.mouse.down()
    for x, y in path[1:]:
    page.mouse.move(x, y)
    page.mouse.up()
    else:
    raise ValueError(f"Unsupported action: {kind}")
    finally:
    for k in reversed(held):
    page.keyboard.up(k)
    def main():
    session = httpx.post(
    f"{API}/browser/sessions", headers=HEADERS, json={"timeout_s": 600}, timeout=60
    )
    session.raise_for_status()
    session = session.json()["data"]
    print("Watch it:", session["live_url"])
    client = OpenAI()
    try:
    with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(session["connect_url"])
    context = browser.contexts[0]
    page = context.pages[0] if context.pages else context.new_page()
    page.goto(START_URL)
    response = client.responses.create(model=MODEL, tools=TOOLS, input=TASK)
    for _ in range(30):
    call = next((item for item in response.output if item.type == "computer_call"), None)
    if call is None:
    print(response.output_text)
    break
    handle(page, call.actions)
    image = base64.b64encode(page.screenshot(type="png")).decode()
    response = client.responses.create(
    model=MODEL,
    tools=TOOLS,
    previous_response_id=response.id,
    input=[
    {
    "type": "computer_call_output",
    "call_id": call.call_id,
    "output": {
    "type": "computer_screenshot",
    "image_url": f"data:image/png;base64,{image}",
    "detail": "original",
    },
    }
    ],
    )
    finally:
    ended = httpx.post(
    f"{API}/browser/sessions/{session['id']}/release", headers=HEADERS, timeout=60
    ).json()["data"]
    print(f"{ended['seconds']} seconds, ${ended['charged_usd']:.4f}")
    if __name__ == "__main__":
    main()
  4. 4. Run it

    The script prints the address where you can watch the browser, then the model's answer, then the seconds the session ran and what it cost. Change START_URL and TASK to your own.

    Terminal
    python agent.py

What the script does

Each part of the loop follows OpenAI's computer use guide.

  • POST /v1/browser/sessions opens the browser. The answer's data holds the session's id, its connect_url and its live_url.
  • connect_url is a WebSocket that speaks the Chrome DevTools Protocol, so Playwright's connect_over_cdp takes it as it is. Its token belongs to that one session, so your SpecStream key never reaches the browser side.
  • The first request sends the task with tools=[{"type": "computer"}]. The first computer_call may hold only a screenshot action, and the script answers it with the current screen.
  • Each computer_call holds an ordered actions array. The script runs the actions in order, then takes one screenshot.
  • The screenshot goes back as a computer_call_output with the same call_id, as a computer_screenshot with detail set to original, and previous_response_id continues the conversation, so the script keeps no message history.
  • The loop ends when a response holds no computer_call, and the script prints output_text.
  • POST /v1/browser/sessions/{id}/release ends the session, also when the script fails, and returns the seconds it ran and charged_usd.

How each action reaches the browser

The computer tool can request nine actions. A mouse action may carry a keys array of modifiers, such as Ctrl for a click that opens a new tab. The script holds them down for that one action.

The model returnsThe script runs
clickpage.mouse.click(x, y) with button, where wheel is Playwright's middle
double_clickpage.mouse.dblclick(x, y)
movepage.mouse.move(x, y)
scrollpage.mouse.move(x, y), then page.mouse.wheel(scroll_x, scroll_y)
dragpage.mouse.move to the first point of path, down, move through the other points, up
keypresspage.keyboard.press with names such as CTRL and ENTER changed to Playwright's Control and Enter
typepage.keyboard.type(text)
waittime.sleep(2)
screenshotNothing: the script takes one after every batch

If your code still uses computer-use-preview

Many guides still show the preview integration. OpenAI's migration table lists four changes, and the script above is the newer form of each.

PartPreviewNow
Modelcomputer-use-previewgpt-6.1-sol
Tool{"type": "computer_use_preview"} with display_width and display_height{"type": "computer"}
ActionsOne action on each computer_callAn actions array on each computer_call
Truncationtruncation: "auto" requiredNot needed

Screenshots and coordinates

The model gives x and y in the pixels of the screenshot you return. The session's screen is 1280 by 720 unless you send another viewport, the same size as the browser in OpenAI's local example, and Playwright's screenshot is that screen. The script passes the coordinates to the mouse unchanged.

OpenAI recommends detail: "original" on screenshots to keep the resolution and improve click accuracy. A larger viewport gives the model more to read and uses more input tokens for each screenshot.

What a session costs

Browser time is $0.10 an hour, charged by the second. The script drives the session over connect_url, so the clicks and keystrokes add nothing: you pay for the time alone. The $1 per 1,000 actions applies only to steps you send to POST /v1/browser/sessions/{id}/actions.

The model's tokens are billed by OpenAI: each Responses request is charged for its input and output tokens, and screenshots count as image input.

Session lengthBrowser time
1 minute$0.0017
5 minutes$0.0083
10 minutes$0.0167
1 hour$0.10

Keep a run inside limits

The model acts on real pages, so bound the run in the session and in the script.

  • timeout_s is the most seconds the session runs, from 60 to 3,600. Leave it out and the session ends after 300. The script sends 600.
  • allowed_domains limits the hosts the browser may open, such as ["example.com", "*.example.com"]. A page outside them is refused.
  • viewport sets the screen, 1280 by 720 unless you send another, for example {"width": 1280, "height": 800}.
  • The loop stops after 30 turns. Raise or lower the number in range(30).
  • The key's spending limit bounds what all its sessions cost together.
  • live_url is a page that shows the browser's screen as it changes. The script prints it at the start, so you can watch the run.
  • Text on a page is data. OpenAI's guide asks you to treat screen content as untrusted and to confirm purchases, sent data and changes that are hard to undo with a person.

If a run fails

The status of the call tells you which side refused.

What you seeWhat to do
401 from SpecStreamThe key is missing or wrong. Check SPECSTREAM_API_KEY.
402 from SpecStreamThe balance or the key's spending limit does not hold the first minute. Add credit or raise the limit.
403 from SpecStreamThe key lacks the finds:spend scope. Create a key that has it.
429 from SpecStreamThe account holds as many open sessions as it may. Release one, or create again after the seconds in Retry-After.
503 from SpecStreamNo browser is free at the moment. Create again after the seconds in Retry-After.
An error from OpenAI that names the model or the toolThe project has no access to a model with the computer tool. Check the model's Tools section in OpenAI's models catalog.
Unsupported action from the scriptThe model returned an action the handler does not know. Add a branch for it in handle.

More

SpecStream for OpenAI computer use covers what it does in OpenAI computer use on one page. Every other tool has its guide in Connect your tools. You see the most a request can cost before it starts, and the matches are paid from your balance. Billing has the details. For help, email [email protected].