feat: 发布热更 1.5.94,修复中转历史会话并支持一键清空
This commit is contained in:
+374
-6
@@ -264,6 +264,77 @@ test("small explicit context windows are honored instead of silently ignored", {
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
@@ -361,9 +432,11 @@ test("stopped router writes the selected group directly to Codex immediately", {
|
||||
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.doesNotMatch(configToml, /^\[model_providers\.local-router\]$/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");
|
||||
@@ -377,6 +450,267 @@ test("stopped router writes the selected group directly to Codex immediately", {
|
||||
}
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -791,11 +1125,14 @@ test("large chunked requests are forwarded with one content-length and no client
|
||||
const chunks = [];
|
||||
req.on("data", (chunk) => chunks.push(chunk));
|
||||
req.on("end", () => {
|
||||
observed = {
|
||||
headers: req.headers,
|
||||
rawHeaders: req.rawHeaders,
|
||||
body: Buffer.concat(chunks),
|
||||
};
|
||||
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}');
|
||||
});
|
||||
@@ -875,6 +1212,7 @@ test("large chunked requests are forwarded with one content-length and no client
|
||||
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));
|
||||
@@ -895,6 +1233,36 @@ test("large chunked requests are forwarded with one content-length and no client
|
||||
}
|
||||
});
|
||||
|
||||
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) => {
|
||||
|
||||
Reference in New Issue
Block a user