工具跑在 OpenRouter 服务端还是本地:两种形态各自的约束
给模型接一个”能跑命令”的工具,很多人第一反应是挑工具名。但真正决定后面所有麻烦的,是这条命令最终落在哪台机器上。执行位置一旦定了,权限归谁批、出问题时你能看到什么、凭据暴露在哪一侧,全都跟着定死了——而这两件事,OpenRouter 和 Claude Code 的官方文档恰好写在两个完全不同的位置上。
这篇只对照双方文档白纸黑字写明的机制,不排名,也不替任何一方补它没写的东西。
先看两边把执行位置写死在哪
OpenRouter 官方文档《Server Tools》页(openrouter.ai/docs/guides/features/server-tools)开头有一张三列对照表,把工具按”谁决定用、谁执行”分成三类:Server Tools(模型决定用,OpenRouter 执行)、Plugins(启用后每次请求固定跑一次,OpenRouter 执行)、User-Defined Tools(模型决定用,你的应用执行)。类型前缀也不同:服务端工具是 openrouter:*,用户自定义工具是 function。这张表本身就是本文的题眼——执行主体是写在类型里的,不是运行时才协商的。
这一整块目前标着 Beta。 文档原话是服务端工具处于 beta,API 与行为可能变化,openrouter:shell 与 openrouter:apply_patch 两页也各自挂了同一个 Beta 标记。下面提到的字段和默认值都在这个前提下看。
openrouter:shell 这一页写得更绝:它明说这个工具没有客户端执行模式,命令永远在托管环境里跑——文档给的两种落法是 OpenAI 自己的原生 shell,或者 OpenRouter 的 sandbox;engine 那一栏的说明也是同一个口径,auto 会在可用时保留供应商的原生托管 shell、其余情况路由到 OpenRouter 沙箱。配置项 environment 的说明里则直接写了 local 环境不受支持。命令在一个隔离的 Linux 容器里执行,文档的措辞是”不在 OpenRouter 基础设施上,也不在你的机器上”。
Claude Code 官方文档《Tools reference》页(code.claude.com/docs/en/tools-reference)对 Bash 工具的一句话描述则是”在你的环境中执行 shell 命令”,权限列标的是 Yes——同一页还补了一条例外:Bash 虽然标 Yes,但内置的一组只读命令不会触发询问。同一页开头还写明,工具名这个字符串会同时出现在三个地方:permission 规则、subagent 的工具清单、hook matcher。
顺带说一个口径不一致:openrouter:shell 页里两处提到一个 Bash 工具,说”与 Bash 工具不同,shell 工具没有客户端执行模式”,并在末尾给了它的链接。但《Server Tools》概览页的可用工具表里并没有这一行,我们手上的落盘文档集合里也没有这一页。所以那个”有客户端执行模式”的工具究竟怎么工作,我们没有依据,不写。
谁批准这条命令:两边的粒度根本不在一个层级
Claude Code 这边是逐条命令的。《Tools reference》页给出的规则格式是 ToolName(specifier),Bash 与 Monitor 共用命令模式匹配,写法形如 Bash(npm run *);这套规则可以出现在 settings 的 permissions.allow / permissions.deny、--allowedTools / --disallowedTools、subagent frontmatter、skill 的 allowed-tools 以及 hook 的 if 条件里。
再往下一层是沙箱。《Configure the sandboxed Bash tool》页(code.claude.com/docs/en/sandboxing)把两层的区别讲得很清楚:permission 规则在命令跑之前评估,依据是命令字符串;沙箱边界由操作系统在已经运行的进程上强制执行,所以”不管模型选择运行什么,也不管一条被允许的命令实际做的比名字多多少”,边界都成立。实现层面文档也写了:macOS 用 Seatbelt,Linux 与 WSL2 用 bubblewrap。
OpenRouter 这边的控制点不在单条命令上,而在整个请求的循环预算上。《Server Tools》页写明两个与 messages、tools 同级的顶层字段:max_tool_calls 限制整个请求中服务端工具的总步数,stop_server_tools_when 接收一组停止条件(文档举的例子包括步数与花费上限等),并且一旦设置就覆盖 max_tool_calls。另外,跑内层 agent 循环的工具(Fusion、Advisor、Subagent)还各有一份写在自己 parameters 里的独立预算,与外层互不影响。这几个字段的默认值与上限文档里都列了表,但这里不抄具体数值——服务端工具这一组页面自己就写明,默认值与上限反映的是当前服务端强制的限额,beta 期间可能变动。
“每条命令单独批一次”这个动作,我们在 OpenRouter 的文档里没有找到对应说明,不比。 它给的是预算与停止条件,不是逐条审批。
可见性:你能看到的东西完全不一样
服务端执行意味着你只能通过返回结构来看。openrouter:shell 页写明两个 API 表面不一样:在 Responses API 上,一次调用变成一个 openrouter:shell 输出项(如果你发的是 OpenAI 的原生工具形状,则是原生 shell_call);在 Messages API 上,它变成一个名为 openrouter:shell 的 server_tool_use 内容块,配一个 openrouter_shell_tool_result 块承载每条命令的输出。文档自述了这个命名的原因:Anthropic 那边没有定义原生的 shell result 块,所以输出落在 OpenRouter 自己命名空间的这个块里。
每条命令回来的结构是 stdout、stderr 和一个 outcome,后者要么是 {"type": "exit", "exit_code": <int>},要么是 {"type": "timeout"}。非零退出码算失败,错误输出走 stderr 交给模型自己读、自己反应。用量那侧,《Server Tools》页写明服务端工具的用量记在响应 usage 对象的 server_tool_use 字段里。
本地执行的暴露面在另一头。Claude Code 沙箱文档写明默认的读策略是整台机器可读(除少数被拒目录),并且特意点名说这个默认”仍然允许读取 ~/.aws/credentials 和 ~/.ssh/ 这类凭据文件”,要挡住得自己写 sandbox.credentials 的 files / envVars 条目,或者把路径加进 denyRead。写权限则默认只有当前工作目录及其子目录、加上会话临时目录。环境变量方面文档也写明:沙箱内的 Bash 命令默认继承父进程环境,包括设在那里的凭据。
把这两段并排看,结论不是谁更安全,而是你要防的东西不是同一个:服务端那侧你的本机凭据本来就不在那台机器上,但容器里预置了什么、能连哪些外网、日志留存多久,官方文档没有说明这些,我们没有依据;本地那侧你能逐条审计命令,代价是默认可见范围就是你整台电脑。
顺便,两边文档都写了各自的安全边界口径,值得原样记住:Claude Code 的沙箱页有一节标题就叫 Limitations,写明沙箱”降低风险但不是一个完整的隔离边界”;OpenRouter 的 shell 页则写容器按账户与工作区划分、不跨租户共享,container_auto 下容器是临时的、container_reference 下会跨请求保留。这是两句各自的原话,不是我们的评价。
状态留不留得住
这一维两边都有依据,可以直接比。
OpenRouter 的 shell 工具靠 environment 决定:{"type": "container_auto"} 是 OpenRouter 托管的临时容器,{"type": "container_reference", "container_id": "..."} 复用一个已有容器。还有一个 sleep_after_seconds 控制容器在最后一条命令之后保持温热多久,文档写明它是空闲计时——每条命令都会重置这个计时器,并且有服务端上限(具体数值这里不抄,文档自己标了 beta 期间可能变)。
Claude Code 的 Bash 工具则是每条命令一个独立进程。文档写明:环境变量不持久,前一条里的 export 到下一条就没了;shell 启动文件里的别名和函数会在会话启动时被捕获并应用到每条命令;cd 在主会话里会带到后面的命令,但仅限停留在项目目录或你用 --add-dir 之类加进来的额外工作目录内,超出就会被重置并在结果里追加一行 Shell cwd was reset to <dir>,而 subagent 会话从不携带工作目录变更。
要改的是”你本机的文件”时,路子完全变了
这就是 openrouter:apply_patch 存在的原因。这一页(openrouter.ai/docs/guides/features/server-tools/apply-patch)写得非常直白:这是一个 human-in-the-loop 工具,OpenRouter 只校验 diff 语法(行前缀是否正确、标记是否合法、路径是否非空),从不应用它;校验通过后作为 apply_patch_call 输出项交回你的应用,由你落盘,再在下一轮把结果作为 apply_patch_call_output 回声回去。校验不通过,错误回给模型让它自己改。
文档写明它支持三种操作类型,都挂在 apply_patch_call 的 operation 字段上:create_file、update_file、delete_file。回声那条输入项的字段是 call_id(必须与 apply_patch_call 的一致)、status("completed" 或 "failed")、可选的 output(人类可读的日志)。
两条限制必须照实标:这个工具只在 Responses API 上可用,通过 Chat Completions API 不受支持;另外文档单独挂了一条 streaming 说明——只有 OpenAI 模型会通过 response.apply_patch_call_operation_diff.delta 事件增量地流式返回补丁,其它模型是把完整补丁作为单个工具输出一次性返回。engine 参数三档:auto(端点支持增量 diff 流式时走原生透传,否则回落到 OpenRouter 的 HITL 校验器)、native(强制原生透传,不支持时回落 HITL)、openrouter(永远走 HITL 校验器)。文档写明原生透传目前支持在 OpenAI 端点上——平台上的供应商与端点随时在变,以官方文档最新内容为准,别把这一句当成长期成立的清单。
对照过来,Claude Code 的写文件路径在另一个体系里:沙箱页的 Scope 一节写明,内置文件工具 Read、Edit、Write 直接走权限系统,不经过沙箱——沙箱隔离的是 Bash 子进程。
所以这一维的结论很干脆:要动你本机的文件,OpenRouter 的设计是把执行权留在你这边,它只做语法校验;Claude Code 则是自己动手,靠权限规则和读前必读之类的检查来兜。 两条路要写的代码完全不同,别指望换个工具名就能平移。
{
"type": "openrouter:shell",
"parameters": {
"engine": "openrouter",
"environment": { "type": "container_auto" }
}
}
{
"type": "openrouter:apply_patch",
"parameters": {
"engine": "auto"
}
}
以上为按官方文档中的字段语义组合的示例,未经实测,以官方文档与 API 的实际响应为准。
Windows 这一侧要单独说
本地那条路在 Windows 上有硬约束。Claude Code 的沙箱文档写明:沙箱运行在 macOS、Linux 和 WSL2 上,原生 Windows 不受支持,Windows 用户要在 WSL2 发行版里跑;WSL1 也不支持,因为 bubblewrap 需要只有 WSL2 才有的内核特性。同一页还写了 WSL2 下的一个具体行为:启动 cmd.exe、powershell.exe 或 /mnt/c/ 下的 Windows 二进制,WSL 会通过 Unix socket 交给 Windows 宿主,所以能不能拦住取决于沙箱的 Unix socket 设置,而且要先装上那个可选的 seccomp 过滤器才拦得住;要放行用 allowAllUnixSockets,要整个排除出沙箱用 excludedCommands。另外《Tools reference》页在 PowerShell 工具的 Preview limitations 里明写:在 Windows 上不支持 sandboxing。
服务端那条路对本机系统不挑——openrouter:shell 跑在 Linux 容器里,你本机是不是 Windows 不影响调用;但也正因为文档写明执行环境是 Linux 容器,那边能跑的就是 Linux 那套命令,别拿本机 PowerShell 的习惯去套。真正会卡住你的约束在 API 表面:shell 工具只在 Responses API 与 Messages API 上可用,在 Chat Completions API 上请求它会返回 400。如果你的接入层是围着 Chat Completions 建的,这不是配置问题,是要改接入方式。
Linux / macOS 侧则相反:本地沙箱在这两个平台上是原生可用的,/sandbox 面板、bubblewrap 与 socat 这些依赖都在文档里写明了安装方式。
决策路径
从你的处境倒推,而不是从功能表倒推:
- 你要让模型改你本地仓库里的文件 → 服务端 shell 帮不上,它的
local环境明确不受支持。走openrouter:apply_patch自己落盘,或者用本地执行的工具链。 - 你要跑的东西和你的仓库无关(一段计算、一次格式校验、一个临时脚本)→ 服务端 shell 省掉了你自己设计本机权限边界这件事,代价是你只能通过返回块看到发生了什么。
- 你在 Windows 上,且需要操作系统级的隔离 → 本地那条路必须落到 WSL2,原生 Windows 没有这个能力;PowerShell 工具在 Windows 上也不支持 sandboxing。
- 你需要逐条命令留痕、按模式放行或拦截 → 本地这套
ToolName(specifier)规则加 hook matcher 是有依据的;OpenRouter 侧我们只查到请求级的预算与停止条件。 - 你的接入层只走 Chat Completions → 先解决接口,shell 与 apply patch 在这个接口上都不可用。
- 你需要跨请求保留状态 → OpenRouter 侧有
container_reference;本地 Bash 每条命令独立进程,环境变量不跨命令。
明确没有依据、这篇不比的几项
- 服务端容器的网络出口策略:Claude Code 沙箱有
allowedDomains、代理与严格白名单一整套写明的机制,OpenRouter 容器能连哪些外网,我们在文档里没有找到对应说明。 - 容器内预置了什么运行时与工具链:没有说明。
- 两边的审计日志留存:没有找到可对照的说明。
- 性能、并发、延迟:两边文档都没有可核对的口径,任何比较都会是编的。
最后提醒一句老生常谈但确实会咬人的事:这两个产品的迭代都很快,本文引用的字段名、参数取值和 beta 状态随时可能变,动手前请以官方文档最新内容为准。
本文依据 OpenRouter 官方文档(openrouter.ai/docs)于 2026-08-18 的公开内容整理。
该平台闭源,本文只复述官方文档写明的机制,不推断其内部实现;
我们没有对文中涉及的功能做过实测,因此不涉及界面外观与运行表现的任何描述。
该平台的供应商、模型与路由策略随时变动,文中不列具体供应商名单与模型清单;
价格、额度与限流的具体数值请以官方定价页与用量说明为准。
本文涉及的另一方内容依据其官方文档整理(Claude Code:code.claude.com/docs)。
双方均为闭源商业产品,本文只对照各方公开写明的机制,不推断实现,也不对产品做优劣排名。
安全与合规相关做法请结合自身环境评估,本文不构成安全方案建议。