用脚本按 Claude Code plan 模式切换模型
Claude Code 在 plan 模式(计划模式)做规划、退出后做执行。很多团队希望 plan 阶段走强推理模型、执行阶段落性价比模型——但 Claude Code 客户端本身没有「按模式选模型」的开关。可在网关侧用脚本识别对话里注入的 plan 模式标记,据此 context.useModel 切换路由:plan 态留在当前(高质量)模型,退出 plan 后切到执行模型。
前置:先读 写第一个脚本变换 了解基本结构、挂载方式、Context API。
plan 模式标记怎么判定
Claude Code 的 plan 模式(计划模式)会在对话里注入标记:进入时通过 <system-reminder> 在当前轮末尾消息注入 Plan mode is active,退出时注入 Exited Plan Mode。一旦进入过 plan 模式,进入标记会永久留在对话历史里——所以不能用"是否包含该字符串"判断当前状态(退出后历史里仍能匹配到旧的进入标记,会永远误判为 plan 模式)。正确做法是从末尾消息向前扫描,取最后出现的标记作为当前状态。
plan 模式是 Claude Code(Anthropic 协议)特有的概念。如果入口模型同时服务其他协议的客户端,必须先判断 context.sourceProtocol——非 Anthropic 请求里不会有这两个标记,全文扫描会误判为"非 plan"而把流量切走。脚本开头先放过非 Anthropic 请求。
挂在哪里:入口模型的 before 槽
本例把脚本挂在入口模型(plan 模式所用的模型)的 before 槽(request_transform_before):plan 模式中不切换、保持当前模型;退出 plan 模式时 useModel 切到执行模型。
挂 before 槽是因为脚本要读客户端原始 Anthropic body 里的标记文本(after 槽拿到的是已翻译的上游格式,标记位置已变),且 plan→exec 是跨模型路由切换(before 槽支持跨协议切换,after 槽的 useModel 只能同协议)。before/after 两槽的完整说明见 脚本 API 参考。
控制台操作:上游与模型 → 编辑入口模型(plan 阶段所用的模型)→ 高级设置 → 转换脚本(JavaScript) → 请求 页签 → 运行于选 翻译前(Before),粘贴下面的脚本。
控制台实际界面(截图为英文 UI,中文 UI 对应「高级设置 / 转换脚本(JavaScript) / 请求 / 翻译前」):

