OpenRouter tool calling: one code sandbox for every model
Write the sandbox tool once in OpenAI's function format; OpenRouter passes it to any tool-capable model, and a microVM runs the code.
One handler, hundreds of models: on 25 September 2026 OpenRouter's model
list held 460 models, 392 of them accepting tools. Switching from GPT to
Claude to Gemini changes one string, and the code every one of them writes
runs in the same kind of Runtime sandbox: a Firecracker microVM with its own
Linux kernel and the internet off, which costs $0.03125 an hour while it waits
at 2 vCPU and 4 GiB (pricing).
The loop, with the model as a parameter
OpenRouter's TypeScript SDK takes the request inside chatRequest and returns
camelCase fields: toolCalls on the message and toolCallId on the reply.
Its Python examples use the openai package pointed at
https://openrouter.ai/api/v1.
TypeScriptimport { OpenRouter } from "@openrouter/sdk";import type { ChatFunctionTool, ChatMessages } from "@openrouter/sdk/models";import { Sandbox } from "withruntime";const openRouter = new OpenRouter({ apiKey: process.env.OPENROUTER_API_KEY ?? "" });async 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: 60_000 }); return JSON.stringify({ exit_code: run.exitCode, timed_out: run.timedOut, stdout: run.stdout, stderr: run.stderr, });}const tools: ChatFunctionTool[] = [ { type: "function", function: { name: "run_python", description: "Run a Python 3.12 script in an offline sandbox; returns exit_code, stdout and stderr.", parameters: { type: "object", properties: { code: { type: "string" } }, required: ["code"], }, }, },];export async function solve(model: string, task: string): Promise<string> { const messages: ChatMessages[] = [{ role: "user", content: task }]; for (let turn = 0; turn < 8; turn++) { const result = await openRouter.chat.send({ chatRequest: { model, messages, tools, stream: false }, }); if (!("choices" in result)) throw new Error("expected a complete response, not a stream"); const message = result.choices[0]!.message; messages.push(message); if (!message.toolCalls?.length) return String(message.content ?? ""); for (const call of message.toolCalls) { const { code } = JSON.parse(call.function.arguments) as { code: string }; messages.push({ role: "tool", toolCallId: call.id, content: await runPython(code) }); } } return "stopped after 8 turns";}const task = "How many primes are there below 10 million? Compute it.";for (const model of ["openai/gpt-6-sol", "anthropic/claude-opus-5.5", "google/gemini-3.8-flash"]) console.log(model, await solve(model, task));Pythonimport jsonimport osfrom openai import OpenAIfrom withruntime import Sandboxclient = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])def 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=60_000) return json.dumps({"exit_code": run.exit_code, "timed_out": run.timed_out, "stdout": run.stdout, "stderr": run.stderr})tools = [ { "type": "function", "function": { "name": "run_python", "description": "Run a Python 3.12 script in an offline sandbox; returns exit_code, stdout and stderr.", "parameters": {"type": "object", "properties": {"code": {"type": "string"}}, "required": ["code"]}, }, }]def solve(model: str, task: str) -> str: messages = [{"role": "user", "content": task}] for _ in range(8): message = client.chat.completions.create(model=model, messages=messages, tools=tools).choices[0].message messages.append(message) if not message.tool_calls: return message.content or "" for call in message.tool_calls: code = json.loads(call.function.arguments)["code"] messages.append({"role": "tool", "tool_call_id": call.id, "content": run_python(code)}) return "stopped after 8 turns"task = "How many primes are there below 10 million? Compute it."for model in ["openai/gpt-6-sol", "anthropic/claude-opus-5.5", "google/gemini-3.8-flash"]: print(model, solve(model, task))OpenRouter's guide says to send tools on every request, which the loop does,
so each call is checked against the same schema. The model ids above were
listed with tools support by OpenRouter's models API on 25 September 2026;
its catalogue filter supported_parameters=tools shows the current set.
What changes from model to model, and what does not
| Part of the loop | Across OpenRouter's models |
|---|---|
| Tool definition | The same OpenAI-style function object |
| Which models may call it | Only those listing tools in supported_parameters |
| How often a model calls it | Varies; cap the turns, as the loop does |
| Where the code runs | Always your sandbox: same kernel boundary, network rule and limits |
| What the handler returns | The same JSON, so results compare fairly across models |
Because the sandbox is the same for every model, a benchmark of models on coding tasks measures the models, not their execution environments. For graded runs at scale, see agent evals and SWE-bench.
Compare models in parallel
The three calls to solve above run one after another. Wrap them in
Promise.all, or a thread pool in Python, and every model works at once: each
tool call still creates its own sandbox, so no model can see another's files
or processes. A paid account runs 100 sandboxes at once to start, and the free
trial eight, which bounds how many models and tasks you run side by side.
When code in the sandbox calls OpenRouter
Everything above keeps OPENROUTER_API_KEY on your server, because only the
model's script enters the sandbox. The key belongs in Runtime only when the
program inside the sandbox calls OpenRouter itself, for instance an agent you
run there. Store it as a secret bound to OpenRouter's host:
Terminalprintf %s "$OPENROUTER_API_KEY" | npx withruntime secrets set OPENROUTER_API_KEY --host openrouter.aiThen allow that host, and the program uses the variable as if it held the key:
TypeScriptimport { Sandbox } from "withruntime";await using sbx = await Sandbox.create({ network: { internet: true, allow: ["openrouter.ai", "pypi.org", "*.pythonhosted.org"] }, timeoutSeconds: 900, onLeaseEnd: "stop",});await sbx.exec(["pip", "install", "--quiet", "openai"], { check: true, timeoutMs: 180_000 });await sbx.files.write( "/workspace/agent.py", `import osfrom openai import OpenAIclient = OpenAI(base_url="https://openrouter.ai/api/v1", api_key=os.environ["OPENROUTER_API_KEY"])reply = client.chat.completions.create(model="openai/gpt-6-luna", messages=[{"role": "user", "content": "Say hi"}])print(reply.choices[0].message.content)`,);const run = await sbx.exec(["python3", "agent.py"], { timeoutMs: 120_000 });console.log(run.stdout);Inside the sandbox OPENROUTER_API_KEY holds a placeholder such as
rtsec_3f9c…. The host's proxy replaces it with the real key only in HTTPS
requests to openrouter.ai, so a prompt injection that prints or sends the
variable leaks nothing usable (secrets sandboxes never see).
Cost of the sandbox side
OpenRouter bills tokens; Runtime bills the sandbox on the CPU the code uses, at $0.025 per vCPU-hour with a floor of a twentieth of a vCPU, plus memory at $0.0075 per GiB-hour. A thousand 60-second runs at 2 vCPU and 4 GiB that use 20 CPU-seconds each cost $0.64, whichever model wrote the code. New accounts get 50 free sandbox hours, no card:
Terminalnpx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)'More: run untrusted LLM code, egress control, coding agent sandbox.
Sources
Checked 25 September 2026.
- OpenRouter: tool calling
- OpenRouter: models API, counted for
toolsinsupported_parameters - @openrouter/sdk on npm (1.3.27) and openai on PyPI (3.19.2)
Facts on this page were checked on 25 September 2026.