mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
fix fusion error
This commit is contained in:
parent
c8cbc09b96
commit
bc872a29b3
5 changed files with 514 additions and 26 deletions
|
|
@ -68,6 +68,7 @@ function runtimeEntryPointsForSuite(suiteName) {
|
|||
return [];
|
||||
}
|
||||
return [
|
||||
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
|
||||
path.join(coreSourceRoot, "mcp", "toolhub-mcp.ts")
|
||||
];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -246,7 +246,7 @@ function resolveBuiltInClaudeCodeSubagentRouteDecision(
|
|||
return undefined;
|
||||
}
|
||||
const target = normalizeRouteSelector(request.builtInSubagentModel);
|
||||
if (!target) {
|
||||
if (!target || isSubagentModelPlaceholder(target) || !isKnownInlineRoute(target, config)) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
|
|
@ -322,6 +322,7 @@ function builtInAgentUserAgentNeedle(agent: RouterBuiltInAgentRuleId): string {
|
|||
const ccrSubagentModelOpenTag = "<CCR-SUBAGENT-MODEL>";
|
||||
const ccrSubagentModelCloseTag = "</CCR-SUBAGENT-MODEL>";
|
||||
const ccrSubagentModelTagExample = `${ccrSubagentModelOpenTag}Provider/model${ccrSubagentModelCloseTag}`;
|
||||
const ccrSubagentModelPlaceholder = "provider/model";
|
||||
const claudeCodeBillingSystemHeaderPrefix = "x-anthropic-billing-header";
|
||||
const ccrSubagentToolModelInstruction =
|
||||
`CCR subagent routing is enabled. When calling this tool, the prompt parameter MUST start with ` +
|
||||
|
|
@ -1200,6 +1201,10 @@ function isKnownInlineRoute(model: string | undefined, config: AppConfig): boole
|
|||
return config.Providers.some((provider) => provider.name.trim().toLowerCase() === providerName);
|
||||
}
|
||||
|
||||
function isSubagentModelPlaceholder(model: string): boolean {
|
||||
return model.trim().toLowerCase() === ccrSubagentModelPlaceholder;
|
||||
}
|
||||
|
||||
function calculateTokenCount(messages: unknown, system: unknown, tools: unknown): number {
|
||||
return countMessageTokens(messages) + countSystemTokens(system) + countToolTokens(tools);
|
||||
}
|
||||
|
|
|
|||
188
tests/main/fusion-vision-mcp.test.mjs
Normal file
188
tests/main/fusion-vision-mcp.test.mjs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { spawn } from "node:child_process";
|
||||
import http from "node:http";
|
||||
import path from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
test("Fusion vision MCP sends the injected core gateway API key", async (t) => {
|
||||
const seen = {
|
||||
authorization: "",
|
||||
body: undefined,
|
||||
path: ""
|
||||
};
|
||||
const server = http.createServer(async (request, response) => {
|
||||
seen.authorization = request.headers.authorization ?? "";
|
||||
seen.path = request.url ?? "";
|
||||
const body = await readRequestBody(request);
|
||||
seen.body = JSON.parse(body);
|
||||
response.writeHead(200, { "content-type": "application/json" });
|
||||
response.end(JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "vision ok"
|
||||
}
|
||||
}
|
||||
]
|
||||
}));
|
||||
});
|
||||
|
||||
try {
|
||||
await listen(server);
|
||||
} catch (error) {
|
||||
if (isLocalListenUnavailable(error)) {
|
||||
t.skip(`Local HTTP listen is unavailable: ${formatError(error)}`);
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
t.after(() => server.close());
|
||||
|
||||
const address = server.address();
|
||||
assert.ok(address && typeof address === "object");
|
||||
const child = spawn(process.execPath, [path.join(process.cwd(), "dist", "tests", "main", "fusion-vision-mcp.js")], {
|
||||
env: {
|
||||
...process.env,
|
||||
ELECTRON_RUN_AS_NODE: "1",
|
||||
FUSION_BUILTIN_TOOL_KIND: "vision",
|
||||
FUSION_TOOL_NAME: "vision_understand_glm_5_2v",
|
||||
VISION_GATEWAY_API_KEY: "core-token",
|
||||
VISION_GATEWAY_BASE_URL: `http://127.0.0.1:${address.port}/v1`,
|
||||
VISION_MODEL: "provider-zhipu-ai-china---coding-plan-d63a2c4b21::openai_chat_completions::cred:test-1/glm-5v-turbo"
|
||||
},
|
||||
stdio: ["pipe", "pipe", "pipe"]
|
||||
});
|
||||
t.after(() => {
|
||||
if (!child.killed) {
|
||||
child.kill();
|
||||
}
|
||||
});
|
||||
|
||||
const response = await sendJsonRpc(child, {
|
||||
id: 1,
|
||||
jsonrpc: "2.0",
|
||||
method: "tools/call",
|
||||
params: {
|
||||
arguments: {
|
||||
imageBase64: "iVBORw0KGgo=",
|
||||
mimeType: "image/png",
|
||||
prompt: "Describe this image."
|
||||
},
|
||||
name: "vision_understand_glm_5_2v"
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(response.error, undefined);
|
||||
assert.equal(response.result?.isError, undefined);
|
||||
assert.equal(response.result?.content?.[0]?.text, "vision ok");
|
||||
assert.equal(seen.authorization, "Bearer core-token");
|
||||
assert.equal(seen.path, "/v1/chat/completions");
|
||||
assert.equal(seen.body?.model, "provider-zhipu-ai-china---coding-plan-d63a2c4b21::openai_chat_completions::cred:test-1/glm-5v-turbo");
|
||||
assert.equal(seen.body?.messages?.[0]?.content?.[1]?.type, "image_url");
|
||||
});
|
||||
|
||||
function listen(server) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const onError = (error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
const onListening = () => {
|
||||
cleanup();
|
||||
resolve();
|
||||
};
|
||||
const cleanup = () => {
|
||||
server.off("error", onError);
|
||||
server.off("listening", onListening);
|
||||
};
|
||||
server.once("error", onError);
|
||||
server.once("listening", onListening);
|
||||
server.listen(0, "127.0.0.1");
|
||||
});
|
||||
}
|
||||
|
||||
function readRequestBody(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = [];
|
||||
request.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
||||
request.on("error", reject);
|
||||
request.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
});
|
||||
}
|
||||
|
||||
function sendJsonRpc(child, payload) {
|
||||
return new Promise((resolve, reject) => {
|
||||
let stdout = Buffer.alloc(0);
|
||||
let stderr = "";
|
||||
const cleanup = () => {
|
||||
clearTimeout(timer);
|
||||
child.stdout.off("data", onStdout);
|
||||
child.stderr.off("data", onStderr);
|
||||
child.off("exit", onExit);
|
||||
child.off("error", onError);
|
||||
};
|
||||
const finish = (value) => {
|
||||
cleanup();
|
||||
resolve(value);
|
||||
};
|
||||
const fail = (error) => {
|
||||
cleanup();
|
||||
reject(error);
|
||||
};
|
||||
const onStdout = (chunk) => {
|
||||
stdout = Buffer.concat([stdout, Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)]);
|
||||
const parsed = readJsonRpcFrame(stdout);
|
||||
if (!parsed) {
|
||||
return;
|
||||
}
|
||||
finish(parsed.value);
|
||||
};
|
||||
const onStderr = (chunk) => {
|
||||
stderr += Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
||||
};
|
||||
const onExit = (code, signal) => {
|
||||
fail(new Error(`fusion-vision MCP exited before response: code=${code ?? ""} signal=${signal ?? ""} stderr=${stderr}`));
|
||||
};
|
||||
const onError = (error) => fail(error);
|
||||
const timer = setTimeout(() => fail(new Error(`Timed out waiting for fusion-vision MCP response. stderr=${stderr}`)), 5000);
|
||||
|
||||
child.stdout.on("data", onStdout);
|
||||
child.stderr.on("data", onStderr);
|
||||
child.once("exit", onExit);
|
||||
child.once("error", onError);
|
||||
|
||||
const message = Buffer.from(JSON.stringify(payload), "utf8");
|
||||
child.stdin.write(`Content-Length: ${message.byteLength}\r\n\r\n`);
|
||||
child.stdin.write(message);
|
||||
});
|
||||
}
|
||||
|
||||
function readJsonRpcFrame(buffer) {
|
||||
const headerEnd = buffer.indexOf("\r\n\r\n");
|
||||
if (headerEnd < 0) {
|
||||
return undefined;
|
||||
}
|
||||
const header = buffer.subarray(0, headerEnd).toString("utf8");
|
||||
const match = header.match(/content-length:\s*(\d+)/i);
|
||||
if (!match) {
|
||||
throw new Error(`Missing Content-Length in MCP response: ${header}`);
|
||||
}
|
||||
const length = Number(match[1]);
|
||||
const bodyStart = headerEnd + 4;
|
||||
const bodyEnd = bodyStart + length;
|
||||
if (buffer.length < bodyEnd) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
rest: buffer.subarray(bodyEnd),
|
||||
value: JSON.parse(buffer.subarray(bodyStart, bodyEnd).toString("utf8"))
|
||||
};
|
||||
}
|
||||
|
||||
function isLocalListenUnavailable(error) {
|
||||
return error && typeof error === "object" && (error.code === "EPERM" || error.code === "EACCES");
|
||||
}
|
||||
|
||||
function formatError(error) {
|
||||
return error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
|
|
@ -81,28 +81,136 @@ test("gateway config rewrites Fusion fixed base and vision models to core provid
|
|||
assert.equal(profiles[0].baseModel.fixedModel, `${providerName}/glm-5.2`);
|
||||
});
|
||||
|
||||
test("gateway config injects core auth token into Fusion vision MCP gateway runtime", async () => {
|
||||
const profiles = [
|
||||
{
|
||||
enabled: true,
|
||||
id: "glm-vision",
|
||||
key: "glm-vision",
|
||||
metadata: {
|
||||
fusionVision: {
|
||||
modelSelector: "VisionProvider/glm-5v",
|
||||
toolName: "vision_understand_glm"
|
||||
}
|
||||
test("issue 1480 Fusion vision config injects core auth token into MCP gateway runtime", async () => {
|
||||
const providerName = "Zhipu AI (China) - Coding Plan";
|
||||
const config = {
|
||||
CUSTOM_ROUTER_PATH: "",
|
||||
Providers: [
|
||||
{
|
||||
api_base_url: "https://nebulacoder.example/v1/chat/completions",
|
||||
credentials: [{ apiKey: "nebulacoder-key", id: "nebulacoder-main" }],
|
||||
models: ["nebulacoder-v8.0", "nebulacoder-cot-v8.0"],
|
||||
name: "NebulaCoder"
|
||||
},
|
||||
{
|
||||
api_base_url: "https://coclaw.example/v1/chat/completions",
|
||||
credentials: [{ apiKey: "coclaw-key", id: "coclaw-main" }],
|
||||
models: ["Qwen3-235B-A22B"],
|
||||
name: "CoClaw"
|
||||
},
|
||||
{
|
||||
api_base_url: "https://opencode.ai/zen/go/v1/chat/completions",
|
||||
capabilities: [
|
||||
{ baseUrl: "https://opencode.ai/zen/go/v1/chat/completions", type: "openai_chat_completions" }
|
||||
],
|
||||
credentials: [{ apiKey: "opencode-key", id: "opencode-main" }],
|
||||
models: ["kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash"],
|
||||
name: "OpenCode"
|
||||
},
|
||||
{
|
||||
baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4",
|
||||
capabilities: [
|
||||
{ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", type: "openai_chat_completions" },
|
||||
{ baseUrl: "https://open.bigmodel.cn/api/coding/paas/v4", type: "anthropic_messages" }
|
||||
],
|
||||
credentials: [{ apiKey: "zhipu-key", id: "test-1" }],
|
||||
models: ["glm-5.2", "glm-4.5-air", "glm-5v-turbo"],
|
||||
name: providerName,
|
||||
type: "openai_chat_completions"
|
||||
}
|
||||
}
|
||||
];
|
||||
],
|
||||
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: "127.0.0.1", corePort: 3457 },
|
||||
profile: {
|
||||
enabled: true,
|
||||
profiles: [
|
||||
{
|
||||
agent: "claude-code",
|
||||
enabled: true,
|
||||
id: "claude-code-main",
|
||||
model: "kimi-k2.6",
|
||||
name: "Claude Code",
|
||||
scope: "global"
|
||||
}
|
||||
]
|
||||
},
|
||||
virtualModelProfiles: [
|
||||
{
|
||||
baseModel: { fixedModel: `${providerName}/glm-5.2`, mode: "fixed" },
|
||||
displayName: "GLM 5 2V",
|
||||
enabled: true,
|
||||
execution: {
|
||||
clientToolsPolicy: "allow",
|
||||
matchMultimodal: true,
|
||||
maxToolCalls: 8,
|
||||
maxTurns: 6,
|
||||
mode: "tool_loop",
|
||||
streamMode: "buffered"
|
||||
},
|
||||
id: "glm-5.2v",
|
||||
key: "glm-5.2v",
|
||||
match: { exactAliases: ["GLM-5.2V", "Fusion/GLM-5.2V"], prefixes: [], suffixes: [] },
|
||||
materialization: { enabled: true, includeInGatewayModels: true },
|
||||
metadata: {
|
||||
fusionVision: {
|
||||
modelSelector: `${providerName}/glm-5v-turbo`,
|
||||
toolName: "vision_understand_glm_5_2v"
|
||||
}
|
||||
},
|
||||
tools: [{ name: "vision_understand_glm_5_2v", visibility: "internal" }]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
const profiles = normalizeCoreGatewayVirtualModelProfiles(config.virtualModelProfiles, config);
|
||||
const artifacts = await fusionBuiltinToolArtifactsForTest(profiles, "http://127.0.0.1:3457", "core-token");
|
||||
const server = artifacts.mcpServers.find((item) => item.name === "fusion-vision-glm-vision");
|
||||
const server = artifacts.mcpServers.find((item) => item.name === "fusion-vision-glm-5.2v");
|
||||
|
||||
assert.ok(server);
|
||||
assert.equal(server.env.VISION_GATEWAY_BASE_URL, "http://127.0.0.1:3457/v1");
|
||||
assert.equal(server.env.VISION_GATEWAY_API_KEY, "core-token");
|
||||
assert.equal(server.env.VISION_API_KEY, undefined);
|
||||
assert.match(
|
||||
server.env.VISION_MODEL,
|
||||
/^provider-zhipu-ai-china---coding-plan-[a-f0-9]{10}::openai_chat_completions::cred:test-1\/glm-5v-turbo$/
|
||||
);
|
||||
});
|
||||
|
||||
test("gateway config does not inject core auth token into external Fusion vision MCP runtime", async () => {
|
||||
|
|
|
|||
|
|
@ -42,13 +42,14 @@ function createRouterPlugin(options = {}) {
|
|||
});
|
||||
}
|
||||
|
||||
function createIssue1480RouterConfig() {
|
||||
function createIssue1480UserConfig() {
|
||||
return {
|
||||
APIKEY: "gateway-key",
|
||||
CUSTOM_ROUTER_PATH: "",
|
||||
Providers: [
|
||||
{
|
||||
api_base_url: "https://nebulacoder.example/v1/chat/completions",
|
||||
credentials: [{ apiKey: "nebulacoder-key", id: "nebulacoder-main" }],
|
||||
api_key: "nebulacoder-key",
|
||||
modelDescriptions: {
|
||||
"nebulacoder-v8.0": "Code reading model.",
|
||||
"nebulacoder-cot-v8.0": "Code reading reasoning model."
|
||||
|
|
@ -58,7 +59,7 @@ function createIssue1480RouterConfig() {
|
|||
},
|
||||
{
|
||||
api_base_url: "https://coclaw.example/v1/chat/completions",
|
||||
credentials: [{ apiKey: "coclaw-key", id: "coclaw-main" }],
|
||||
api_key: "coclaw-key",
|
||||
modelDescriptions: {
|
||||
"Qwen3-235B-A22B": "Background task model."
|
||||
},
|
||||
|
|
@ -67,24 +68,43 @@ function createIssue1480RouterConfig() {
|
|||
},
|
||||
{
|
||||
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" }
|
||||
],
|
||||
credentials: [{ apiKey: "opencode-key", id: "opencode-main" }],
|
||||
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.6": "Claude Code profile model.",
|
||||
"kimi-k2.7": "Fast model.",
|
||||
"mimo-v2.5": "Lightweight model.",
|
||||
"mimo-v2.5-pro": "Enhanced lightweight model."
|
||||
},
|
||||
models: ["kimi-k2.6", "deepseek-v4-pro", "deepseek-v4-flash"],
|
||||
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: {
|
||||
builtInRules: {
|
||||
"claude-code": { enabled: true },
|
||||
codex: { enabled: true }
|
||||
},
|
||||
fallback: { mode: "retry", models: [], retryCount: 1 },
|
||||
rules: [
|
||||
{
|
||||
|
|
@ -134,6 +154,13 @@ function createIssue1480RouterConfig() {
|
|||
}
|
||||
]
|
||||
},
|
||||
gateway: {
|
||||
coreHost: "0.0.0.0",
|
||||
corePort: 3457,
|
||||
enabled: true,
|
||||
host: "0.0.0.0",
|
||||
port: 3456
|
||||
},
|
||||
profile: {
|
||||
enabled: true,
|
||||
profiles: [
|
||||
|
|
@ -147,7 +174,68 @@ function createIssue1480RouterConfig() {
|
|||
}
|
||||
]
|
||||
},
|
||||
virtualModelProfiles: []
|
||||
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` }]
|
||||
}))
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -367,6 +455,83 @@ test("router rules can add headers after the built-in Claude Code profile route"
|
|||
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);
|
||||
|
|
@ -854,6 +1019,27 @@ test("built-in Claude Code subagent route uses model tag from system", async ()
|
|||
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({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue