DeepSeek tool calls: how to run DeepSeek's code safely
Point the OpenAI SDK at api.deepseek.com, give the model a run_python tool, and run each call in a microVM that installs from PyPI only.
DeepSeek's API runs no code for you, so where its code runs is your
decision, and a Runtime sandbox makes it a throwaway microVM that costs
$0.03125 an hour while it waits. DeepSeek's tool-calls guide says so plainly:
"The model itself does not execute specific functions." Every Runtime sandbox
is a Firecracker microVM with its own Linux kernel, and its network rules are
enforced on the host, where root inside cannot change them
(security). The examples use deepseek-flash, which
DeepSeek's pricing page mapped to DeepSeek-V4.1-Flash on 25 September 2026.
Let the model ask for packages
Model-written Python often starts with an import the default image lacks. This tool takes a list of packages as well as the code. The handler installs them while the sandbox can reach PyPI and nothing else, then switches the internet off before the model's script runs.
TypeScriptimport OpenAI from "openai";import { Sandbox } from "withruntime";const deepseek = new OpenAI({ baseURL: "https://api.deepseek.com", apiKey: process.env.DEEPSEEK_API_KEY,});const PACKAGE = /^[A-Za-z0-9][A-Za-z0-9._-]*(==[A-Za-z0-9.]+)?$/;async function runPython(code: string, packages: string[]): Promise<string> { const bad = packages.filter((name) => !PACKAGE.test(name)); if (bad.length) return JSON.stringify({ error: `not a package name: ${bad.join(", ")}` }); await using sbx = await Sandbox.create({ network: { internet: true, allow: ["pypi.org", "*.pythonhosted.org"] }, timeoutSeconds: 600, onLeaseEnd: "stop", }); if (packages.length) { const install = await sbx.exec(["pip", "install", "--quiet", ...packages], { timeoutMs: 180_000, }); if (install.exitCode !== 0) return JSON.stringify({ stage: "install", stderr: install.stderr }); } await sbx.network.set({ internet: false }); // nothing leaves while the script runs 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: OpenAI.Chat.ChatCompletionTool[] = [ { type: "function", function: { name: "run_python", description: "Run a Python 3.12 script in a disposable sandbox. List any PyPI packages it needs " + "in `packages`; they are installed first, then the script runs with no internet.", parameters: { type: "object", properties: { code: { type: "string" }, packages: { type: "array", items: { type: "string" } }, }, required: ["code", "packages"], }, }, },];const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [ { role: "user", content: "Use sympy to factor x**6 - 1 and show the result." },];for (let turn = 0; turn < 8; turn++) { const completion = await deepseek.chat.completions.create({ model: "deepseek-flash", messages, tools, }); const message = completion.choices[0]!.message; messages.push(message); // keeps reasoning_content, which DeepSeek requires back if (!message.tool_calls?.length) { console.log(message.content); break; } for (const call of message.tool_calls) { if (call.type !== "function") continue; const args = JSON.parse(call.function.arguments) as { code: string; packages?: string[] }; messages.push({ role: "tool", tool_call_id: call.id, content: await runPython(args.code, args.packages ?? []), }); }}Pythonimport jsonimport osimport refrom openai import OpenAIfrom withruntime import Sandboxclient = OpenAI(api_key=os.environ["DEEPSEEK_API_KEY"], base_url="https://api.deepseek.com")PACKAGE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*(==[A-Za-z0-9.]+)?$")def run_python(code: str, packages: list[str]) -> str: bad = [name for name in packages if not PACKAGE.match(name)] if bad: return json.dumps({"error": f"not a package name: {', '.join(bad)}"}) with Sandbox.create( network={"internet": True, "allow": ["pypi.org", "*.pythonhosted.org"]}, timeout_seconds=600, on_lease_end="stop", ) as sbx: if packages: install = sbx.exec(["pip", "install", "--quiet", *packages], timeout_ms=180_000) if install.exit_code != 0: return json.dumps({"stage": "install", "stderr": install.stderr}) sbx.network.set(internet=False) # nothing leaves while the script runs 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 a disposable sandbox. List any PyPI packages it needs " "in `packages`; they are installed first, then the script runs with no internet.", "parameters": { "type": "object", "properties": {"code": {"type": "string"}, "packages": {"type": "array", "items": {"type": "string"}}}, "required": ["code", "packages"], }, }, }]messages = [{"role": "user", "content": "Use sympy to factor x**6 - 1 and show the result."}]for _ in range(8): message = client.chat.completions.create(model="deepseek-flash", messages=messages, tools=tools).choices[0].message messages.append(message) # keeps reasoning_content, which DeepSeek requires back if not message.tool_calls: print(message.content) break for call in message.tool_calls: args = json.loads(call.function.arguments) messages.append({"role": "tool", "tool_call_id": call.id, "content": run_python(args["code"], args.get("packages", []))})Why the handler is written this way
| Line in the handler | What it stops |
|---|---|
| The package-name pattern | A "package" such as --index-url=https://evil.example changing pip |
allow: ["pypi.org", "*.pythonhosted.org"] |
The install fetching from anywhere but PyPI |
network.set({ internet: false }) |
The script sending data out; the rule applies to open connections too |
exec([...]) with an array |
Any part of the model's text being read by a shell |
timeoutMs |
An endless loop; the result comes back with timedOut: true |
await using / with |
A sandbox left running after the answer |
A package that runs code while it installs runs it inside the same throwaway machine, behind the same PyPI-only rule, so it can reach nothing else either (egress control).
DeepSeek details that break loops
- Pass reasoning back. DeepSeek's thinking mode is on by default, and its
guide warns that for requests with
tools, "the reasoning_content must be fully passed back to the API in all subsequent requests", or the API returns a 400 error. Appending the whole assistantmessage, as above, does that. - Strict schemas are beta. Setting
strict: trueon a function needs the base URLhttps://api.deepseek.com/beta. - Prices change with the clock. DeepSeek charges a peak rate from 01:00 to 04:00 and 06:00 to 10:00 UTC on weekdays, excluding Chinese public holidays.
| Model (25 September 2026) | Input, cache miss, per 1M tokens | Output per 1M tokens |
|---|---|---|
deepseek-flash, off-peak / peak |
$0.15 / $0.30 | $0.60 / $1.20 |
deepseek-v4-pro, off-peak / peak |
$0.66 / $1.32 | $1.98 / $3.96 |
Where the DeepSeek key goes
The loop runs on your server, so DEEPSEEK_API_KEY stays in its environment
and never enters the sandbox. Store it as a Runtime secret, bound to
api.deepseek.com, only if something inside the sandbox must call DeepSeek
itself, such as an agent you run there; the sandbox then sees a placeholder
that works on that host alone (secrets).
What the sandbox costs
A 2 vCPU, 4 GiB sandbox costs $0.03125 an hour waiting and $0.08 with both CPUs busy, billed on the CPU the code uses (pricing). The install is part of each run; to skip it for packages the model asks for often, bake them into a custom image and create from that. New accounts get 50 free sandbox hours, no card:
Terminalnpx withruntime sandbox run --trial -- python3 -c 'print(6 * 7)'More: run untrusted LLM code, turn off sandbox internet, agent evals and SWE-bench.
Sources
Checked 25 September 2026.
- DeepSeek: tool calls
- DeepSeek: thinking mode, section on tool calls
- DeepSeek: models and pricing
- openai on npm (7.23.0) and on PyPI (3.19.2)
Facts on this page were checked on 25 September 2026.