# 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 works with hundreds of models. On 25 October 2026, OpenRouter's model list held 467 models, and 399 of them accept `tools`.** Switching from GPT to Claude to Gemini changes one string. The code that each model writes runs in the same kind of Runtime sandbox: a Firecracker microVM with its own Linux kernel and the internet off. At 2 vCPU and 4 GiB, it costs $0.03125 an hour while it waits ([pricing](/docs/pricing)). ## The loop, with the model as a parameter OpenRouter's TypeScript SDK takes the request inside `chatRequest`. It 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`. ```ts check import { 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 { 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: 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 { 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)); ``` ```python check import json import os from openai import OpenAI from withruntime import Sandbox client = 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_total_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. The loop does that, 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 October 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](/use-cases/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 in a thread pool in Python, and every model works at the same time. 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, and an account without credit four. That limit sets how many models and tasks you can 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: ```bash no-run printf %s "$OPENROUTER_API_KEY" | npx withruntime secrets set OPENROUTER_API_KEY --host openrouter.ai ``` Then allow that host. The program uses the variable as if it held the key: ```ts check import { 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 os from openai import OpenAI client = 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`. A prompt injection that prints or sends the variable leaks nothing usable ([secrets sandboxes never see](/docs/security#secrets-sandboxes-never-see)). ## Cost of the sandbox side OpenRouter bills tokens. Runtime bills the sandbox for the CPU the code uses, at $0.025 per vCPU-hour with a floor of a twentieth of a vCPU. It adds memory at $0.0075 per GiB-hour. A thousand 60-second runs at 2 vCPU and 4 GiB, each using 20 CPU-seconds, cost $0.64. That holds whichever model wrote the code. New accounts get 100 free hours of a 1 GB machine in the first 30 days, with no card: ```bash no-run printf '%s\n' 'print(sum([125, 250, 375]))' > invoice.py npx withruntime sandboxes run -- python3 - < invoice.py ``` More: [run untrusted LLM code](/use-cases/run-untrusted-llm-code), [egress control](/glossary/egress-control), [coding agent sandbox](/use-cases/coding-agent-sandbox). ## Sources Checked 25 October 2026. - [OpenRouter: tool calling](https://openrouter.ai/docs/guides/features/tool-calling) - [OpenRouter: models API](https://openrouter.ai/api/v1/models), counted for `tools` in `supported_parameters` - [@openrouter/sdk on npm](https://www.npmjs.com/package/@openrouter/sdk) (1.5.7) and [openai on PyPI](https://pypi.org/project/openai/) (3.37.0) Facts on this page were checked on 25 October 2026.