How to run code from OpenAI function calling in a sandbox
Define a run_python function tool, run each call's code in a fresh microVM with no network, and return stdout, stderr and exit code.
On Runtime the tool's handler is a dozen lines, and a run is billed on the
CPU it uses. Each call gets its own Firecracker microVM, which ran its first Python
command 351 ms after the request at the median on 24 September 2026
(speed). A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour while it
waits and $0.08 an hour with both CPUs busy (pricing). The
examples use gpt-6-astra, the model OpenAI's model guide said to start with
on 25 September 2026.
The loop in TypeScript
The model never runs anything. It returns a function_call item with a
call_id and JSON arguments; your code runs it and answers with a
function_call_output for the same call_id. previous_response_id carries
the conversation, so each follow-up request sends only the outputs.
TypeScriptimport OpenAI from "openai";import { Sandbox } from "withruntime";const openai = new OpenAI(); // OPENAI_API_KEY, on your server onlyasync function runPython(code: string): Promise<string> { await using sbx = await Sandbox.create({ network: { internet: false }, timeoutSeconds: 300, onLeaseEnd: "stop", maxCostMicros: 50_000, }); await sbx.files.write("/workspace/main.py", code); const run = await sbx.exec(["python3", "main.py"], { timeoutMs: 30_000 }); return JSON.stringify({ exit_code: run.exitCode, timed_out: run.timedOut, stdout: run.stdout, stderr: run.stderr, });}const tools: OpenAI.Responses.Tool[] = [ { type: "function", name: "run_python", description: "Run a Python 3.12 script in an isolated Linux sandbox with no internet. " + "Returns exit_code, timed_out, stdout and stderr. Print what you need to see.", parameters: { type: "object", properties: { code: { type: "string", description: "The whole script" } }, required: ["code"], additionalProperties: false, }, strict: true, },];let response = await openai.responses.create({ model: "gpt-6-astra", tools, input: "What is the 40th Fibonacci number? Compute it, do not recall it.",});for (let turn = 0; turn < 8; turn++) { const calls = response.output.filter((item) => item.type === "function_call"); if (calls.length === 0) break; const outputs: OpenAI.Responses.ResponseInputItem[] = []; for (const call of calls) { const { code } = JSON.parse(call.arguments) as { code: string }; outputs.push({ type: "function_call_output", call_id: call.call_id, output: await runPython(code), }); } response = await openai.responses.create({ model: "gpt-6-astra", tools, previous_response_id: response.id, input: outputs, });}console.log(response.output_text);The same loop in Python
Pythonimport jsonfrom openai import OpenAIfrom withruntime import Sandboxclient = OpenAI() # OPENAI_API_KEY, on your server onlydef run_python(code: str) -> str: with Sandbox.create( network={"internet": False}, timeout_seconds=300, on_lease_end="stop", max_cost_micros=50_000, ) as sbx: sbx.files.write("/workspace/main.py", code) run = sbx.exec(["python3", "main.py"], timeout_ms=30_000) return json.dumps( {"exit_code": run.exit_code, "timed_out": run.timed_out, "stdout": run.stdout, "stderr": run.stderr} )tools = [ { "type": "function", "name": "run_python", "description": "Run a Python 3.12 script in an isolated Linux sandbox with no internet. " "Returns exit_code, timed_out, stdout and stderr. Print what you need to see.", "parameters": { "type": "object", "properties": {"code": {"type": "string", "description": "The whole script"}}, "required": ["code"], "additionalProperties": False, }, "strict": True, }]response = client.responses.create( model="gpt-6-astra", tools=tools, input="What is the 40th Fibonacci number? Compute it, do not recall it.",)for _ in range(8): calls = [item for item in response.output if item.type == "function_call"] if not calls: break outputs = [ {"type": "function_call_output", "call_id": call.call_id, "output": run_python(json.loads(call.arguments)["code"])} for call in calls ] response = client.responses.create( model="gpt-6-astra", tools=tools, previous_response_id=response.id, input=outputs )print(response.output_text)strict: true makes the arguments match the schema, which OpenAI requires to
list every property in required and set additionalProperties to false. The
eight-turn cap stops a model that keeps calling the tool. The sandbox stops when
the handler returns, even if the script raised.
What the handler sends back, and why
OpenAI's guide says a function's output "should typically be a string" and leaves the format to you. JSON with four fields gives the model enough to fix its own mistakes:
| Field | Where it comes from | What the model does with it |
|---|---|---|
exit_code |
The script's exit status; null if killed |
Tells success from failure without guessing |
timed_out |
true when timeoutMs ended the command |
Rewrites a loop that never finishes |
stdout |
Up to 64 KiB, with a flag if more was cut | Reads the answer it printed |
stderr |
Up to 64 KiB: tracebacks, warnings | Sees the exception and corrects the code |
A command's timeout is a result, not an exception, so a runaway script still
produces a tool output the model can read (JavaScript SDK).
The array form of exec runs python3 directly with no shell, so nothing in
the model's text is ever parsed as a shell command.
Where the OpenAI key lives
Keep OPENAI_API_KEY in your server's environment. The loop calls OpenAI from
your process and only the model's script enters the sandbox, so the sandbox
never needs the key.
Store it as a Runtime secret only when code inside the sandbox calls OpenAI itself, such as an agent you run there:
Terminalprintf %s "$OPENAI_API_KEY" | npx withruntime secrets set OPENAI_API_KEY --host api.openai.comThe sandbox then sees a placeholder, and the host's proxy writes the real key
into HTTPS requests to api.openai.com and nowhere else
(secrets sandboxes never see).
Code Interpreter, or your own function?
OpenAI's hosted tool is {"type": "code_interpreter"}. Its guide, checked 25
September 2026, describes Python only ("the model knows it as the 'python
tool'"), containers of 1g, 4g, 16g or 64g, and a container that "expires if it
is not used for 20 minutes".
| You need | Better fit |
|---|---|
| Quick Python analysis inside one OpenAI chat | Code Interpreter |
| Packages you choose, or a custom image | Your function and Runtime |
| JavaScript, TypeScript, R, Java, Bash or Go too | Your function and Runtime |
| A session that pauses for days and resumes | Your function and Runtime |
| The same tool behind models from other vendors | Your function and Runtime |
Prices for both, session by session, are in OpenAI Code Interpreter alternative.
Using the OpenAI Agents SDK instead
With the Agents SDK there is no loop to write: RuntimeCloudSandboxClient
runs a SandboxAgent's shell and file edits in a Runtime sandbox, and the
agent definition stays as it is (OpenAI Agents SDK).
What it costs
Each call above creates a 2 vCPU, 4 GiB sandbox and stops it when the script
ends. Runtime bills measured CPU at $0.025 per vCPU-hour, with a floor of a
twentieth of a vCPU, and memory at $0.0075 per GiB-hour. A thousand 60-second
runs that use 20 CPU-seconds each cost $0.64. maxCostMicros: 50_000 refuses a
create whose first lease would cost more than five cents. OpenAI's tokens are
billed by OpenAI.
New accounts get 50 free sandbox hours, no card:
Terminalnpx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)'More: run untrusted LLM code, add a code interpreter to a chatbot, turn off sandbox internet, Codex in a sandbox.
Sources
Checked 25 September 2026.
- OpenAI: function calling guide
- OpenAI: conversation state, for
previous_response_id - OpenAI: models
- OpenAI: Code Interpreter guide
- openai on npm (7.23.0) and on PyPI (3.19.2)
Facts on this page were checked on 25 September 2026.