Files

1607 lines
53 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const test = require("node:test");
const assert = require("node:assert/strict");
const fs = require("node:fs");
const http = require("node:http");
const os = require("node:os");
const path = require("node:path");
const Module = require("node:module");
const ROUTER_MODULE = path.resolve(__dirname, "../electron/lib/router.js");
function b64(value) {
return "b64:" + Buffer.from(String(value || ""), "utf-8").toString("base64");
}
function listen(server) {
return new Promise((resolve, reject) => {
server.once("error", reject);
server.listen(0, "127.0.0.1", () => {
server.removeListener("error", reject);
resolve(server.address().port);
});
});
}
function close(server) {
return new Promise((resolve) => server.close(resolve));
}
function fixture(options = {}) {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "codex-market-router-"));
const home = path.join(root, "home");
const userData = path.join(root, "user-data");
const codexHome = path.join(home, ".codex");
const claudeHome = path.join(home, ".claude");
fs.mkdirSync(codexHome, { recursive: true });
fs.mkdirSync(claudeHome, { recursive: true });
fs.mkdirSync(userData, { recursive: true });
const officialConfig = options.officialConfig || 'model_catalog_json = "cc-switch-model-catalog.json"\n';
const officialAuthObject = options.officialAuth || {
auth_mode: "chatgpt",
tokens: {
access_token: "official-access-token",
refresh_token: "official-refresh-token",
},
};
const officialAuth = JSON.stringify(officialAuthObject, null, 2);
const routerKey = options.routerKey || "rk-test-router-key";
const groups = [
{
id: "default",
name: "我的账号",
isDefault: true,
key: "",
modelProvider: "",
providerBlock: "",
authKey: "",
origModel: "",
officialBackup: {
done: true,
ts: 1,
configToml: officialConfig,
authJson: officialAuth,
hadTokens: true,
},
},
...(options.groups || []),
];
const persistedGroups = groups.map((group) => {
const out = { ...group, key: b64(group.key) };
if (out.officialBackup && out.officialBackup.authJson) {
out.officialBackup = {
...out.officialBackup,
authJson: b64(out.officialBackup.authJson),
};
}
return out;
});
fs.writeFileSync(path.join(codexHome, "config.toml"), options.configToml || officialConfig);
fs.writeFileSync(
path.join(codexHome, "auth.json"),
JSON.stringify(options.auth || officialAuthObject, null, 2)
);
fs.writeFileSync(
path.join(userData, "router.json"),
JSON.stringify({
port: options.port || 8787,
routerKey: b64(routerKey),
activeId: options.activeId || "default",
activeClaudeId: options.activeClaudeId || null,
groups: persistedGroups,
}, null, 2)
);
if (options.claudeSettings) {
fs.writeFileSync(path.join(claudeHome, "settings.json"), JSON.stringify(options.claudeSettings, null, 2));
}
return { root, home, userData, codexHome, claudeHome, officialConfig, officialAuth, routerKey };
}
function loadRouter(fx) {
const previousHome = process.env.HOME;
process.env.HOME = fx.home;
delete require.cache[ROUTER_MODULE];
const originalLoad = Module._load;
Module._load = function load(request, parent, isMain) {
if (request === "electron") {
return {
app: {
getPath(name) {
assert.equal(name, "userData");
return fx.userData;
},
},
safeStorage: {
isEncryptionAvailable: () => false,
},
};
}
return originalLoad.call(this, request, parent, isMain);
};
try {
return require(ROUTER_MODULE);
} finally {
Module._load = originalLoad;
if (previousHome == null) delete process.env.HOME;
else process.env.HOME = previousHome;
}
}
test("DeepSeek and Kimi get conservative context windows when none is configured", { concurrency: false }, () => {
const fx = fixture({
activeId: "deepseek",
groups: [{
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://proxy.example.test",
key: "deepseek-key",
model: "deepseek-chat",
mode: "responses",
}],
});
const catalogFile = path.join(fx.codexHome, "cc-switch-model-catalog.json");
fs.writeFileSync(catalogFile, JSON.stringify({
models: [{
slug: "template",
display_name: "Template",
context_window: 128000,
max_context_window: 128000,
effective_context_window_percent: 95,
visibility: "list",
}],
}));
const router = loadRouter(fx);
try {
router.applyToCodex();
const catalog = JSON.parse(fs.readFileSync(catalogFile, "utf-8"));
const deepseek = catalog.models.find((model) => model.slug === "deepseek-chat");
assert.equal(deepseek.context_window, 65536);
assert.equal(deepseek.max_context_window, 65536);
assert.equal(deepseek.effective_context_window_percent, 80);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("takeover links the model catalog into config.toml with an absolute path", { concurrency: false }, () => {
const fx = fixture({
activeId: "deepseek",
groups: [{
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://proxy.example.test",
key: "deepseek-key",
model: "deepseek-chat",
mode: "chat",
}],
configToml: 'model_reasoning_effort = "high"\n', // 无 model_catalog_json:Codex 会忽略目录
});
const catalogFile = path.join(fx.codexHome, "cc-switch-model-catalog.json");
fs.writeFileSync(catalogFile, JSON.stringify({
models: [{
slug: "template",
display_name: "Template",
context_window: 128000,
max_context_window: 128000,
effective_context_window_percent: 95,
visibility: "list",
}],
}));
const router = loadRouter(fx);
try {
router.applyToCodex();
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, new RegExp(`^model_catalog_json = "${catalogFile.replace(/\./g, "\\.")}"$`, "m"));
// 再次接管:已是绝对路径时不重复注入
router.applyToCodex();
const again = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.equal(again.match(/^model_catalog_json = /gm).length, 1);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("relative model_catalog_json is upgraded to an absolute path and user catalogs are preserved", { concurrency: false }, () => {
const fx = fixture({
activeId: "deepseek",
groups: [{
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://proxy.example.test",
key: "deepseek-key",
model: "deepseek-chat",
mode: "responses",
}],
configToml: 'model_catalog_json = "cc-switch-model-catalog.json"\n',
});
const catalogFile = path.join(fx.codexHome, "cc-switch-model-catalog.json");
fs.writeFileSync(catalogFile, JSON.stringify({ models: [{ slug: "template", context_window: 128000, visibility: "list" }] }));
const router = loadRouter(fx);
try {
router.applyToCodex();
let configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, new RegExp(`^model_catalog_json = "${catalogFile.replace(/\./g, "\\.")}"$`, "m"));
assert.doesNotMatch(configToml, /model_catalog_json = "cc-switch/);
// 指向用户自建目录时不被改写
fs.writeFileSync(path.join(fx.codexHome, "config.toml"), 'model_catalog_json = "my-own-catalog.json"\n');
router.applyDirectToCodex("deepseek");
configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_catalog_json = "my-own-catalog.json"$/m);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("small explicit context windows are honored instead of silently ignored", { concurrency: false }, () => {
const fx = fixture({
activeId: "deepseek",
groups: [{
id: "deepseek",
name: "DeepSeek",
baseUrl: "https://proxy.example.test",
key: "deepseek-key",
model: "deepseek-chat",
mode: "responses",
contextWindow: "2048",
}],
});
const catalogFile = path.join(fx.codexHome, "cc-switch-model-catalog.json");
fs.writeFileSync(catalogFile, JSON.stringify({
models: [{ slug: "template", context_window: 128000, max_context_window: 128000, visibility: "list" }],
}));
const router = loadRouter(fx);
try {
router.applyToCodex();
const catalog = JSON.parse(fs.readFileSync(catalogFile, "utf-8"));
const deepseek = catalog.models.find((model) => model.slug === "deepseek-chat");
assert.equal(deepseek.context_window, 2048);
assert.equal(deepseek.effective_context_window_percent, 80);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("GPT groups expose only GPT text models and hide stale mixed-provider inventory", { concurrency: false }, async () => {
const fx = fixture({
activeId: "gpt",
configToml: [
'model_catalog_json = "cc-switch-model-catalog.json"',
'model_provider = "td-direct"',
'model = "gpt-5.6-sol"',
'model_reasoning_effort = "xhigh"',
"",
"[model_providers.td-direct]",
'name = "GPT relay"',
'base_url = "https://proxy.example.test/v1"',
'wire_api = "responses"',
"",
"[model_providers.local-router]",
'name = "GPT relay"',
'base_url = "https://proxy.example.test/v1"',
'wire_api = "responses"',
"",
].join("\n"),
groups: [{
id: "gpt",
name: "GPT relay",
baseUrl: "https://proxy.example.test",
key: "gpt-key",
model: "gpt-5.5",
mode: "responses",
}],
});
const catalogFile = path.join(fx.codexHome, "cc-switch-model-catalog.json");
fs.writeFileSync(catalogFile, JSON.stringify({
models: [
{ slug: "gpt-5.5", context_window: 128000, visibility: "list" },
{ slug: "gpt-5.6-sol", context_window: 128000, visibility: "list" },
{ slug: "deepseek-v4-pro", context_window: 128000, visibility: "list" },
{ slug: "qwen3.8-max-preview", context_window: 128000, visibility: "list" },
{ slug: "claude-opus-4-8", context_window: 128000, visibility: "list" },
{ slug: "gpt-image-2", context_window: 128000, visibility: "list" },
{ slug: "seedance2.0", context_window: 128000, visibility: "list" },
],
}));
const previousFetch = global.fetch;
global.fetch = async () => ({
ok: true,
json: async () => ({
data: [
{ id: "gpt-5.5" },
{ id: "gpt-5.6-sol" },
{ id: "gpt-5.4-mini" },
{ id: "deepseek-v4-pro" },
{ id: "qwen3.8-max-preview" },
{ id: "claude-opus-4-8" },
{ id: "gpt-image-2" },
{ id: "seedance2.0" },
],
}),
});
const router = loadRouter(fx);
try {
const result = await router.syncCatalogFromProvider(router.llmGroup());
assert.equal(result.ok, true);
assert.equal(result.hidden, 5);
const catalog = JSON.parse(fs.readFileSync(catalogFile, "utf-8"));
const visible = catalog.models.filter((model) => model.visibility === "list").map((model) => model.slug).sort();
assert.deepEqual(visible, ["gpt-5.4-mini", "gpt-5.5", "gpt-5.6-sol"]);
} finally {
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("running router writes the newly selected model immediately", { concurrency: false }, async () => {
const probe = http.createServer();
const port = await listen(probe);
await close(probe);
const fx = fixture({
port,
activeId: "model-a",
configToml: [
'model_provider = "td-direct"',
'model = "gpt-model-a"',
"",
"[model_providers.td-direct]",
'name = "Old direct route"',
'base_url = "https://old.example.test"',
'wire_api = "responses"',
"requires_openai_auth = true",
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "key-a",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-a",
name: "Model A",
baseUrl: "http://127.0.0.1:1",
key: "key-a",
model: "gpt-model-a",
mode: "responses",
},
{
id: "model-b",
name: "Model B",
baseUrl: "http://127.0.0.1:1",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const previousFetch = global.fetch;
global.fetch = async () => {
throw new Error("offline test");
};
const router = loadRouter(fx);
try {
await router.start();
router.applyToCodex();
const result = router.setActive("model-b");
assert.equal(result.activeId, "model-b");
assert.equal(result._needsCodexRefresh, true);
assert.equal(result._needsRestart, false);
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "local-router"$/m);
assert.match(configToml, /^model = "gpt-model-b"$/m);
assert.doesNotMatch(configToml, /^model = "gpt-model-a"$/m);
const saved = JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8"));
assert.equal(saved.activeId, "model-b");
await new Promise((resolve) => setImmediate(resolve));
} finally {
router.stop();
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("stopped router writes the selected group directly to Codex immediately", { concurrency: false }, () => {
const fx = fixture({
activeId: "default",
groups: [
{
id: "model-b",
name: "Model B",
baseUrl: "https://proxy.example.test/v1",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const previousFetch = global.fetch;
global.fetch = async () => {
throw new Error("offline test");
};
const router = loadRouter(fx);
try {
assert.equal(router.isRunning(), false);
const result = router.setActive("model-b");
assert.equal(result.activeId, "model-b");
assert.equal(result._mode, "direct");
assert.equal(result._needsRestart, true);
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "td-direct"$/m);
assert.match(configToml, /^model = "gpt-model-b"$/m);
assert.match(configToml, /^model_reasoning_effort = "high"$/m);
assert.match(configToml, /^disable_response_storage = true$/m);
assert.match(configToml, /^\[model_providers\.td-direct\]$/m);
assert.match(configToml, /^base_url = "https:\/\/proxy\.example\.test\/v1"$/m);
assert.match(configToml, /^\[model_providers\.local-router\]$/m);
const auth = JSON.parse(fs.readFileSync(path.join(fx.codexHome, "auth.json"), "utf-8"));
assert.equal(auth.OPENAI_API_KEY, "key-b");
assert.equal(auth.tokens.access_token, "official-access-token");
const saved = JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8"));
assert.equal(saved.activeId, "model-b");
} finally {
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("status repairs an empty Codex config when router auth is still active", { concurrency: false }, () => {
const fx = fixture({
activeId: "model-a",
configToml: " \n",
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "rk-test-router-key",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-a",
name: "Model A",
baseUrl: "https://proxy.example.test",
key: "key-a",
model: "gpt-model-a",
mode: "responses",
},
],
});
const previousFetch = global.fetch;
global.fetch = async () => {
throw new Error("offline test");
};
const router = loadRouter(fx);
try {
const status = router.status();
assert.equal(status.repair.repaired, true);
assert.equal(status.repair.mode, "proxy");
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "local-router"$/m);
assert.match(configToml, /^\[model_providers\.local-router\]$/m);
assert.match(configToml, /^\[model_providers\.td-direct\]$/m);
assert.match(configToml, /^base_url = "http:\/\/127\.0\.0\.1:8787"$/m);
assert.match(configToml, /^model = "gpt-model-a"$/m);
} finally {
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("status repairs a managed provider that is missing its provider block", { concurrency: false }, () => {
const fx = fixture({
activeId: "model-a",
configToml: [
'model_provider = "local-router"',
'model = "gpt-model-a"',
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "rk-test-router-key",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-a",
name: "Model A",
baseUrl: "https://proxy.example.test",
key: "key-a",
model: "gpt-model-a",
mode: "responses",
},
],
});
const previousFetch = global.fetch;
global.fetch = async () => {
throw new Error("offline test");
};
const router = loadRouter(fx);
try {
const status = router.status();
assert.equal(status.repair.repaired, true);
assert.equal(status.repair.mode, "proxy");
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "local-router"$/m);
assert.match(configToml, /^\[model_providers\.local-router\]$/m);
assert.match(configToml, /^\[model_providers\.td-direct\]$/m);
assert.match(configToml, /^wire_api = "responses"$/m);
} finally {
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("status adds a missing compatibility provider without resetting the current model", { concurrency: false }, () => {
const fx = fixture({
activeId: "model-b",
configToml: [
'model_provider = "td-direct"',
'model = "gpt-5.6-sol"',
'model_reasoning_effort = "xhigh"',
"",
"[model_providers.td-direct]",
'name = "Model B"',
'base_url = "https://proxy.example.test/v1"',
'wire_api = "responses"',
"requires_openai_auth = true",
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "key-b",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-b",
name: "Model B",
baseUrl: "https://proxy.example.test",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const router = loadRouter(fx);
try {
const status = router.status();
assert.equal(status.repair.repaired, true);
assert.equal(status.repair.mode, "direct");
assert.equal(status.repair.preservedModel, "gpt-5.6-sol");
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "td-direct"$/m);
assert.match(configToml, /^model = "gpt-5.6-sol"$/m);
assert.match(configToml, /^model_reasoning_effort = "xhigh"$/m);
assert.match(configToml, /^\[model_providers\.td-direct\]$/m);
assert.match(configToml, /^\[model_providers\.local-router\]$/m);
assert.equal((configToml.match(/^model\s*=/gm) || []).length, 1);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("status preserves the legacy default provider for historical sessions after switching relay", { concurrency: false }, () => {
const fx = fixture({
officialAuth: { OPENAI_API_KEY: "official-key" },
activeId: "model-b",
configToml: [
'model_provider = "td-direct"',
'model = "gpt-5.6-sol"',
"",
"[model_providers.td-direct]",
'name = "Model B"',
'base_url = "https://proxy.example.test/v1"',
'wire_api = "responses"',
"requires_openai_auth = true",
"",
].join("\n"),
auth: { OPENAI_API_KEY: "key-b" },
groups: [{ id: "model-b", name: "Model B", baseUrl: "https://proxy.example.test", key: "key-b", model: "gpt-model-b", mode: "responses" }],
});
const routerFile = path.join(fx.userData, "router.json");
const routerConfig = JSON.parse(fs.readFileSync(routerFile, "utf-8"));
routerConfig.groups[0].modelProvider = "custom";
fs.writeFileSync(routerFile, JSON.stringify(routerConfig, null, 2));
const router = loadRouter(fx);
try {
const result = router.setActive("model-b");
assert.equal(result._mode, "direct");
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^\[model_providers\.custom\]$/m);
assert.match(configToml, /^base_url = "https:\/\/proxy\.example\.test\/v1"$/m);
assert.match(configToml, /^wire_api = "responses"$/m);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("status repairs an empty Codex config when direct provider auth is still active", { concurrency: false }, () => {
const fx = fixture({
activeId: "model-b",
configToml: " \n",
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "key-b",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-b",
name: "Model B",
baseUrl: "https://proxy.example.test",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const previousFetch = global.fetch;
global.fetch = async () => {
throw new Error("offline test");
};
const router = loadRouter(fx);
try {
const status = router.status();
assert.equal(status.repair.repaired, true);
assert.equal(status.repair.mode, "direct");
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^model_provider = "td-direct"$/m);
assert.match(configToml, /^model = "gpt-model-b"$/m);
assert.match(configToml, /^model_reasoning_effort = "high"$/m);
assert.match(configToml, /^\[model_providers\.td-direct\]$/m);
assert.match(configToml, /^\[model_providers\.local-router\]$/m);
assert.match(configToml, /^base_url = "https:\/\/proxy\.example\.test\/v1"$/m);
} finally {
global.fetch = previousFetch;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("Sub2API provider import is idempotent and does not touch Codex config", { concurrency: false }, () => {
const fx = fixture({
activeId: "default",
groups: [],
});
const router = loadRouter(fx);
const beforeConfig = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
const beforeAuth = fs.readFileSync(path.join(fx.codexHome, "auth.json"), "utf-8");
try {
const first = router.importSub2apiProvider({
name: "Sub2API · Codex",
endpoint: "https://proxy.example.test",
apiKey: "sk-first",
model: "gpt-5.5",
sourceGroupId: "group-42",
});
assert.equal(first._imported, true);
assert.equal(first._updated, false);
assert.equal(first.activeId, "default");
assert.equal(first.groups.filter((g) => g.source === "sub2api").length, 1);
const second = router.importSub2apiProvider({
name: "Sub2API · Codex Pro",
endpoint: "https://proxy.example.test/v1",
apiKey: "sk-second",
model: "gpt-5.6",
sourceGroupId: "group-42",
});
assert.equal(second._updated, true);
assert.equal(second.activeId, "default");
assert.equal(second.groups.filter((g) => g.source === "sub2api").length, 1);
const imported = second.groups.find((g) => g.source === "sub2api");
assert.equal(imported.name, "Sub2API · Codex Pro");
assert.equal(imported.baseUrl, "https://proxy.example.test/v1");
assert.equal(imported.model, "gpt-5.6");
assert.equal(imported.mode, "responses");
assert.equal(imported.sourceGroupId, "group-42");
const persisted = JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8"));
assert.equal(persisted.groups.filter((g) => g.source === "sub2api").length, 1);
assert.equal(persisted.groups[1].key, b64("sk-second"));
assert.equal(fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8"), beforeConfig);
assert.equal(fs.readFileSync(path.join(fx.codexHome, "auth.json"), "utf-8"), beforeAuth);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("direct switch retries a transient Windows-style auth.json lock", { concurrency: false }, () => {
const fx = fixture({
activeId: "default",
groups: [
{
id: "model-b",
name: "Model B",
baseUrl: "https://proxy.example.test/v1",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const router = loadRouter(fx);
const authFile = path.join(fx.codexHome, "auth.json");
const originalRenameSync = fs.renameSync;
let authRenameAttempts = 0;
fs.renameSync = function renameSyncWithTransientLock(from, to) {
if (to === authFile && authRenameAttempts++ === 0) {
const error = new Error("file is temporarily locked");
error.code = "EPERM";
throw error;
}
return originalRenameSync.call(this, from, to);
};
try {
const result = router.setActive("model-b");
assert.equal(result.activeId, "model-b");
assert.ok(authRenameAttempts >= 2);
assert.equal(
JSON.parse(fs.readFileSync(authFile, "utf-8")).OPENAI_API_KEY,
"key-b"
);
assert.match(
fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8"),
/^model_provider = "td-direct"$/m
);
} finally {
fs.renameSync = originalRenameSync;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("direct switch rolls back config and active group when auth.json stays locked", { concurrency: false }, () => {
const fx = fixture({
activeId: "default",
groups: [
{
id: "model-b",
name: "Model B",
baseUrl: "https://proxy.example.test/v1",
key: "key-b",
model: "gpt-model-b",
mode: "responses",
},
],
});
const router = loadRouter(fx);
const configFile = path.join(fx.codexHome, "config.toml");
const authFile = path.join(fx.codexHome, "auth.json");
const beforeConfig = fs.readFileSync(configFile);
const beforeAuth = fs.readFileSync(authFile);
const originalRenameSync = fs.renameSync;
const originalWriteFileSync = fs.writeFileSync;
fs.renameSync = function renameSyncWithPersistentLock(from, to) {
if (to === authFile) {
const error = new Error("file remains locked");
error.code = "EPERM";
throw error;
}
return originalRenameSync.call(this, from, to);
};
fs.writeFileSync = function writeFileSyncWithPersistentLock(file, ...args) {
if (file === authFile) {
const error = new Error("file remains locked");
error.code = "EPERM";
throw error;
}
return originalWriteFileSync.call(this, file, ...args);
};
try {
assert.throws(
() => router.setActive("model-b"),
/无法写入配置文件 .*auth\.json/
);
assert.deepEqual(fs.readFileSync(configFile), beforeConfig);
assert.deepEqual(fs.readFileSync(authFile), beforeAuth);
assert.equal(router.status().activeId, "default");
assert.equal(
JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8")).activeId,
"default"
);
} finally {
fs.renameSync = originalRenameSync;
fs.writeFileSync = originalWriteFileSync;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("claude proxy writes active model to settings and forwards that model upstream", { concurrency: false }, async () => {
let observed;
const upstream = http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
observed = {
headers: req.headers,
body: Buffer.concat(chunks).toString("utf-8"),
};
res.writeHead(200, { "content-type": "application/json" });
res.end('{"type":"message","content":[]}');
});
});
const upstreamPort = await listen(upstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeClaudeId: "claude-glm",
claudeSettings: {
env: {
ANTHROPIC_BASE_URL: "https://original.example.test",
ANTHROPIC_AUTH_TOKEN: "original-token",
ANTHROPIC_MODEL: "original-model",
},
},
groups: [
{
id: "claude-glm",
name: "国模",
baseUrl: `http://127.0.0.1:${upstreamPort}`,
key: "glm-key",
model: "glm-5.2",
mode: "claude",
},
],
});
const router = loadRouter(fx);
const payload = JSON.stringify({
model: "claude-opus-4-8",
max_tokens: 128,
messages: [{ role: "user", content: "hi" }],
});
try {
await router.start();
const applied = router.applyToClaudeCode();
assert.equal(applied.ok, true);
const settings = JSON.parse(fs.readFileSync(path.join(fx.claudeHome, "settings.json"), "utf-8"));
assert.equal(settings.env.ANTHROPIC_BASE_URL, `http://127.0.0.1:${routerPort}`);
assert.equal(settings.env.ANTHROPIC_AUTH_TOKEN, fx.routerKey);
assert.equal(settings.env.ANTHROPIC_MODEL, "glm-5.2");
const response = await new Promise((resolve, reject) => {
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/v1/messages",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
"anthropic-version": "2023-06-01",
},
}, (res) => {
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve({
statusCode: res.statusCode,
body: Buffer.concat(chunks).toString("utf-8"),
}));
});
req.on("error", reject);
req.end(payload);
});
assert.equal(response.statusCode, 200);
assert.equal(response.body, '{"type":"message","content":[]}');
assert.ok(observed);
assert.equal(JSON.parse(observed.body).model, "glm-5.2");
assert.equal(observed.headers.authorization, "Bearer glm-key");
assert.equal(observed.headers["x-api-key"], undefined);
assert.equal(observed.headers["content-length"], String(Buffer.byteLength(observed.body)));
const restored = router.restoreClaudeCode();
assert.equal(restored.ok, true);
const restoredSettings = JSON.parse(fs.readFileSync(path.join(fx.claudeHome, "settings.json"), "utf-8"));
assert.equal(restoredSettings.env.ANTHROPIC_MODEL, "original-model");
} finally {
router.stop();
await close(upstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("switching active claude group refreshes the cli model setting while router is running", { concurrency: false }, async () => {
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeClaudeId: "claude-opus",
groups: [
{
id: "claude-opus",
name: "Claude",
baseUrl: "https://claude.example.test",
key: "opus-key",
model: "claude-opus-4-8",
mode: "claude",
},
{
id: "claude-glm",
name: "国模",
baseUrl: "https://glm.example.test",
key: "glm-key",
model: "glm-5.2",
mode: "claude",
},
],
});
const router = loadRouter(fx);
try {
await router.start();
assert.equal(router.applyToClaudeCode().ok, true);
assert.equal(
JSON.parse(fs.readFileSync(path.join(fx.claudeHome, "settings.json"), "utf-8")).env.ANTHROPIC_MODEL,
"claude-opus-4-8"
);
const result = router.setActive("claude-glm");
assert.equal(result.activeClaudeId, "claude-glm");
assert.equal(result._mode, "proxy");
assert.equal(
JSON.parse(fs.readFileSync(path.join(fx.claudeHome, "settings.json"), "utf-8")).env.ANTHROPIC_MODEL,
"glm-5.2"
);
} finally {
router.stop();
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("claude switch rolls back active group when settings.json stays locked", { concurrency: false }, () => {
const fx = fixture({
activeClaudeId: "claude-opus",
claudeSettings: {
env: {
ANTHROPIC_BASE_URL: "https://opus.example.test",
ANTHROPIC_AUTH_TOKEN: "opus-key",
ANTHROPIC_MODEL: "claude-opus-4-8",
},
},
groups: [
{
id: "claude-opus",
name: "Claude",
baseUrl: "https://opus.example.test",
key: "opus-key",
model: "claude-opus-4-8",
mode: "claude",
},
{
id: "claude-glm",
name: "国模",
baseUrl: "https://glm.example.test",
key: "glm-key",
model: "glm-5.2",
mode: "claude",
},
],
});
const router = loadRouter(fx);
const settingsFile = path.join(fx.claudeHome, "settings.json");
const beforeSettings = fs.readFileSync(settingsFile);
const originalRenameSync = fs.renameSync;
const originalWriteFileSync = fs.writeFileSync;
fs.renameSync = function renameSyncWithPersistentLock(from, to) {
if (to === settingsFile) {
const error = new Error("file remains locked");
error.code = "EPERM";
throw error;
}
return originalRenameSync.call(this, from, to);
};
fs.writeFileSync = function writeFileSyncWithPersistentLock(file, ...args) {
if (file === settingsFile) {
const error = new Error("file remains locked");
error.code = "EPERM";
throw error;
}
return originalWriteFileSync.call(this, file, ...args);
};
try {
assert.throws(
() => router.setActive("claude-glm"),
/无法写入配置文件 .*settings\.json/
);
assert.deepEqual(fs.readFileSync(settingsFile), beforeSettings);
assert.equal(router.status().activeClaudeId, "claude-opus");
assert.equal(
JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8")).activeClaudeId,
"claude-opus"
);
} finally {
fs.renameSync = originalRenameSync;
fs.writeFileSync = originalWriteFileSync;
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("stopping restores official config when managed sections or keys remain", { concurrency: false }, () => {
const fx = fixture({
activeId: "model-a",
configToml: [
'model_provider = "custom-user-provider"',
'model = "gpt-model-a"',
"",
"[model_providers.custom-user-provider]",
'name = "User provider"',
'base_url = "https://user.example.test"',
'wire_api = "responses"',
"",
"[model_providers.local-router]",
'name = "Stale local route"',
'base_url = "http://127.0.0.1:8787"',
'wire_api = "responses"',
"",
"[model_providers.td-direct]",
'name = "Stale direct route"',
'base_url = "https://stale.example.test"',
'wire_api = "responses"',
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "rk-test-router-key",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-a",
name: "Model A",
baseUrl: "https://proxy.example.test",
key: "key-a",
model: "gpt-model-a",
mode: "responses",
},
],
});
const router = loadRouter(fx);
try {
const result = router.stopAndRestoreDirect();
assert.equal(result.released, true);
assert.equal(result.removedManagedProvider, false);
assert.equal(result.keptUserProvider, true);
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.doesNotMatch(configToml, /local-router/);
assert.doesNotMatch(configToml, /td-direct/);
assert.match(configToml, /^model_provider = "custom-user-provider"$/m);
assert.match(configToml, /^\[model_providers\.custom-user-provider\]$/m);
assert.match(configToml, /^model = "gpt-model-a"$/m);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("disabling an already-stopped local route removes its Codex takeover", { concurrency: false }, () => {
const fx = fixture({
configToml: [
'model_provider = "local-router"',
'model = "gpt-model-a"',
"",
"[model_providers.local-router]",
'name = "本地路由"',
'base_url = "http://127.0.0.1:8787"',
'wire_api = "responses"',
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "rk-test-router-key",
tokens: { access_token: "official-access-token" },
},
groups: [],
});
const router = loadRouter(fx);
try {
assert.equal(router.status().running, false);
assert.equal(router.status().codexApplied, true);
const result = router.disableCodex();
assert.equal(result.released, true);
assert.equal(result.removedManagedProvider, true);
assert.equal(result.keptUserProvider, false);
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
const auth = JSON.parse(fs.readFileSync(path.join(fx.codexHome, "auth.json"), "utf-8"));
assert.doesNotMatch(configToml, /local-router/);
assert.doesNotMatch(configToml, /^model_provider\s*=/m);
assert.doesNotMatch(configToml, /^model\s*=/m);
assert.equal(auth.OPENAI_API_KEY, undefined);
assert.equal(router.status().codexApplied, false);
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("large chunked requests are forwarded with one content-length and no client hop-by-hop headers", { concurrency: false }, async () => {
let observed;
const upstream = http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
if (/\/responses\b/.test(req.url || "")) {
observed = {
url: req.url,
headers: req.headers,
rawHeaders: req.rawHeaders,
body: Buffer.concat(chunks),
};
}
res.writeHead(200, { "content-type": "application/json" });
res.end('{"ok":true}');
});
});
const upstreamPort = await listen(upstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeId: "model-a",
configToml: [
'model_provider = "local-router"',
'model = "gpt-model-a"',
"",
"[model_providers.local-router]",
'name = "Local route"',
`base_url = "http://127.0.0.1:${routerPort}"`,
'wire_api = "responses"',
"",
].join("\n"),
auth: {
auth_mode: "chatgpt",
OPENAI_API_KEY: "rk-test-router-key",
tokens: { access_token: "official-access-token" },
},
groups: [
{
id: "model-a",
name: "Model A",
baseUrl: `http://127.0.0.1:${upstreamPort}`,
key: "upstream-key",
model: "gpt-model-a",
mode: "responses",
},
],
});
const router = loadRouter(fx);
const payload = Buffer.from(JSON.stringify({
model: "gpt-model-a",
instructions: "x".repeat(180 * 1024),
input: [{ role: "user", content: "long conversation" }],
}));
try {
await router.start();
const response = await new Promise((resolve, reject) => {
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/responses",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
connection: "keep-alive, x-remove-me",
"x-remove-me": "must-not-reach-upstream",
"transfer-encoding": "chunked",
te: "trailers",
trailer: "x-client-trailer",
upgrade: "websocket",
},
}, (res) => {
const chunks = [];
res.on("data", (chunk) => chunks.push(chunk));
res.on("end", () => resolve({
statusCode: res.statusCode,
body: Buffer.concat(chunks).toString("utf-8"),
}));
});
req.on("error", reject);
for (let offset = 0; offset < payload.length; offset += 4096) {
req.write(payload.subarray(offset, offset + 4096));
}
req.end();
});
assert.equal(response.statusCode, 200);
assert.equal(response.body, '{"ok":true}');
assert.ok(observed);
assert.equal(observed.url, "/v1/responses");
assert.equal(observed.body.length, payload.length);
assert.deepEqual(JSON.parse(observed.body.toString("utf-8")), JSON.parse(payload.toString("utf-8")));
assert.equal(observed.headers["content-length"], String(payload.length));
assert.equal(observed.headers["transfer-encoding"], undefined);
assert.equal(observed.headers.te, undefined);
assert.equal(observed.headers.trailer, undefined);
assert.equal(observed.headers.upgrade, undefined);
assert.equal(observed.headers["x-remove-me"], undefined);
assert.equal(observed.headers.host, `127.0.0.1:${upstreamPort}`);
assert.equal(
observed.rawHeaders.filter((_, index) => index % 2 === 0 && observed.rawHeaders[index].toLowerCase() === "content-length").length,
1
);
} finally {
router.stop();
await close(upstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("responses direct switch upgrades a site root base_url for Codex", { concurrency: false }, () => {
const fx = fixture({
activeId: null,
auth: {},
groups: [
{
id: "first-relay",
name: "First \\ Relay",
baseUrl: "https://proxy.example.test/",
key: "first-key",
model: "gpt-first",
mode: "responses",
},
],
});
const router = loadRouter(fx);
try {
const result = router.setActive("first-relay");
assert.equal(result._mode, "direct");
assert.equal(result._needsRestart, true);
const configToml = fs.readFileSync(path.join(fx.codexHome, "config.toml"), "utf-8");
assert.match(configToml, /^base_url = "https:\/\/proxy\.example\.test\/v1"$/m);
assert.match(configToml, /^name = "First \\\\ Relay"$/m);
const auth = JSON.parse(fs.readFileSync(path.join(fx.codexHome, "auth.json"), "utf-8"));
assert.equal(auth.OPENAI_API_KEY, "first-key");
} finally {
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("chat multimodal group injects MiniMax evidence before forwarding to DeepSeek", { concurrency: false }, async () => {
let upstreamBody;
const upstream = http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
upstreamBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
res.writeHead(200, { "content-type": "text/event-stream" });
res.end('data: {"choices":[{"delta":{"content":"ok"}}]}\n\ndata: [DONE]\n\n');
});
});
const upstreamPort = await listen(upstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeId: "deepseek-chat",
groups: [
{
id: "deepseek-chat",
name: "DeepSeek",
baseUrl: `http://127.0.0.1:${upstreamPort}`,
key: "deepseek-key",
model: "deepseek-chat",
mode: "chat",
multimodal: true,
},
],
});
const previousFetch = global.fetch;
const previousKey = process.env.TD_MULTIMODAL_GATEWAY_KEY;
process.env.TD_MULTIMODAL_GATEWAY_KEY = "test-multimodal-key";
global.fetch = async () => ({
ok: true,
json: async () => ({
choices: [{
message: {
content: '{"summary":"登录页截图","items":[{"kind":"ocr","text":"账号登录","confidence":0.99}]}',
},
}],
}),
});
const router = loadRouter(fx);
try {
await router.start();
await new Promise((resolve, reject) => {
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/responses",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
},
}, (res) => {
res.resume();
res.on("end", resolve);
});
req.on("error", reject);
req.end(JSON.stringify({
model: "gpt-5",
stream: true,
tool_choice: "auto",
tools: [
{ type: "function", name: "exec_command", parameters: { type: "object" } },
{ type: "function", name: "view_image", parameters: { type: "object" } },
],
input: [{
type: "message",
role: "user",
content: [
{ type: "input_text", text: "图片里有什么?" },
{ type: "input_image", image_url: "data:image/png;base64,cm91dGVyLXRlc3Q=" },
],
}],
}));
});
assert.ok(upstreamBody);
assert.equal(upstreamBody.model, "deepseek-chat");
const userContent = upstreamBody.messages
.filter((message) => message.role === "user")
.map((message) => message.content)
.join("\n");
assert.match(userContent, /图片里有什么/);
assert.match(userContent, /\[TD 多模态识别证据\]/);
assert.match(userContent, /账号登录/);
assert.doesNotMatch(userContent, /test-multimodal-key/);
assert.equal(upstreamBody.tool_choice, "auto");
assert.deepEqual(upstreamBody.tools.map((tool) => tool.function.name), ["exec_command"]);
} finally {
router.stop();
global.fetch = previousFetch;
if (previousKey == null) delete process.env.TD_MULTIMODAL_GATEWAY_KEY;
else process.env.TD_MULTIMODAL_GATEWAY_KEY = previousKey;
await close(upstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("explicit model routes total control and worker requests to their registered groups", { concurrency: false }, async () => {
const seen = [];
const makeUpstream = (name) => http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
seen.push({ name, body: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
res.writeHead(200, { "content-type": "application/json" });
res.end('{"ok":true}');
});
});
const gptUpstream = makeUpstream("gpt");
const deepseekUpstream = makeUpstream("deepseek");
const gptPort = await listen(gptUpstream);
const deepseekPort = await listen(deepseekUpstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeId: "gpt-group",
groups: [
{
id: "gpt-group",
name: "GPT 总控",
baseUrl: `http://127.0.0.1:${gptPort}`,
key: "gpt-key",
model: "gpt-5.6-luna",
mode: "responses",
},
{
id: "deepseek-group",
name: "DeepSeek 执行",
baseUrl: `http://127.0.0.1:${deepseekPort}`,
key: "deepseek-key",
model: "deepseek-v4-flash",
mode: "responses",
},
],
});
const router = loadRouter(fx);
const send = (model) => new Promise((resolve, reject) => {
const payload = JSON.stringify({ model, input: [{ role: "user", content: "ping" }] });
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/responses",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
},
}, (res) => {
res.resume();
res.on("end", () => resolve(res.statusCode));
});
req.on("error", reject);
req.end(payload);
});
try {
await router.start();
const route = router.registerModelRoute("deepseek-group", "deepseek-v4-flash");
assert.equal(route.ok, true);
assert.equal(route.groupId, "deepseek-group");
const saved = JSON.parse(fs.readFileSync(path.join(fx.userData, "router.json"), "utf-8"));
assert.deepEqual(saved.groups.find((g) => g.id === "deepseek-group").routeModels, ["deepseek-v4-flash"]);
assert.equal(await send("gpt-5.6-luna"), 200);
assert.equal(await send("deepseek-v4-flash"), 200);
assert.deepEqual(seen.map((x) => [x.name, x.body.model]), [
["gpt", "gpt-5.6-luna"],
["deepseek", "deepseek-v4-flash"],
]);
} finally {
router.stop();
await close(gptUpstream);
await close(deepseekUpstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("stale selector model does not override the active group while router is running", { concurrency: false }, async () => {
const seen = [];
const makeUpstream = (name) => http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
seen.push({ name, body: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
res.writeHead(200, { "content-type": "application/json" });
res.end('{"ok":true}');
});
});
const kimiUpstream = makeUpstream("kimi");
const deepseekUpstream = makeUpstream("deepseek");
const kimiPort = await listen(kimiUpstream);
const deepseekPort = await listen(deepseekUpstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeId: "deepseek-group",
groups: [
{
id: "kimi-group",
name: "Kimi",
baseUrl: `http://127.0.0.1:${kimiPort}`,
key: "kimi-key",
model: "kimi-k3",
mode: "responses",
},
{
id: "deepseek-group",
name: "DeepSeek",
baseUrl: `http://127.0.0.1:${deepseekPort}`,
key: "deepseek-key",
model: "deepseek-v4-pro",
mode: "responses",
},
],
});
const router = loadRouter(fx);
// Codex 顶部选择器尚未刷新,普通请求仍携带旧模型名。它不应覆盖 activeId。
const payload = JSON.stringify({
model: "kimi-k3",
instructions: "hello",
input: [{ role: "user", content: "ping" }],
});
try {
await router.start();
const status = await new Promise((resolve, reject) => {
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/responses",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
},
}, (res) => {
res.resume();
res.on("end", () => resolve(res.statusCode));
});
req.on("error", reject);
req.end(payload);
});
assert.equal(status, 200);
assert.deepEqual(seen.map((x) => [x.name, x.body.model]), [
["deepseek", "deepseek-v4-pro"],
]);
} finally {
router.stop();
await close(kimiUpstream);
await close(deepseekUpstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});
test("agent route hint overrides root model for worker requests", { concurrency: false }, async () => {
const seen = [];
const makeUpstream = (name) => http.createServer((req, res) => {
const chunks = [];
req.on("data", (chunk) => chunks.push(chunk));
req.on("end", () => {
seen.push({ name, body: JSON.parse(Buffer.concat(chunks).toString("utf-8")) });
res.writeHead(200, { "content-type": "application/json" });
res.end('{"ok":true}');
});
});
const gptUpstream = makeUpstream("gpt");
const deepseekUpstream = makeUpstream("deepseek");
const gptPort = await listen(gptUpstream);
const deepseekPort = await listen(deepseekUpstream);
const probe = http.createServer();
const routerPort = await listen(probe);
await close(probe);
const fx = fixture({
port: routerPort,
activeId: "gpt-group",
groups: [
{
id: "gpt-group",
name: "GPT 总控",
baseUrl: `http://127.0.0.1:${gptPort}`,
key: "gpt-key",
model: "gpt-5.6-luna",
mode: "responses",
},
{
id: "deepseek-group",
name: "DeepSeek 执行",
baseUrl: `http://127.0.0.1:${deepseekPort}`,
key: "deepseek-key",
model: "deepseek-v4-flash",
mode: "responses",
},
],
});
const router = loadRouter(fx);
const payload = JSON.stringify({
model: "gpt-5.6-luna",
instructions: '你是 worker。\n[TD_MODEL_ROUTE model="deepseek-v4-flash" role="worker" agent="frontend"; runtime hint only.]',
input: [{ role: "user", content: "ping" }],
});
try {
await router.start();
router.registerModelRoute("deepseek-group", "deepseek-v4-flash");
const status = await new Promise((resolve, reject) => {
const req = http.request({
method: "POST",
host: "127.0.0.1",
port: routerPort,
path: "/responses",
headers: {
authorization: "Bearer " + fx.routerKey,
"content-type": "application/json",
"content-length": Buffer.byteLength(payload),
},
}, (res) => {
res.resume();
res.on("end", () => resolve(res.statusCode));
});
req.on("error", reject);
req.end(payload);
});
assert.equal(status, 200);
assert.deepEqual(seen.map((x) => [x.name, x.body.model]), [
["deepseek", "deepseek-v4-flash"],
]);
} finally {
router.stop();
await close(gptUpstream);
await close(deepseekUpstream);
fs.rmSync(fx.root, { recursive: true, force: true });
}
});