注意 Runs 必须停在 Before、页签停在 Request——after 槽拿到的是翻译后的上游 body,读不到客户端注入的 plan 标记。
完整脚本
function transform(body, context) {
if (context.sourceProtocol !== "Anthropic") {
return body;
}
const EXEC_MODEL = "<your-execution-model>";
const extractText = (content) => {
if (!content) return "";
if (typeof content === "string") return content;
if (Array.isArray(content)) return content.map(b => (b && b.text) || "").join("\n");
return "";
};
// Claude Code 在 <system-reminder> 中注入 "Plan mode is active"(进入)
// 和 "Exited Plan Mode"(退出)。入口标记会留在历史中,
// 所以从末尾向前扫描,取最后出现的标记作为当前状态。
const messages = body.messages || [];
for (let i = messages.length - 1; i >= 0; i--) {
const lower = extractText(messages[i].content).toLowerCase();
const planIdx = lower.lastIndexOf("plan mode is active");
const exitIdx = lower.lastIndexOf("exited plan mode");
if (planIdx !== -1 || exitIdx !== -1) {
if (planIdx <= exitIdx) {
context.useModel(EXEC_MODEL);
}
return body;
}
}
// 消息中无标记,检查 system prompt 兜底
const sysLower = extractText(body.system).toLowerCase();
if (sysLower.includes("plan mode is active")) {
return body;
}
// 默认:切换到执行模型
context.useModel(EXEC_MODEL);
return body;
}要点:
- 必须放在 before 槽(
request_transform_before):脚本要读客户端原始 Anthropic body 里的标记文本,且 plan→exec 是跨模型路由切换。 context.sourceProtocol的取值是"Anthropic"/"OpenAIChatCompletions"/"Google"这种驼峰写法(不是anthropic这种小写形式)。脚本开头用!== "Anthropic"放过非 Claude Code 流量。EXEC_MODEL替换为你网关里配好的执行模型名(与客户端在model里请求的名字一致),不是「上游名/模型名」——useModel 按网关模型名解析路由。- 挂在入口模型上时,plan 态不调
useModel,请求落在本模型,本模型的request_payload规则(如prompt_cache_key)正好生效;exec 态切走后应用的是目标模型的规则,所以这类规则要在两个模型上各配一份。 - Claude Code 还会发旁路请求(如对话标题生成),这类请求不携带 plan 标记、也不是 plan 对话轮,按"无标记=非 plan"切到执行模型,属正常且无害。
- 从末尾向前逐消息扫描:标记只注入在当前轮末尾消息里,故从末尾向前取首个含标记的消息、比较其内两标记的位置即可判定当前状态。
- 大小写不敏感靠
.toLowerCase()+lastIndexOf:先将内容转小写再搜索。extractText拼接各文本块、.toLowerCase()各产生一次字符串分配,开销与消息体大小成正比;对 Claude Code 的典型请求(几十 KB)可忽略。
脚本内存上限与 OOM
每个脚本在独立的 QuickJS 沙箱里执行,堆上限由 SCRIPT_MEMORY_LIMIT_MB 控制(默认 64MB;改动需重启,不热生效)。脚本超过该上限即抛 out of memory,变换按错误模式兜底——默认的 log-and-continue 会静默丢弃本次脚本的全部副作用(包括 context.useModel),请求按原 body 继续,外在表现就是"脚本没生效 / 路由没切"。
脚本层救不了 body → JS 的固有转换内存:网关把请求体送进沙箱时,tools / messages 等结构(例如 Claude Code 的几十个工具定义)会先占一份内存,这是脚本层无法消除的 floor。若超大 body 仍 OOM,先调高 SCRIPT_MEMORY_LIMIT_MB;若希望彻底绕开 JS 内存墙、按文本标记做条件路由,需要执行引擎层面的原生能力(尚未实现,列入后续规划)。
验证
配好后分别发一条 plan 态、一条退出 plan 后的请求,到控制台 → 日志 看模型列:
- 退出 plan 后被
useModel切走的请求,模型名前会挂SW标签(script useModel swap),悬停可见切换来源;模型列显示执行模型(即你配置的EXEC_MODEL)。 - plan 态请求不切换,模型列仍是入口模型、无
SW标签。
两类请求的落点符合预期即脚本生效。若该切没切,先查是否 OOM 被静默丢弃(见上)。
常见问题
Q:退出 plan 模式后没切走 / 一直被判成 plan 态? 多半用了「是否包含 Plan mode is active」判断,或脚本挂在了 after 模式槽。进入标记永久留在历史里,退出后旧标记仍能匹配到,会永远误判为 plan 态。改成从末尾向前取末次标记;同时确认脚本挂在 before 槽——after 槽拿到的是上游格式 body,读不到客户端注入的标记文本位置。
Q:非 Claude Code 的客户端流量也被切到执行模型了? 漏了脚本开头的 context.sourceProtocol !== "Anthropic" 放行分支。其他协议的 body 里不会有这两个标记,会被兜底逻辑判成「非 plan」而切走。先判协议、非 Anthropic 直接 return body 放过。
Q:脚本看起来没生效,日志也没报错? 脚本 OOM 时按 log-and-continue 静默丢弃全部副作用(含 context.useModel),外在表现就是「路由没切」。Claude Code 的几十个工具定义会先占 body→JS 的固有转换内存,这是脚本层消除不了的 floor。先调高 SCRIPT_MEMORY_LIMIT_MB;见本页内存上限说明与 脚本配置。
下一步:用脚本做协议翻译修正 看其余 before / after 槽实战例子;脚本 API 参考 看 context.useModel 与完整内置函数表;脚本性能与内存 看开销来源与 after 槽 OOM 反模式。
