Enable apply_patch bridging for all non-GPT Codex models

This commit is contained in:
musistudio
2026-07-20 07:13:53 +08:00
parent 9fb8a89831
commit 5021cc35c0
7 changed files with 582 additions and 108 deletions
@@ -55,11 +55,11 @@ After saving, CCR formats those descriptions as “Configured CCR gateway models
### Codex
The built-in Codex route adapts Codex's `apply_patch` file-editing tool for third-party or non-GPT models. The goal is for those models to edit files through the patch tool instead of generating commands or scripts such as `cat >`, `sed -i`, `python`, or `node`.
CCR automatically adapts Codex's `apply_patch` file-editing tool for third-party or non-GPT models. The goal is for those models to edit files through the patch tool instead of generating commands or scripts such as `cat >`, `sed -i`, `python`, or `node`.
Technically, this is a tool protocol bridge. Native Codex `apply_patch` is a custom/freeform tool whose input is raw patch text, while many OpenAI-compatible third-party models handle ordinary function tools more reliably. CCR rewrites `apply_patch` into an upstream-visible `virtual_apply_patch` function tool and injects the full `apply_patch.lark` grammar into the tool description, requiring the model to put the patch in the `patch` field.
When the model returns `virtual_apply_patch`, CCR rewrites it back to Codex's expected shape: `custom_tool_call` with `name = apply_patch` and `input = raw patch text`. CCR does not edit files directly; Codex still executes the resulting patch. This adaptation follows the built-in **Codex** route and has no separate switch. GPT-named models keep using Codex's native freeform `apply_patch` path.
When the model returns `virtual_apply_patch`, CCR rewrites it back to Codex's expected shape: `custom_tool_call` with `name = apply_patch` and `input = raw patch text`. CCR does not edit files directly; Codex still executes the resulting patch. This adaptation is enabled automatically for non-GPT models and is independent of the built-in **Codex** routing switch. GPT-named models, including Fusion models whose resolved base model is GPT, keep using Codex's native freeform `apply_patch` path.
## Custom Routing
@@ -55,11 +55,11 @@ Description 建议写成任务导向,而不是只写模型厂商名。例如
### Codex
Codex 内置路由会为第三方或非 GPT 模型适配 Codex 的 `apply_patch` 文件编辑工具。目标是让这些模型通过 patch 工具完成文件修改,而不是生成 `cat >``sed -i``python``node` 等命令或脚本来编辑文件。
CCR 会自动为第三方或非 GPT 模型适配 Codex 的 `apply_patch` 文件编辑工具。目标是让这些模型通过 patch 工具完成文件修改,而不是生成 `cat >``sed -i``python``node` 等命令或脚本来编辑文件。
技术原理是做一次工具协议桥接:Codex 原生的 `apply_patch` 是 custom/freeform 工具,入参是原始 patch 文本;很多 OpenAI-compatible 三方模型更擅长普通 function tool。CCR 会在上游请求中把 `apply_patch` 转成 `virtual_apply_patch` function tool,并在工具说明里注入完整的 `apply_patch.lark` 语法,要求模型把 patch 写入 `patch` 字段。
模型返回 `virtual_apply_patch` 后,CCR 会把它转换回 Codex 期望的 `custom_tool_call``name = apply_patch``input = 原始 patch 文本`。CCR 不直接修改文件,真正执行 patch 的仍然是 Codex 客户端。这个适配跟随 **Codex** 内置路由启用或关闭,没有单独开关;GPT 命名模型继续使用 Codex 原生 freeform `apply_patch` 路径。
模型返回 `virtual_apply_patch` 后,CCR 会把它转换回 Codex 期望的 `custom_tool_call``name = apply_patch``input = 原始 patch 文本`。CCR 不直接修改文件,真正执行 patch 的仍然是 Codex 客户端。这个适配会对非 GPT 模型自动启用,不受 **Codex** 内置路由开关影响;GPT 命名模型以及实际基模为 GPT 的 Fusion 模型继续使用 Codex 原生 freeform `apply_patch` 路径。
## 自定义路由
@@ -182,7 +182,6 @@ function codexModelCapabilityProfile(
const catalogEntry = findModelCatalogEntry(model);
const capabilities = catalogEntry?.capabilities ?? {};
const providerProtocol = provider ? codexProviderProtocol(provider) : undefined;
const providerSupportsResponses = provider ? codexProviderSupportsResponses(provider) : false;
const supportsFusionVision = codexVirtualModelSupportsFusionVision(model, config);
const supportsFusionWebSearch = codexVirtualModelSupportsFusionWebSearch(model, config);
const metadataReasoningLevels = normalizeProviderReasoningLevels(providerModelMetadata?.supportedReasoningLevels);
@@ -195,9 +194,9 @@ function codexModelCapabilityProfile(
?? (metadataReasoningLevels !== undefined || readCatalogCapability(capabilities, "reasoning"));
const supportsImageInput = supportsFusionVision || catalogEntrySupportsImageInput(catalogEntry);
const supportsParallelToolCalls = readCatalogCapability(capabilities, "parallelFunctionCalling");
const applyPatchToolType = providerSupportsResponses || catalogModelLooksLikeGpt(model, catalogEntry) || codexPatchBridgeApplies(model, catalogEntry, config)
? "freeform"
: null;
// Codex must emit apply_patch for both native GPT models and non-GPT models
// that the gateway converts through the compatibility bridge.
const applyPatchToolType = "freeform";
const supportsSearchTool =
supportsFusionWebSearch ||
(
@@ -489,13 +488,6 @@ function codexProviderProtocol(provider: GatewayProviderConfig): GatewayProvider
return normalizeProviderProtocol(provider.type) ?? normalizeProviderProtocol(provider.provider) ?? inferProviderProtocol(provider);
}
function codexProviderSupportsResponses(provider: GatewayProviderConfig): boolean {
return uniqueProviderProtocols((provider.capabilities ?? []).map((capability) => normalizeProviderProtocol(capability.type))).includes("openai_responses") ||
normalizeProviderProtocol(provider.type) === "openai_responses" ||
normalizeProviderProtocol(provider.provider) === "openai_responses" ||
providerEndpointLooksLikeResponses(provider);
}
function inferProviderProtocol(provider: GatewayProviderConfig): GatewayProviderProtocol {
const url = (provider.baseUrl || provider.baseurl || provider.api_base_url || "").toLowerCase();
const transformer = JSON.stringify(provider.transformer ?? "").toLowerCase();
@@ -519,30 +511,6 @@ function providerEndpointLooksLikeResponses(provider: GatewayProviderConfig): bo
return url.endsWith("/responses") || url.includes("/responses?");
}
function catalogModelLooksLikeGpt(model: string, entry: ModelCatalogEntry | undefined): boolean {
return [
model,
entry?.id,
entry?.model
].some((value) => typeof value === "string" && value.toLowerCase().includes("gpt"));
}
function codexPatchBridgeApplies(
model: string,
entry: ModelCatalogEntry | undefined,
config?: Partial<Pick<AppConfig, "Router">>
): boolean {
const codexRule = config?.Router?.builtInRules?.codex;
if (!codexRule || codexRule.enabled === false) {
return false;
}
return !catalogModelLooksLikeGpt(modelNameForPatchBridge(model), entry);
}
function modelNameForPatchBridge(model: string): string {
return parseModelSelector(model)?.model ?? model;
}
function normalizeProviderProtocol(value: unknown): GatewayProviderProtocol | undefined {
if (typeof value !== "string") {
return undefined;
@@ -10,6 +10,7 @@ import { readHeader } from "@ccr/core/gateway/http/io";
import { codexPatchBridgeInstructionText, codexPatchBridgeShellToolGuidance, virtualApplyPatchLarkGrammar, virtualApplyPatchToolName } from "@ccr/core/gateway/internal/shared";
import { parseJsonObjectSafe, serializeJsonBody } from "@ccr/core/gateway/http/body";
import { requestProtocolForPath } from "@ccr/core/routing/protocol-endpoints";
import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution";
export function prepareCodexApplyPatchBridgeRequest(input: {
@@ -20,7 +21,7 @@ export function prepareCodexApplyPatchBridgeRequest(input: {
path: string;
routedModel?: string;
}): { body: Buffer; diagnostic: string } | undefined {
if (!codexApplyPatchBridgeEnabled(input.config, input.headers, input.method, input.path)) {
if (!codexApplyPatchBridgeEnabled(input.headers, input.method, input.path)) {
return undefined;
}
const parsedBody = parseJsonObjectSafe(input.body);
@@ -28,7 +29,7 @@ export function prepareCodexApplyPatchBridgeRequest(input: {
return undefined;
}
const model = input.routedModel || stringValue(parsedBody.model);
if (!codexPatchBridgeModelEligible(model)) {
if (!codexPatchBridgeModelEligible(model, input.config)) {
return undefined;
}
const transformed = transformCodexApplyPatchBridgeRequestBody(parsedBody);
@@ -255,12 +256,10 @@ function virtualApplyPatchToolSpec(): Record<string, unknown> {
}
function codexApplyPatchBridgeEnabled(config: AppConfig, headers: IncomingHttpHeaders, method: string, path: string): boolean {
const codexRule = config.Router.builtInRules?.codex;
function codexApplyPatchBridgeEnabled(headers: IncomingHttpHeaders, method: string, path: string): boolean {
return (method || "GET").toUpperCase() === "POST" &&
requestProtocolForPath(path) === "openai_responses" &&
isCodexUserAgent(headers) &&
codexRule?.enabled !== false;
isCodexUserAgent(headers);
}
@@ -269,9 +268,13 @@ function isCodexUserAgent(headers: IncomingHttpHeaders): boolean {
}
function codexPatchBridgeModelEligible(model: string | undefined): boolean {
function codexPatchBridgeModelEligible(model: string | undefined, config: AppConfig): boolean {
const modelName = modelNameForPatchBridge(model);
return Boolean(modelName) && !modelName.toLowerCase().includes("gpt");
if (!modelName || modelName.toLowerCase().includes("gpt")) {
return false;
}
const baseModelName = modelNameForPatchBridge(resolveUsageModelAttribution(config, model).model);
return !baseModelName.toLowerCase().includes("gpt");
}
@@ -22,7 +22,7 @@ test("codex catalog removes duplicate model IDs case-insensitively without reord
assert.deepEqual(ids, ["provider/model-a", "Provider/MODEL-B"]);
});
test("codex catalog treats unknown models as text-only without advanced tools", () => {
test("codex catalog treats unknown models as text-only while enabling apply_patch", () => {
const model = catalogModelFor({
Providers: [
{ name: "Custom", type: "openai_chat_completions", models: ["unknown-model"] }
@@ -36,7 +36,7 @@ test("codex catalog treats unknown models as text-only without advanced tools",
assert.equal(model.supports_image_detail_original, false);
assert.deepEqual(model.supported_reasoning_levels, []);
assert.equal(model.default_reasoning_level, null);
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog uses model catalog capabilities for known text models", () => {
@@ -53,7 +53,7 @@ test("codex catalog uses model catalog capabilities for known text models", () =
assert.equal(model.supports_image_detail_original, false);
assert.deepEqual(model.supported_reasoning_levels, []);
assert.equal(model.default_reasoning_level, null);
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog enables multimodal reasoning and search when provider protocol supports it", () => {
@@ -342,7 +342,7 @@ test("codex catalog enables native search for Gemini Interactions providers", ()
assert.equal(model.supports_search_tool, true);
});
test("codex catalog does not expose native search through chat-completions-only providers", () => {
test("codex catalog omits native search but enables apply_patch for non-GPT chat-completions models", () => {
const model = catalogModelFor({
Providers: [
{ name: "openrouter", type: "openai_chat_completions", models: ["google/gemini-2.5-pro"] }
@@ -354,7 +354,7 @@ test("codex catalog does not expose native search through chat-completions-only
assert.equal(model.supports_reasoning_summaries, true);
assert.equal(model.supports_search_tool, false);
assert.equal(model.web_search_tool_type, "text");
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog keeps freeform apply_patch for GPT-named chat-compatible models", () => {
@@ -403,7 +403,7 @@ test("codex catalog enables apply_patch bridge for non-GPT models when Codex bui
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog disables apply_patch bridge for non-GPT models when the Codex built-in route is off", () => {
test("codex catalog automatically enables apply_patch bridge for non-GPT models when the Codex built-in route is off", () => {
const model = catalogModelFor({
Providers: [
{ name: "openrouter", type: "openai_chat_completions", models: ["google/gemini-2.5-pro"] }
@@ -418,7 +418,7 @@ test("codex catalog disables apply_patch bridge for non-GPT models when the Code
}
}, "openrouter/google/gemini-2.5-pro");
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog marks Fusion aliases with builtin web search as searchable", () => {
@@ -451,7 +451,7 @@ test("codex catalog marks Fusion aliases with builtin web search as searchable",
assert.deepEqual(model.input_modalities, ["text"]);
assert.equal(model.supports_search_tool, true);
assert.equal(model.web_search_tool_type, "text");
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
test("codex catalog marks image-recognition Fusion aliases as image capable", () => {
@@ -514,5 +514,5 @@ test("codex catalog marks prefixed Fusion virtual models with legacy web search
assert.deepEqual(model.input_modalities, ["text"]);
assert.equal(model.supports_search_tool, true);
assert.equal(model.web_search_tool_type, "text");
assert.equal(model.apply_patch_tool_type, null);
assert.equal(model.apply_patch_tool_type, "freeform");
});
@@ -62,6 +62,89 @@ test("Codex patch bridge leaves GPT models untouched", () => {
assert.equal(result, undefined);
});
test("Codex patch bridge automatically rewrites non-GPT models when the built-in route is disabled", () => {
const result = prepareCodexApplyPatchBridgeRequest({
body: Buffer.from(JSON.stringify({
model: "openrouter/google/gemini-2.5-pro",
tools: [{ type: "custom", name: "apply_patch" }]
})),
config: {
...config,
Router: {
...config.Router,
builtInRules: {
...config.Router.builtInRules,
codex: { enabled: false }
}
}
},
headers: { "user-agent": "codex-test" },
method: "POST",
path: "/v1/responses"
});
assert.ok(result);
const body = JSON.parse(result.body.toString("utf8"));
assert.equal(body.tools[0].type, "function");
assert.equal(body.tools[0].name, "virtual_apply_patch");
});
test("Codex patch bridge leaves GPT-backed Fusion models untouched", () => {
const result = prepareCodexApplyPatchBridgeRequest({
body: Buffer.from(JSON.stringify({
model: "Fusion/research",
tools: [{ type: "custom", name: "apply_patch" }]
})),
config: {
...config,
Providers: [
{ name: "openai", type: "openai_chat_completions", models: ["gpt-5-codex"] }
],
virtualModelProfiles: [
{
baseModel: { fixedModel: "openai/gpt-5-codex", mode: "fixed" },
enabled: true,
match: { exactAliases: ["research"], prefixes: [], suffixes: [] }
}
]
},
headers: { "user-agent": "codex-test" },
method: "POST",
path: "/v1/responses"
});
assert.equal(result, undefined);
});
test("Codex patch bridge rewrites non-GPT-backed Fusion models", () => {
const result = prepareCodexApplyPatchBridgeRequest({
body: Buffer.from(JSON.stringify({
model: "Fusion/research",
tools: [{ type: "custom", name: "apply_patch" }]
})),
config: {
...config,
Providers: [
{ name: "anthropic", type: "anthropic_messages", models: ["claude-sonnet-4"] }
],
virtualModelProfiles: [
{
baseModel: { fixedModel: "anthropic/claude-sonnet-4", mode: "fixed" },
enabled: true,
match: { exactAliases: ["research"], prefixes: [], suffixes: [] }
}
]
},
headers: { "user-agent": "codex-test" },
method: "POST",
path: "/v1/responses"
});
assert.ok(result);
const body = JSON.parse(result.body.toString("utf8"));
assert.equal(body.tools[0].name, "virtual_apply_patch");
});
test("Codex patch bridge discourages shell-based file edits", () => {
const result = prepareCodexApplyPatchBridgeRequest({
body: Buffer.from(JSON.stringify({
@@ -1,9 +1,10 @@
import assert from "node:assert/strict";
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { createServer } from "node:http";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import { gatewayService } from "@ccr/core/gateway/application/gateway-service.ts";
import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin.ts";
import { buildRouteScriptInput } from "@ccr/core/routing/route-script-context.ts";
import { RouteScriptRuntime } from "@ccr/core/routing/route-script-runtime.ts";
@@ -18,8 +19,8 @@ const routeScriptDirectory = mkdtempSync(path.join(os.tmpdir(), "ccr-route-scrip
let routeScriptFileIndex = 0;
test.after(() => rmSync(routeScriptDirectory, { force: true, recursive: true }));
function routeScript(source, overrides = {}) {
const file = path.join(routeScriptDirectory, `route-${++routeScriptFileIndex}.js`);
function routeScript(source, overrides = {}, extension = "js") {
const file = path.join(routeScriptDirectory, `route-${++routeScriptFileIndex}.${extension}`);
writeFileSync(file, source, "utf8");
return {
apiVersion: 1,
@@ -30,6 +31,31 @@ function routeScript(source, overrides = {}) {
};
}
function scriptRule(id, script, overrides = {}) {
return {
enabled: true,
id,
name: id,
script,
type: "script",
...overrides
};
}
function routingConfig(rules = []) {
return {
CUSTOM_ROUTER_PATH: "",
Providers: [{ models: ["alpha", "beta", "fallback"], name: "Provider", type: "anthropic_messages" }],
Router: {
builtInRules: { "claude-code": { enabled: false }, codex: { enabled: false } },
fallback: { mode: "off", models: [], retryCount: 0 },
rules
},
profile: { enabled: false, profiles: [] },
virtualModelProfiles: []
};
}
function scriptInput(script, body = {}) {
return buildRouteScriptInput({
body,
@@ -60,15 +86,112 @@ test("route script input exposes the complete request body and headers", () => {
assert.equal(input.headers["x-visible"], "yes");
});
test("route script input derives every documented routing summary field", () => {
const body = {
messages: [
{ content: "ignore assistant", role: "assistant" },
{
content: [
{ text: "route this request", type: "text" },
{ source: { media_type: "image/png" }, type: "image" }
],
role: "user"
}
],
model: "Provider/alpha",
system: [{ text: "system policy", type: "text" }],
tools: [{ name: "direct_tool" }, { function: { name: "function_tool" } }]
};
const input = buildRouteScriptInput({
body,
builtInSubagentModel: "Provider/beta",
headers: { "X-Auth-Api-Key-Id": "key-id", "x-tags": ["one", "two"] },
log: console,
method: "POST",
sessionId: "session-1",
tokenCount: 321,
url: "/v1/messages?beta=true"
});
assert.notEqual(input.body, body);
assert.deepEqual(input, {
apiKeyId: "key-id",
body,
builtInSubagentModel: "Provider/beta",
headers: { "X-Auth-Api-Key-Id": "key-id", "x-tags": ["one", "two"] },
method: "POST",
model: "Provider/alpha",
sessionId: "session-1",
summary: {
hasImage: true,
lastUserText: "route this request",
messageCount: 2,
systemText: "system policy",
toolNames: ["direct_tool", "function_tool"]
},
tokenCount: 321,
url: "/v1/messages?beta=true"
});
});
test("route scripts receive frozen per-request input and a stable hash helper", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const script = routeScript(`
const blocked = [];
try { input.body.model = "Provider/beta"; } catch { blocked.push("body"); }
try { input.headers["x-visible"] = "changed"; } catch { blocked.push("headers"); }
try { api.fs.readText = undefined; } catch { blocked.push("api"); }
const leaked = globalThis.routeScriptLeak === true;
globalThis.routeScriptLeak = true;
return {
blocked,
hash: api.hash(input.sessionId),
leaked,
model: input.body.model,
visibleHeader: input.headers["x-visible"]
};
`);
const input = buildRouteScriptInput({
body: { model: "Provider/alpha" },
headers: { "x-visible": "yes" },
log: console,
method: "POST",
sessionId: "stable-session",
url: "/v1/messages"
});
try {
const first = await runtime.execute("frozen-input-1", script, input);
const second = await runtime.execute("frozen-input-2", script, input);
assert.equal(first.status, "ok");
assert.equal(second.status, "ok");
assert.deepEqual(first.value.blocked, ["body", "headers", "api"]);
assert.equal(first.value.model, "Provider/alpha");
assert.equal(first.value.visibleHeader, "yes");
assert.equal(first.value.leaked, false);
assert.equal(second.value.leaked, false);
assert.equal(first.value.hash, second.value.hash);
assert.ok(Number.isInteger(first.value.hash));
} finally {
await runtime.close();
}
});
test("route scripts load local files and pick up file changes", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const script = routeScript("return 'first';");
try {
assert.deepEqual(await runtime.validate(script), { diagnostics: [], ok: true });
const first = await runtime.execute("file-reload", script, scriptInput(script));
assert.equal(first.status, "ok");
assert.equal(first.value, "first");
writeFileSync(script.file, "return {;", "utf8");
const invalid = await runtime.validate(script);
assert.equal(invalid.ok, false);
writeFileSync(script.file, "return 'second';", "utf8");
assert.deepEqual(await runtime.validate(script), { diagnostics: [], ok: true });
const second = await runtime.execute("file-reload", script, scriptInput(script));
assert.equal(second.status, "ok");
assert.equal(second.value, "second");
@@ -77,6 +200,60 @@ test("route scripts load local files and pick up file changes", async () => {
}
});
test("route scripts support every documented file extension and legacy inline source", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
try {
for (const extension of ["js", "mjs", "cjs"]) {
const script = routeScript(`return "${extension}";`, {}, extension);
assert.deepEqual(await runtime.validate(script), { diagnostics: [], ok: true });
const result = await runtime.execute(`extension-${extension}`, script, scriptInput(script));
assert.equal(result.status, "ok");
assert.equal(result.value, extension);
}
const inline = {
apiVersion: 1,
language: "javascript",
source: "await Promise.resolve(); return 'inline';",
timeoutMs: 500
};
assert.deepEqual(await runtime.validate(inline), { diagnostics: [], ok: true });
const result = await runtime.execute("legacy-inline", inline, scriptInput(inline));
assert.equal(result.status, "ok");
assert.equal(result.value, "inline");
} finally {
await runtime.close();
}
});
test("route script validation rejects invalid metadata, paths, content, and limits", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const valid = routeScript("return true;");
const directoryFile = path.join(routeScriptDirectory, `directory-${++routeScriptFileIndex}.js`);
mkdirSync(directoryFile);
const cases = [
[{ ...valid, apiVersion: 2 }, /unsupported route script api or language/i],
[{ ...valid, language: "typescript" }, /unsupported route script api or language/i],
[{ ...valid, file: undefined, source: " " }, /between 1 and 65536 bytes/i],
[{ ...valid, file: `${valid.file}.txt` }, /\.js, \.mjs, or \.cjs extension/i],
[{ ...valid, timeoutMs: 9 }, /between 10 and 30000 ms/i],
[{ ...valid, timeoutMs: 30001 }, /between 10 and 30000 ms/i],
[{ ...valid, file: path.join(routeScriptDirectory, "missing.js") }, /unable to read route script file/i],
[{ ...valid, file: directoryFile }, /is not a file/i],
[routeScript("x".repeat(64 * 1024 + 1)), /exceeds 65536 bytes/i]
];
try {
for (const [script, expectedMessage] of cases) {
const result = await runtime.validate(script);
assert.equal(result.ok, false);
assert.equal(result.diagnostics[0].code, "script-source-invalid");
assert.match(result.diagnostics[0].message, expectedMessage);
}
} finally {
await runtime.close();
}
});
test("route scripts validate and execute async Node.js source", async () => {
process.env.CCR_ROUTE_SCRIPT_ENV_TEST = "available";
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
@@ -131,24 +308,8 @@ test("Node.js script rules participate in ordered routing and return dynamic dec
rewrites: [{ key: "request.header.x-script-route", operation: "set", value: "matched" }]
};
`);
const rule = {
enabled: true,
id: "dynamic-script",
name: "Dynamic script",
script,
type: "script"
};
const config = {
CUSTOM_ROUTER_PATH: "",
Providers: [{ models: ["alpha", "beta"], name: "Provider", type: "anthropic_messages" }],
Router: {
builtInRules: { "claude-code": { enabled: false }, codex: { enabled: false } },
fallback: { mode: "off", models: [], retryCount: 0 },
rules: [rule]
},
profile: { enabled: false, profiles: [] },
virtualModelProfiles: []
};
const rule = scriptRule("dynamic-script", script, { name: "Dynamic script" });
const config = routingConfig([rule]);
try {
const validationErrors = await runtime.prepare([rule]);
const plugin = new ClaudeCodeRouterPlugin(config, {
@@ -173,6 +334,178 @@ test("Node.js script rules participate in ordered routing and return dynamic dec
}
});
test("Node.js script rules fail open and continue to the first valid dynamic decision", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const rules = [
scriptRule("null-result", routeScript("return null;")),
scriptRule("runtime-error", routeScript("throw new Error('policy unavailable');")),
scriptRule("invalid-result", routeScript("return 'Provider/beta';")),
scriptRule("unconfigured-model", routeScript('return { model: "Provider/missing" };')),
scriptRule("invalid-fallback", routeScript(`
return { fallback: { mode: "model-chain", models: ["Provider/missing"], retryCount: 0 } };
`)),
scriptRule("protected-rewrite", routeScript(`
return {
rewrites: [{ key: "request.header.authorization", operation: "set", value: "replaced" }]
};
`)),
scriptRule("explicit-no-match", routeScript(`
return { match: false, model: "Provider/alpha" };
`)),
scriptRule("valid-result", routeScript(`
return {
model: "Provider/beta",
rewrites: [
{ key: "request.body.temperature", operation: "set", value: 0.25 },
{ key: "request.body.tags", operation: "array-append", value: "script" },
{ key: "request.header.x-route-policy", operation: "set", value: "dynamic" }
],
fallback: {
mode: "model-chain",
models: ["Provider/fallback"],
retryCount: 0
}
};
`))
];
const config = routingConfig(rules);
try {
const validationErrors = await runtime.prepare(rules);
assert.deepEqual([...validationErrors], []);
const plugin = new ClaudeCodeRouterPlugin(config, {
scriptRuntime: runtime,
scriptValidationErrors: validationErrors
});
const headers = {};
const result = await plugin.routeRequest({
body: { messages: [], model: "Provider/alpha", tags: ["base"] },
headers,
method: "POST",
url: "/v1/messages"
});
assert.equal(result.body.model, "Provider/beta");
assert.equal(result.body.temperature, 0.25);
assert.deepEqual(result.body.tags, ["base", "script"]);
assert.equal(headers["x-route-policy"], "dynamic");
assert.equal(result.decision.reason, "script:valid-result");
assert.deepEqual(result.decision.fallback, {
mode: "model-chain",
models: ["Provider/fallback"],
retryCount: 0
});
assert.deepEqual(
result.decision.diagnostics.map((diagnostic) => diagnostic.code),
[
"script-runtime-error",
"script-invalid-result",
"script-model-not-configured",
"script-invalid-result",
"script-invalid-result"
]
);
} finally {
await runtime.close();
}
});
test("startup validation disables an invalid script without blocking later rules", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const invalidRule = scriptRule("invalid-syntax", routeScript("return {;"));
const validRule = scriptRule("valid-after-invalid", routeScript('return { model: "Provider/beta" };'));
const rules = [invalidRule, validRule];
const config = routingConfig(rules);
try {
const validationErrors = await runtime.prepare(rules);
assert.equal(validationErrors.size, 1);
assert.match(validationErrors.get("invalid-syntax"), /syntax|unexpected/i);
const plugin = new ClaudeCodeRouterPlugin(config, {
scriptRuntime: runtime,
scriptValidationErrors: validationErrors
});
const result = await plugin.routeRequest({
body: { messages: [], model: "Provider/alpha" },
headers: {},
method: "POST",
url: "/v1/messages"
});
assert.equal(result.body.model, "Provider/beta");
assert.equal(result.decision.reason, "script:valid-after-invalid");
assert.ok(result.decision.diagnostics.some((diagnostic) =>
diagnostic.code === "script-source-invalid" && diagnostic.ruleId === "invalid-syntax"
));
} finally {
await runtime.close();
}
});
test("a true script result applies the rule's static target, rewrites, and fallback", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const rule = scriptRule("static-script-decision", routeScript("return true;"), {
fallback: { mode: "retry", models: [], retryCount: 2 },
rewrites: [
{ key: "request.body.model", operation: "set", value: "Provider/beta" },
{ key: "request.body.metadata.route", operation: "set", value: "static" }
]
});
const config = routingConfig([rule]);
try {
const validationErrors = await runtime.prepare([rule]);
const plugin = new ClaudeCodeRouterPlugin(config, {
scriptRuntime: runtime,
scriptValidationErrors: validationErrors
});
const result = await plugin.routeRequest({
body: { messages: [], model: "Provider/alpha" },
headers: {},
method: "POST",
url: "/v1/messages"
});
assert.equal(result.body.model, "Provider/beta");
assert.deepEqual(result.body.metadata, { route: "static" });
assert.deepEqual(result.decision.fallback, { mode: "retry", models: [], retryCount: 2 });
assert.equal(result.decision.reason, "script:static-script-decision");
} finally {
await runtime.close();
}
});
test("the route-script test service validates, executes, and previews a custom decision", async () => {
const script = routeScript(`
return {
model: input.headers["x-use-beta"] === "yes" ? "Provider/beta" : "Provider/alpha",
rewrites: [{ key: "request.body.tested", operation: "set", value: true }]
};
`);
try {
const result = await gatewayService.testRouteScript(routingConfig(), {
request: {
body: { messages: [], model: "Provider/alpha" },
headers: { "x-use-beta": "yes" },
method: "POST",
sessionId: "test-session",
tokenCount: 42,
url: "/v1/messages"
},
script
});
assert.equal(result.ok, true);
assert.equal(result.matched, true);
assert.deepEqual(result.diagnostics, []);
assert.deepEqual(result.output, {
model: "Provider/beta",
rewrites: [{ key: "request.body.tested", operation: "set", value: true }]
});
assert.ok(result.durationMs >= 0);
} finally {
await gatewayService.stop();
}
});
test("dynamic script model deletion overrides an earlier static model rewrite", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const script = routeScript(`
@@ -180,25 +513,11 @@ test("dynamic script model deletion overrides an earlier static model rewrite",
rewrites: [{ key: "request.body.model", operation: "delete" }]
};
`);
const rule = {
enabled: true,
id: "dynamic-model-delete",
const rule = scriptRule("dynamic-model-delete", script, {
name: "Dynamic model delete",
rewrites: [{ key: "request.body.model", operation: "set", value: "Provider/alpha" }],
script,
type: "script"
};
const config = {
CUSTOM_ROUTER_PATH: "",
Providers: [{ models: ["alpha", "beta"], name: "Provider", type: "anthropic_messages" }],
Router: {
builtInRules: { "claude-code": { enabled: false }, codex: { enabled: false } },
fallback: { mode: "off", models: [], retryCount: 0 },
rules: [rule]
},
profile: { enabled: false, profiles: [] },
virtualModelProfiles: []
};
});
const config = routingConfig([rule]);
try {
const validationErrors = await runtime.prepare([rule]);
const plugin = new ClaudeCodeRouterPlugin(config, {
@@ -219,32 +538,72 @@ test("dynamic script model deletion overrides an earlier static model rewrite",
}
});
test("route scripts can read and write arbitrary filesystem paths", async () => {
test("route scripts expose the complete documented filesystem API", async () => {
const directory = mkdtempSync(path.join(os.tmpdir(), "ccr-route-script-"));
const inputFile = path.join(directory, "input.txt");
const inputFile = path.join(directory, "input.json");
const outputFile = path.join(directory, "output.txt");
writeFileSync(inputFile, "allowed", "utf8");
const outputJsonFile = path.join(directory, "output.json");
const missingFile = path.join(directory, "missing.txt");
writeFileSync(inputFile, JSON.stringify({ route: "allowed" }), "utf8");
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const script = routeScript(`
const value = await api.fs.readText(input.body.inputFile);
await api.fs.writeText(input.body.outputFile, value.toUpperCase());
return value;
const text = await api.fs.readText(input.body.inputFile);
const json = await api.fs.readJson(input.body.inputFile);
await api.fs.writeText(input.body.outputFile, json.route.toUpperCase());
await api.fs.writeJson(input.body.outputJsonFile, { copied: json.route });
return {
directory: await api.fs.stat(input.body.directory),
entries: await api.fs.list(input.body.directory),
file: await api.fs.stat(input.body.inputFile),
inputExists: await api.fs.exists(input.body.inputFile),
json,
missingExists: await api.fs.exists(input.body.missingFile),
text
};
`);
try {
const result = await runtime.execute("filesystem", script, scriptInput(script, { inputFile, outputFile }));
const result = await runtime.execute("filesystem", script, scriptInput(script, {
directory,
inputFile,
missingFile,
outputFile,
outputJsonFile
}));
assert.equal(result.status, "ok");
assert.equal(result.value, "allowed");
assert.equal(result.value.inputExists, true);
assert.equal(result.value.missingExists, false);
assert.equal(result.value.text, JSON.stringify({ route: "allowed" }));
assert.deepEqual(result.value.json, { route: "allowed" });
assert.equal(result.value.directory.isDirectory, true);
assert.equal(result.value.file.isFile, true);
assert.ok(result.value.file.size > 0);
assert.match(result.value.file.modifiedAt, /^\d{4}-\d{2}-\d{2}T/);
assert.deepEqual(
result.value.entries.map((entry) => entry.name).sort(),
["input.json", "output.json", "output.txt"]
);
assert.equal(readFileSync(outputFile, "utf8"), "ALLOWED");
assert.equal(readFileSync(outputJsonFile, "utf8"), '{\n "copied": "allowed"\n}\n');
} finally {
await runtime.close();
rmSync(directory, { force: true, recursive: true });
}
});
test("route scripts can access arbitrary HTTP endpoints", async () => {
const server = createServer((_request, response) => {
test("route scripts expose HTTP request options and response metadata", async () => {
let received;
const server = createServer(async (request, response) => {
const chunks = [];
for await (const chunk of request) chunks.push(chunk);
received = {
body: Buffer.concat(chunks).toString("utf8"),
header: request.headers["x-policy-request"],
method: request.method,
url: request.url
};
response.statusCode = 201;
response.setHeader("content-type", "application/json");
response.setHeader("x-policy-response", "available");
response.end(JSON.stringify({ route: "beta" }));
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
@@ -253,13 +612,34 @@ test("route scripts can access arbitrary HTTP endpoints", async () => {
const endpoint = `http://127.0.0.1:${address.port}/route`;
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const script = routeScript(`
const response = await api.fetch(input.body.endpoint);
return JSON.parse(response.body);
const response = await api.fetch(input.body.endpoint, {
method: "POST",
headers: { "content-type": "application/json", "x-policy-request": "check" },
body: JSON.stringify({ tenant: "acme" })
});
return { ...response, body: JSON.parse(response.body) };
`);
try {
const result = await runtime.execute("network", script, scriptInput(script, { endpoint }));
assert.equal(result.status, "ok");
assert.deepEqual(result.value, { route: "beta" });
assert.equal(result.value.ok, true);
assert.equal(result.value.status, 201);
assert.equal(result.value.statusText, "Created");
assert.equal(result.value.redirected, false);
assert.equal(result.value.url, endpoint);
assert.equal(result.value.headers["x-policy-response"], "available");
assert.deepEqual(result.value.body, { route: "beta" });
assert.deepEqual(received, {
body: JSON.stringify({ tenant: "acme" }),
header: "check",
method: "POST",
url: "/route"
});
const invalidUrl = routeScript('await api.fetch("file:///tmp/policy.json");');
const invalidResult = await runtime.execute("network-invalid-url", invalidUrl, scriptInput(invalidUrl));
assert.equal(invalidResult.status, "error");
assert.match(invalidResult.error, /only http\(s\) urls/i);
} finally {
await runtime.close();
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
@@ -282,6 +662,46 @@ test("route scripts report syntax failures and stop synchronous infinite loops",
}
});
test("route scripts stop unresolved async work and keep the worker reusable", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const stalled = routeScript("await new Promise(() => {});", { timeoutMs: 30 });
const healthy = routeScript("return 'recovered';");
try {
const timeout = await runtime.execute("async-timeout", stalled, scriptInput(stalled));
assert.equal(timeout.status, "timeout");
assert.match(timeout.error, /timed out/i);
const recovered = await runtime.execute("after-async-timeout", healthy, scriptInput(healthy));
assert.equal(recovered.status, "ok");
assert.equal(recovered.value, "recovered");
} finally {
await runtime.close();
}
});
test("route scripts reject non-JSON and oversized results without poisoning later executions", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 1, workerFile });
const cases = [
["bigint", routeScript("return 1n;"), /json serializable|bigint/i],
["circular", routeScript("const value = {}; value.self = value; return value;"), /circular/i],
["oversized", routeScript('return { value: "x".repeat(70 * 1024) };'), /exceeds 65536 bytes/i]
];
try {
for (const [ruleId, script, expectedMessage] of cases) {
const result = await runtime.execute(ruleId, script, scriptInput(script));
assert.equal(result.status, "error");
assert.match(result.error, expectedMessage);
}
const healthy = routeScript("return { match: true };");
const recovered = await runtime.execute("after-invalid-results", healthy, scriptInput(healthy));
assert.equal(recovered.status, "ok");
assert.deepEqual(recovered.value, { match: true });
} finally {
await runtime.close();
}
});
test("route script workers distribute concurrent executions across available slots", async () => {
const runtime = new RouteScriptRuntime({ workerCount: 2, workerFile });
const script = routeScript(`