Files
codex-market-app/test/router.test.js
T

1607 lines
53 KiB
JavaScript
Raw Normal View History

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 });
}
});