mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
1211 lines
37 KiB
JavaScript
1211 lines
37 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import { ClaudeCodeRouterPlugin } from "../../packages/core/src/gateway/claude-code-router-plugin.ts";
|
|
import {
|
|
fallbackRetryDelayAfterNetworkErrorForTest,
|
|
fallbackRetryDelayAfterStatusForTest,
|
|
prepareGatewayUpstreamAttemptForTest
|
|
} from "../../packages/core/src/gateway/service.ts";
|
|
|
|
function createRouterPlugin(options = {}) {
|
|
const agent = options.agent ?? "claude-code";
|
|
return new ClaudeCodeRouterPlugin({
|
|
CUSTOM_ROUTER_PATH: "",
|
|
Providers: options.providers ?? [
|
|
{
|
|
modelDescriptions: options.modelDescriptions,
|
|
modelDisplayNames: options.modelDisplayNames,
|
|
models: ["claude-sonnet", "gpt-5-codex"],
|
|
name: "Provider",
|
|
type: "anthropic_messages"
|
|
}
|
|
],
|
|
Router: {
|
|
builtInRules: {
|
|
"claude-code": { enabled: options.claudeCodeRuleEnabled ?? true },
|
|
codex: { enabled: options.codexRuleEnabled ?? true }
|
|
},
|
|
fallback: { mode: "off", models: [], retryCount: 1 },
|
|
rules: options.routerRules ?? []
|
|
},
|
|
profile: {
|
|
enabled: options.profileRuntimeEnabled ?? true,
|
|
profiles: [
|
|
{
|
|
agent,
|
|
enabled: options.profileEnabled ?? true,
|
|
id: `${agent}-profile`,
|
|
model: options.profileModel ?? "",
|
|
name: agent,
|
|
scope: "global"
|
|
}
|
|
]
|
|
},
|
|
toolHub: options.toolHub,
|
|
virtualModelProfiles: options.virtualModelProfiles ?? []
|
|
});
|
|
}
|
|
|
|
test("fallback retry delay backs off retryable HTTP statuses", () => {
|
|
assert.equal(fallbackRetryDelayAfterStatusForTest({ statusCode: 503 }), 1000);
|
|
assert.equal(fallbackRetryDelayAfterStatusForTest({ failedAttemptIndex: 1, statusCode: 408 }), 2000);
|
|
assert.equal(fallbackRetryDelayAfterStatusForTest({ retryAfter: "3", statusCode: 429 }), 3000);
|
|
assert.equal(fallbackRetryDelayAfterStatusForTest({ retryAfter: "0", statusCode: 429 }), 1000);
|
|
});
|
|
|
|
test("fallback retry delay backs off network errors", () => {
|
|
assert.equal(fallbackRetryDelayAfterNetworkErrorForTest(), 1000);
|
|
assert.equal(fallbackRetryDelayAfterNetworkErrorForTest(2), 4000);
|
|
});
|
|
|
|
function createIssue1480UserConfig() {
|
|
return {
|
|
APIKEY: "gateway-key",
|
|
CUSTOM_ROUTER_PATH: "",
|
|
Providers: [
|
|
{
|
|
api_base_url: "https://nebulacoder.example/v1/chat/completions",
|
|
api_key: "nebulacoder-key",
|
|
modelDescriptions: {
|
|
"nebulacoder-v8.0": "Code reading model.",
|
|
"nebulacoder-cot-v8.0": "Code reading reasoning model."
|
|
},
|
|
models: ["nebulacoder-v8.0", "nebulacoder-cot-v8.0"],
|
|
name: "NebulaCoder"
|
|
},
|
|
{
|
|
api_base_url: "https://coclaw.example/v1/chat/completions",
|
|
api_key: "coclaw-key",
|
|
modelDescriptions: {
|
|
"Qwen3-235B-A22B": "Background task model."
|
|
},
|
|
models: ["Qwen3-235B-A22B"],
|
|
name: "CoClaw"
|
|
},
|
|
{
|
|
api_base_url: "https://opencode.ai/zen/go/v1/chat/completions",
|
|
api_key: "opencode-key",
|
|
capabilities: [
|
|
{ baseUrl: "https://opencode.ai/zen/go/v1/chat/completions", type: "openai_chat_completions" }
|
|
],
|
|
modelDescriptions: {
|
|
"glm-5.1": "Previous generation flagship model.",
|
|
"glm-5.2": "Flagship model.",
|
|
"deepseek-v4-flash": "Default route model.",
|
|
"deepseek-v4-pro": "Thinking route model.",
|
|
"kimi-k2.6": "Claude Code profile model.",
|
|
"kimi-k2.7": "Fast model.",
|
|
"mimo-v2.5": "Lightweight model.",
|
|
"mimo-v2.5-pro": "Enhanced lightweight model."
|
|
},
|
|
models: [
|
|
"glm-5.2",
|
|
"glm-5.1",
|
|
"kimi-k2.7",
|
|
"kimi-k2.6",
|
|
"deepseek-v4-pro",
|
|
"deepseek-v4-flash",
|
|
"mimo-v2.5",
|
|
"mimo-v2.5-pro"
|
|
],
|
|
name: "OpenCode"
|
|
},
|
|
{
|
|
api_base_url: "https://reasoning.example/v1/chat/completions",
|
|
api_key: "reasoning-key",
|
|
modelDescriptions: {
|
|
"sap-glm-2-2": "Reasoning model."
|
|
},
|
|
models: ["sap-glm-2-2"],
|
|
name: "Reasoning"
|
|
}
|
|
],
|
|
Router: {
|
|
fallback: { mode: "retry", models: [], retryCount: 1 },
|
|
rules: [
|
|
{
|
|
condition: {
|
|
left: "request.url",
|
|
operator: "contains",
|
|
right: "/v1"
|
|
},
|
|
enabled: true,
|
|
id: "rule-default",
|
|
name: "Default route",
|
|
rewrites: [
|
|
{ key: "request.header.x-target-provider", operation: "set", value: "OpenCode" },
|
|
{ key: "request.body.model", operation: "set", value: "deepseek-v4-flash" }
|
|
],
|
|
type: "condition"
|
|
},
|
|
{
|
|
condition: {
|
|
left: "request.header.anthropic-background",
|
|
operator: "==",
|
|
right: "true"
|
|
},
|
|
enabled: true,
|
|
id: "rule-background",
|
|
name: "Background tasks",
|
|
rewrites: [
|
|
{ key: "request.header.x-target-provider", operation: "set", value: "CoClaw" },
|
|
{ key: "request.body.model", operation: "set", value: "Qwen3-235B-A22B" }
|
|
],
|
|
type: "condition"
|
|
},
|
|
{
|
|
condition: {
|
|
left: "request.header.anthropic-thinking",
|
|
operator: "==",
|
|
right: "true"
|
|
},
|
|
enabled: true,
|
|
id: "rule-thinking",
|
|
name: "Thinking/reasoning tasks",
|
|
rewrites: [
|
|
{ key: "request.header.x-target-provider", operation: "set", value: "OpenCode" },
|
|
{ key: "request.body.model", operation: "set", value: "deepseek-v4-pro" }
|
|
],
|
|
type: "condition"
|
|
}
|
|
]
|
|
},
|
|
gateway: {
|
|
coreHost: "0.0.0.0",
|
|
corePort: 3457,
|
|
enabled: true,
|
|
host: "0.0.0.0",
|
|
port: 3456
|
|
},
|
|
profile: {
|
|
enabled: true,
|
|
profiles: [
|
|
{
|
|
agent: "claude-code",
|
|
enabled: true,
|
|
id: "claude-code-main",
|
|
model: "kimi-k2.6",
|
|
name: "Claude Code",
|
|
scope: "global"
|
|
}
|
|
]
|
|
},
|
|
providerPlugins: [
|
|
{
|
|
enabled: true,
|
|
key: "opencode",
|
|
opencode: { enabled: true },
|
|
provider: "openai"
|
|
}
|
|
],
|
|
virtualModelProfiles: [
|
|
{
|
|
description: "Automatically search the web using Tavily",
|
|
displayName: "Fusion Tavily",
|
|
enabled: true,
|
|
execution: {
|
|
clientToolsPolicy: "allow",
|
|
matchWebSearch: true,
|
|
maxToolCalls: 10,
|
|
maxTurns: 20,
|
|
mode: "tool_loop",
|
|
streamMode: "optimistic"
|
|
},
|
|
id: "fusion-tavily-web-search",
|
|
key: "fusion-tavily",
|
|
match: {
|
|
exactAliases: [],
|
|
prefixes: [],
|
|
suffixes: []
|
|
},
|
|
materialization: {
|
|
enabled: true,
|
|
includeInGatewayModels: true
|
|
},
|
|
metadata: {
|
|
fusionVision: {
|
|
apiKey: "opencode-key",
|
|
baseUrl: "https://opencode.ai/zen/go/v1",
|
|
model: "kimi-k2.6",
|
|
toolName: "vision_understand"
|
|
},
|
|
fusionWebSearch: {
|
|
env: {
|
|
TAVILY_API_KEY: "tavily-key"
|
|
},
|
|
provider: "tavily",
|
|
resultCount: 5,
|
|
toolName: "web_search"
|
|
}
|
|
},
|
|
tools: []
|
|
}
|
|
]
|
|
};
|
|
}
|
|
|
|
function createIssue1480RouterConfig() {
|
|
const config = createIssue1480UserConfig();
|
|
return {
|
|
...config,
|
|
Providers: config.Providers.map((provider) => ({
|
|
...provider,
|
|
credentials: [{ apiKey: provider.api_key, id: `${provider.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}-main` }]
|
|
}))
|
|
};
|
|
}
|
|
|
|
test("built-in Claude Code route matches user-agent case-insensitively", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default"
|
|
},
|
|
headers: {
|
|
"user-agent": "claude-code/1.0"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-sonnet");
|
|
assert.equal(result.decision.model, "Provider/claude-sonnet");
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|
|
|
|
test("built-in Claude Code route does not inject Claude Code native tool search", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
toolHub: {
|
|
enabled: true,
|
|
llm: {
|
|
apiKey: "resolver-key",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
model: "gpt-4.1-mini"
|
|
},
|
|
maxTools: 5,
|
|
requestTimeoutMs: 60000
|
|
}
|
|
});
|
|
const headers = {
|
|
"anthropic-beta": "oauth-2025-04-20",
|
|
"user-agent": "claude-code/1.0"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{ name: "Bash", input_schema: { type: "object" } },
|
|
{ name: "Read", input_schema: { type: "object" } },
|
|
{ name: "WebFetch", input_schema: { type: "object" } },
|
|
{ name: "LookupCustomer", input_schema: { type: "object" } }
|
|
]
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.tools.some((tool) => String(tool.name || "").startsWith("tool_search_")), false);
|
|
assert.deepEqual(result.body.tools.map((tool) => tool.name), ["Bash", "Read", "WebFetch", "LookupCustomer"]);
|
|
assert.equal(result.body.tools.find((tool) => tool.name === "Bash").defer_loading, undefined);
|
|
assert.equal(result.body.tools.find((tool) => tool.name === "Read").defer_loading, undefined);
|
|
assert.equal(result.body.tools.find((tool) => tool.name === "WebFetch").defer_loading, undefined);
|
|
assert.equal(result.body.tools.find((tool) => tool.name === "LookupCustomer").defer_loading, undefined);
|
|
assert.equal(result.body.system, undefined);
|
|
assert.equal(headers["anthropic-beta"], "oauth-2025-04-20");
|
|
});
|
|
|
|
test("built-in Claude Code route injects ToolHub resolver instructions when ToolHub MCP is available", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
toolHub: {
|
|
enabled: true,
|
|
llm: {
|
|
apiKey: "resolver-key",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
model: "gpt-4.1-mini"
|
|
},
|
|
maxTools: 5,
|
|
requestTimeoutMs: 60000
|
|
}
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{ name: "tool_hub.resolve", input_schema: { type: "object" } },
|
|
{ name: "mcp__ccr-toolhub__tool_hub_resolve", input_schema: { type: "object" } },
|
|
{ name: "mcp__ccr-toolhub__tool_hub_invoke", input_schema: { type: "object" } }
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "claude-code/1.0"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.match(result.body.system.at(-1).text, /CCR ToolHub tool resolution is enabled/);
|
|
assert.match(result.body.system.at(-1).text, /ToolHub search\/resolution tool is mcp__ccr-toolhub__tool_hub_resolve/);
|
|
assert.match(result.body.system.at(-1).text, /call this actual tool, do not merely mention its name in text/);
|
|
assert.match(result.body.system.at(-1).text, /MUST call the ToolHub search\/resolution tool mcp__ccr-toolhub__tool_hub_resolve before answering/);
|
|
assert.match(result.body.system.at(-1).text, /external services.*business APIs.*orders.*coupons.*stores.*accounts/);
|
|
assert.match(result.body.system.at(-1).text, /Only skip the ToolHub search\/resolution tool when the request is clearly local/);
|
|
assert.match(result.body.system.at(-1).text, /call the ToolHub invocation tool mcp__ccr-toolhub__tool_hub_invoke/);
|
|
assert.match(result.body.system.at(-1).text, /executionPlanJs.*Promise\.all/);
|
|
});
|
|
|
|
test("built-in Claude Code route does not inject ToolHub instructions without the resolve tool", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
toolHub: {
|
|
enabled: true,
|
|
llm: {
|
|
apiKey: "resolver-key",
|
|
baseUrl: "https://api.openai.com/v1",
|
|
model: "gpt-4.1-mini"
|
|
},
|
|
maxTools: 5,
|
|
requestTimeoutMs: 60000
|
|
}
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{ name: "Bash", input_schema: { type: "object" } },
|
|
{ name: "Read", input_schema: { type: "object" } }
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "claude-code/1.0"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.system, undefined);
|
|
});
|
|
|
|
test("router rules override the built-in Claude Code profile route", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
routerRules: [
|
|
{
|
|
condition: {
|
|
left: "request.url",
|
|
operator: "contains",
|
|
right: "/v1"
|
|
},
|
|
enabled: true,
|
|
id: "default",
|
|
name: "Default",
|
|
rewrites: [
|
|
{ key: "request.header.x-target-provider", operation: "set", value: "OverrideProvider" },
|
|
{ key: "request.body.model", operation: "set", value: "Provider/gpt-5-codex" }
|
|
],
|
|
type: "condition"
|
|
}
|
|
]
|
|
});
|
|
const headers = {
|
|
"user-agent": "claude-code/1.0"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default"
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], "OverrideProvider");
|
|
assert.equal(result.body.model, "Provider/gpt-5-codex");
|
|
assert.equal(result.decision.model, "Provider/gpt-5-codex");
|
|
assert.equal(result.decision.reason, "rule:default");
|
|
});
|
|
|
|
test("router rules can add headers after the built-in Claude Code profile route", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
routerRules: [
|
|
{
|
|
condition: {
|
|
left: "request.url",
|
|
operator: "contains",
|
|
right: "/v1"
|
|
},
|
|
enabled: true,
|
|
id: "target-provider",
|
|
name: "Target provider",
|
|
rewrites: [
|
|
{ key: "request.header.x-target-provider", operation: "set", value: "Provider" }
|
|
],
|
|
type: "condition"
|
|
}
|
|
]
|
|
});
|
|
const headers = {
|
|
"user-agent": "Claude Code"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default"
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], "Provider");
|
|
assert.equal(result.body.model, "Provider/claude-sonnet");
|
|
assert.equal(result.decision.model, "Provider/claude-sonnet");
|
|
assert.equal(result.decision.reason, "rule:target-provider");
|
|
});
|
|
|
|
test("issue 1480 raw user config no longer reproduces the Claude Code profile routing failure", async () => {
|
|
const config = createIssue1480UserConfig();
|
|
const plugin = new ClaudeCodeRouterPlugin(config);
|
|
const headers = {
|
|
"user-agent": "claude-cli/2.1.187 (external, cli)"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-sonnet-4-6",
|
|
tools: []
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages?beta=true"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], "OpenCode");
|
|
assert.equal(result.body.model, "deepseek-v4-flash");
|
|
assert.equal(result.decision.model, "deepseek-v4-flash");
|
|
assert.equal(result.decision.reason, "rule:rule-default");
|
|
});
|
|
|
|
test("issue 1480 raw user config reproduces the old failure precondition when Router rules are skipped", async () => {
|
|
const config = createIssue1480UserConfig();
|
|
const plugin = new ClaudeCodeRouterPlugin({
|
|
...config,
|
|
Router: {
|
|
...config.Router,
|
|
rules: []
|
|
}
|
|
});
|
|
const headers = {
|
|
"user-agent": "claude-cli/2.1.187 (external, cli)"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-sonnet-4-6",
|
|
tools: []
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages?beta=true"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], undefined);
|
|
assert.equal(result.body.model, "kimi-k2.6");
|
|
assert.equal(result.decision.model, "kimi-k2.6");
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|
|
|
|
test("issue 1480 raw user config ignores an unreplaced Provider/model subagent placeholder", async () => {
|
|
const config = createIssue1480UserConfig();
|
|
const plugin = new ClaudeCodeRouterPlugin(config);
|
|
const headers = {
|
|
"user-agent": "claude-cli/2.1.187 (external, cli)"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-sonnet-4-6",
|
|
system: "Use <CCR-SUBAGENT-MODEL>provider/model</CCR-SUBAGENT-MODEL> for this subagent.",
|
|
tools: []
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages?beta=true"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], "OpenCode");
|
|
assert.equal(result.body.model, "deepseek-v4-flash");
|
|
assert.equal(result.body.system, "Use for this subagent.");
|
|
assert.equal(result.decision.model, "deepseek-v4-flash");
|
|
assert.equal(result.decision.reason, "rule:rule-default");
|
|
});
|
|
|
|
test("issue 1480 config routes Claude Code profile traffic through the user default OpenCode rule", async () => {
|
|
const config = createIssue1480RouterConfig();
|
|
const plugin = new ClaudeCodeRouterPlugin(config);
|
|
const headers = {
|
|
"user-agent": "claude-cli/2.1.187 (external, cli)"
|
|
};
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-sonnet-4-6",
|
|
tools: []
|
|
},
|
|
headers,
|
|
method: "POST",
|
|
url: "/v1/messages?beta=true"
|
|
});
|
|
|
|
assert.equal(headers["x-target-provider"], "OpenCode");
|
|
assert.equal(result.body.model, "deepseek-v4-flash");
|
|
assert.equal(result.decision.model, "deepseek-v4-flash");
|
|
assert.equal(result.decision.reason, "rule:rule-default");
|
|
|
|
const upstreamAttempt = prepareGatewayUpstreamAttemptForTest({
|
|
body: result.body,
|
|
config,
|
|
fallback: result.decision.fallback,
|
|
headers,
|
|
method: "POST",
|
|
path: "/v1/messages",
|
|
routedModel: result.decision.model
|
|
});
|
|
|
|
assert.equal(upstreamAttempt.logicalProvider, "OpenCode");
|
|
assert.equal(upstreamAttempt.credentialProtocol, "openai_chat_completions");
|
|
assert.equal(upstreamAttempt.body.model, "deepseek-v4-flash");
|
|
assert.match(upstreamAttempt.headers["x-target-providers"], /^provider-opencode-[a-f0-9]{10}::openai_chat_completions::cred:opencode-main$/);
|
|
assert.doesNotMatch(upstreamAttempt.headers["x-target-providers"], /nebulacoder/i);
|
|
});
|
|
|
|
test("OpenAI chat completion streaming attempts request upstream usage chunks", () => {
|
|
const config = createIssue1480RouterConfig();
|
|
const headers = {
|
|
"x-target-provider": "OpenCode"
|
|
};
|
|
const upstreamAttempt = prepareGatewayUpstreamAttemptForTest({
|
|
body: {
|
|
messages: [],
|
|
model: "deepseek-v4-flash",
|
|
stream: true,
|
|
stream_options: {
|
|
extra_flag: "keep"
|
|
}
|
|
},
|
|
config,
|
|
headers,
|
|
method: "POST",
|
|
path: "/v1/messages",
|
|
routedModel: "deepseek-v4-flash"
|
|
});
|
|
|
|
assert.equal(upstreamAttempt.credentialProtocol, "openai_chat_completions");
|
|
assert.equal(upstreamAttempt.body.stream_options.include_usage, true);
|
|
assert.equal(upstreamAttempt.body.stream_options.extra_flag, "keep");
|
|
});
|
|
|
|
test("explicit OpenAI chat provider selectors request upstream usage chunks without credential routing", () => {
|
|
const config = {
|
|
CUSTOM_ROUTER_PATH: "",
|
|
Providers: [
|
|
{
|
|
api_base_url: "https://api.kimi.example/v1/chat/completions",
|
|
capabilities: [
|
|
{ baseUrl: "https://api.kimi.example/v1/chat/completions", type: "openai_chat_completions" }
|
|
],
|
|
models: ["kimi-for-coding"],
|
|
name: "Kimi Code - Coding Plan"
|
|
}
|
|
],
|
|
Router: {
|
|
fallback: { mode: "off", models: [], retryCount: 0 },
|
|
rules: []
|
|
},
|
|
virtualModelProfiles: []
|
|
};
|
|
|
|
const upstreamAttempt = prepareGatewayUpstreamAttemptForTest({
|
|
body: {
|
|
messages: [],
|
|
model: "Kimi Code - Coding Plan/kimi-for-coding",
|
|
stream: true
|
|
},
|
|
config,
|
|
headers: {},
|
|
method: "POST",
|
|
path: "/v1/messages",
|
|
routedModel: "Kimi Code - Coding Plan/kimi-for-coding"
|
|
});
|
|
|
|
assert.equal(upstreamAttempt.credentialProtocol, undefined);
|
|
assert.equal(upstreamAttempt.logicalProvider, undefined);
|
|
assert.equal(upstreamAttempt.body.stream_options.include_usage, true);
|
|
});
|
|
|
|
test("built-in Claude Code route preserves explicit virtual gateway models", async () => {
|
|
const plugin = createRouterPlugin({
|
|
profileModel: "Provider/claude-sonnet",
|
|
virtualModelProfiles: [
|
|
{
|
|
displayName: "Kimisearch",
|
|
enabled: true,
|
|
id: "fusion-search",
|
|
key: "kimisearch",
|
|
match: { exactAliases: ["kimisearch"], prefixes: [], suffixes: [] },
|
|
materialization: { enabled: true, includeInGatewayModels: true }
|
|
}
|
|
]
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "Fusion/kimisearch"
|
|
},
|
|
headers: {
|
|
"user-agent": "claude-code/1.0"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Fusion/kimisearch");
|
|
assert.equal(result.decision.model, "Fusion/kimisearch");
|
|
assert.equal(result.decision.reason, "inline-model");
|
|
});
|
|
|
|
test("built-in Codex route stays inactive when profile model is unset", async () => {
|
|
const plugin = createRouterPlugin({
|
|
agent: "codex"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "gpt-5"
|
|
},
|
|
headers: {
|
|
"user-agent": "openai-codex test"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "gpt-5");
|
|
assert.equal(result.decision.reason, "default");
|
|
});
|
|
|
|
test("built-in agent route stays off after the user disables it", async () => {
|
|
const plugin = createRouterPlugin({
|
|
claudeCodeRuleEnabled: false,
|
|
profileModel: "Provider/claude-sonnet"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default"
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "claude-default");
|
|
assert.equal(result.decision.reason, "default");
|
|
});
|
|
|
|
test("built-in Claude Code route injects subagent model instructions into Agent and Task tools", async () => {
|
|
const plugin = createRouterPlugin({
|
|
modelDescriptions: {
|
|
"claude-sonnet": "Balanced coding model for everyday implementation.",
|
|
"gpt-5-codex": "Use for long refactors and repository-scale reasoning."
|
|
},
|
|
modelDisplayNames: {
|
|
"claude-sonnet": "Claude Sonnet"
|
|
},
|
|
profileModel: "Provider/claude-sonnet"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
description: "Start a subagent.",
|
|
input_schema: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Agent"
|
|
},
|
|
{
|
|
description: "Start a task.",
|
|
input_schema: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Task"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
for (const tool of result.body.tools) {
|
|
assert.match(tool.description, /<CCR-SUBAGENT-MODEL>Provider\/model<\/CCR-SUBAGENT-MODEL>/);
|
|
assert.match(tool.description, /MUST start/);
|
|
assert.match(tool.description, /Provider\/claude-sonnet \(Claude Sonnet\): Balanced coding model/);
|
|
assert.match(tool.description, /Provider\/gpt-5-codex: Use for long refactors/);
|
|
assert.match(tool.input_schema.properties.prompt.description, /MUST start with <CCR-SUBAGENT-MODEL>Provider\/model<\/CCR-SUBAGENT-MODEL>/);
|
|
assert.match(tool.input_schema.properties.prompt.description, /Provider\/claude-sonnet \(Claude Sonnet\): Balanced coding model/);
|
|
assert.match(tool.input_schema.properties.prompt.description, /Provider\/gpt-5-codex: Use for long refactors/);
|
|
assert.doesNotMatch(tool.input_schema.properties.prompt.description, /optionally include/);
|
|
}
|
|
});
|
|
|
|
test("built-in Claude Code route injects subagent model descriptions in stable order", async () => {
|
|
const providersA = [
|
|
{
|
|
modelDescriptions: {
|
|
"z-model": "Zeta model.",
|
|
"a-model": "Alpha model."
|
|
},
|
|
models: ["z-model", "a-model"],
|
|
name: "Zeta",
|
|
type: "anthropic_messages"
|
|
},
|
|
{
|
|
modelDescriptions: {
|
|
"z-model": "Z model.",
|
|
"a-model": "A model."
|
|
},
|
|
models: ["z-model", "a-model"],
|
|
name: "Alpha",
|
|
type: "anthropic_messages"
|
|
}
|
|
];
|
|
const providersB = [
|
|
{
|
|
modelDescriptions: {
|
|
"a-model": "A model.",
|
|
"z-model": "Z model."
|
|
},
|
|
models: ["a-model", "z-model"],
|
|
name: "Alpha",
|
|
type: "anthropic_messages"
|
|
},
|
|
{
|
|
modelDescriptions: {
|
|
"a-model": "Alpha model.",
|
|
"z-model": "Zeta model."
|
|
},
|
|
models: ["a-model", "z-model"],
|
|
name: "Zeta",
|
|
type: "anthropic_messages"
|
|
}
|
|
];
|
|
const routeRequest = (plugin) => plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
description: "Start a subagent.",
|
|
input_schema: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Agent"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
const pluginA = createRouterPlugin({ profileModel: "Alpha/a-model", providers: providersA });
|
|
const pluginB = createRouterPlugin({ profileModel: "Alpha/a-model", providers: providersB });
|
|
const [resultA, resultB] = await Promise.all([routeRequest(pluginA), routeRequest(pluginB)]);
|
|
const description = resultA.body.tools[0].description;
|
|
|
|
assert.equal(description, resultB.body.tools[0].description);
|
|
assert.ok(description.indexOf("- Alpha/a-model: A model.") < description.indexOf("- Alpha/z-model: Z model."));
|
|
assert.ok(description.indexOf("- Alpha/z-model: Z model.") < description.indexOf("- Zeta/a-model: Alpha model."));
|
|
assert.ok(description.indexOf("- Zeta/a-model: Alpha model.") < description.indexOf("- Zeta/z-model: Zeta model."));
|
|
});
|
|
|
|
test("built-in Claude Code route injects workflow subagent model instructions into the Workflow tool", async () => {
|
|
const plugin = createRouterPlugin({
|
|
modelDescriptions: {
|
|
"claude-sonnet": "Balanced coding model for everyday implementation.",
|
|
"gpt-5-codex": "Use for long refactors and repository-scale reasoning."
|
|
},
|
|
profileModel: "Provider/claude-sonnet"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
description: "Run a workflow.",
|
|
input_schema: {
|
|
properties: {
|
|
script: { description: "Workflow script.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Workflow"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
const tool = result.body.tools[0];
|
|
assert.match(tool.description, /CCR workflow subagent routing is enabled/);
|
|
assert.match(tool.description, /Agent\/Task subagents/);
|
|
assert.match(tool.description, /each spawned agent prompt MUST start with <CCR-SUBAGENT-MODEL>Provider\/model<\/CCR-SUBAGENT-MODEL>/);
|
|
assert.match(tool.description, /Provider\/claude-sonnet: Balanced coding model/);
|
|
assert.match(tool.description, /Provider\/gpt-5-codex: Use for long refactors/);
|
|
assert.equal(tool.input_schema.properties.script.description, "Workflow script.");
|
|
});
|
|
|
|
test("built-in Claude Code route injects subagent model instructions into function-style Agent tools", async () => {
|
|
const plugin = createRouterPlugin({
|
|
modelDescriptions: {
|
|
"claude-sonnet": "Balanced coding model for everyday implementation."
|
|
},
|
|
profileModel: "Provider/claude-sonnet"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
function: {
|
|
description: "Start a subagent.",
|
|
name: "Agent",
|
|
parameters: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
}
|
|
},
|
|
type: "function"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
const tool = result.body.tools[0];
|
|
assert.match(tool.function.description, /<CCR-SUBAGENT-MODEL>Provider\/model<\/CCR-SUBAGENT-MODEL>/);
|
|
assert.match(tool.function.parameters.properties.prompt.description, /MUST start with <CCR-SUBAGENT-MODEL>Provider\/model<\/CCR-SUBAGENT-MODEL>/);
|
|
});
|
|
|
|
test("built-in Claude Code route skips subagent instruction injection when no model has a description", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
description: "Start a subagent.",
|
|
input_schema: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Agent"
|
|
},
|
|
{
|
|
description: "Run a workflow.",
|
|
input_schema: {
|
|
properties: {
|
|
script: { description: "Workflow script.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Workflow"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
const agentTool = result.body.tools[0];
|
|
const workflowTool = result.body.tools[1];
|
|
assert.equal(agentTool.description, "Start a subagent.");
|
|
assert.equal(agentTool.input_schema.properties.prompt.description, "Task prompt.");
|
|
assert.equal(workflowTool.description, "Run a workflow.");
|
|
assert.equal(workflowTool.input_schema.properties.script.description, "Workflow script.");
|
|
});
|
|
|
|
test("disabled built-in Claude Code route does not inject Agent tool instructions", async () => {
|
|
const plugin = createRouterPlugin({
|
|
claudeCodeRuleEnabled: false,
|
|
profileModel: "Provider/claude-sonnet"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
tools: [
|
|
{
|
|
description: "Start a subagent.",
|
|
input_schema: {
|
|
properties: {
|
|
prompt: { description: "Task prompt.", type: "string" }
|
|
},
|
|
type: "object"
|
|
},
|
|
name: "Task"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
const tool = result.body.tools[0];
|
|
assert.equal(tool.description, "Start a subagent.");
|
|
assert.equal(tool.input_schema.properties.prompt.description, "Task prompt.");
|
|
});
|
|
|
|
test("built-in Claude Code subagent route uses model tag from system", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
system: "Use <CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL> for this subagent."
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-opus");
|
|
assert.equal(result.body.system, "Use for this subagent.");
|
|
assert.equal(result.decision.model, "Provider/claude-opus");
|
|
assert.equal(result.decision.reason, "builtin:claude-code-subagent");
|
|
});
|
|
|
|
test("built-in Claude Code subagent route ignores the Provider/model placeholder", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
system: "Use <CCR-SUBAGENT-MODEL>Provider/model</CCR-SUBAGENT-MODEL> for this subagent."
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-sonnet");
|
|
assert.equal(result.body.system, "Use for this subagent.");
|
|
assert.equal(result.decision.model, "Provider/claude-sonnet");
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|
|
|
|
test("built-in Claude Code route removes the first billing system block before subagent tag extraction", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
system: [
|
|
{
|
|
text: "x-anthropic-billing-header: {\"cc_is_subagent\":true}",
|
|
type: "text"
|
|
},
|
|
{
|
|
text: "Use <CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL> for this subagent.",
|
|
type: "text"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-opus");
|
|
assert.deepEqual(result.body.system, [
|
|
{
|
|
text: "Use for this subagent.",
|
|
type: "text"
|
|
}
|
|
]);
|
|
assert.equal(result.decision.reason, "builtin:claude-code-subagent");
|
|
});
|
|
|
|
test("built-in Claude Code route keeps a string billing system prompt unchanged", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
system: "x-anthropic-billing-header: {\"cc_is_subagent\":true}"
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.system, "x-anthropic-billing-header: {\"cc_is_subagent\":true}");
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|
|
|
|
test("built-in Claude Code route removes only the first billing system array item", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "claude-default",
|
|
system: [
|
|
{
|
|
text: "x-anthropic-billing-header: {\"cc_is_subagent\":true}",
|
|
type: "text"
|
|
}
|
|
]
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal("system" in result.body, false);
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|
|
|
|
test("non-Claude-Code routes keep billing system prompts unchanged", async () => {
|
|
const plugin = createRouterPlugin({
|
|
agent: "codex",
|
|
profileModel: "Provider/gpt-5-codex"
|
|
});
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [],
|
|
model: "gpt-5",
|
|
system: "x-anthropic-billing-header: {\"cc_is_subagent\":true}"
|
|
},
|
|
headers: {
|
|
"user-agent": "openai-codex test"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.system, "x-anthropic-billing-header: {\"cc_is_subagent\":true}");
|
|
assert.equal(result.decision.reason, "builtin:codex");
|
|
});
|
|
|
|
test("built-in Claude Code subagent route scans only the first two messages for tags", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [
|
|
{ content: "first", role: "user" },
|
|
{
|
|
content: [
|
|
{
|
|
text: "second <CCR-SUBAGENT-MODEL>Provider/claude-haiku</CCR-SUBAGENT-MODEL>",
|
|
type: "text"
|
|
}
|
|
],
|
|
role: "user"
|
|
},
|
|
{ content: "third <CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL>", role: "user" }
|
|
],
|
|
model: "claude-default"
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-haiku");
|
|
assert.equal(result.body.messages[1].content[0].text, "second ");
|
|
assert.match(result.body.messages[2].content, /Provider\/claude-opus/);
|
|
assert.equal(result.decision.reason, "builtin:claude-code-subagent");
|
|
});
|
|
|
|
test("built-in Claude Code subagent route ignores tags outside the first two messages", async () => {
|
|
const plugin = createRouterPlugin({ profileModel: "Provider/claude-sonnet" });
|
|
const result = await plugin.routeRequest({
|
|
body: {
|
|
messages: [
|
|
{ content: "first", role: "user" },
|
|
{ content: "assistant response", role: "assistant" },
|
|
{ content: "third <CCR-SUBAGENT-MODEL>Provider/claude-opus</CCR-SUBAGENT-MODEL>", role: "user" }
|
|
],
|
|
model: "claude-default"
|
|
},
|
|
headers: {
|
|
"user-agent": "Claude Code"
|
|
},
|
|
method: "POST",
|
|
url: "/v1/messages"
|
|
});
|
|
|
|
assert.equal(result.body.model, "Provider/claude-sonnet");
|
|
assert.match(result.body.messages[2].content, /Provider\/claude-opus/);
|
|
assert.equal(result.decision.reason, "builtin:claude-code");
|
|
});
|