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("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_providers\.td-direct\]$/m); assert.match(configToml, /^base_url = "https:\/\/proxy\.example\.test\/v1"$/m); assert.doesNotMatch(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("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", () => { observed = { 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.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("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 }); } });