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. 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 October 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 the call and answers with a
function_call_output that has 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", maxTotalCostMicros: 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_total_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. OpenAI requires the schema to
list every property in required and to 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 an error.
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. A runaway script still
produces a tool output that the model can read
(JavaScript SDK). The array form of exec runs
python3 directly, with no shell. Nothing in the model's text is 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. Only the model's script enters the sandbox, so the sandbox never
needs the key.
Store the key 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. 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
October 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, you do not write a loop. RuntimeCloudSandboxClient runs a
SandboxAgent's shell and file edits in a Runtime sandbox. 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. It bills memory at $0.0075 per GiB-hour. A thousand
60-second runs that use 20 CPU-seconds each cost $0.64.
maxTotalCostMicros: 50_000 stops a sandbox once it has cost five cents in
total. OpenAI bills its own tokens.
A new account gets 100 free hours of a 1 GB machine in its first 30 days, no card:
Terminalprintf '%s\n' 'print(sum([125, 250, 375]))' > invoice.pynpx withruntime sandboxes run -- python3 - < invoice.pyMore: run untrusted LLM code, add a code interpreter to a chatbot, turn off sandbox internet, Codex in a sandbox.
Sources
Checked 25 October 2026.
- OpenAI: function calling guide
- OpenAI: conversation state, for
previous_response_id - OpenAI: models
- OpenAI: Code Interpreter guide
- openai on npm (7.43.0) and on PyPI (3.37.0)
Facts on this page were checked on 25 October 2026.