mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-02 13:22:11 +03:00
* fix(auto): pool accounts by provider model * test(auto): update provider-family-combos to the #7928 Cartesian pool shape Since #7928 the auto-combo candidate pool is a connections × models Cartesian product, so createBuiltinAutoCombo("auto/<family>") now surfaces each backend's full family line-up rather than exactly one default model per connection. The #6453 invariant is unchanged and still asserted — which providers span the family and that unrelated providers (the connected openai/gpt-4o-mini, the connected glm on auto/zai) are excluded — but the two exact-count assertions are relaxed to the provider SET and the detectModelFamily() family check, matching the pattern the sibling auto/minimax case already uses. Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --------- Co-authored-by: Your Name <you@example.com> Co-authored-by: adrianaryaputra <adrian.arya@gmail.com> Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
255 lines
9.4 KiB
TypeScript
255 lines
9.4 KiB
TypeScript
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
|
|
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-virtual-auto-"));
|
|
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
|
|
|
process.env.DATA_DIR = TEST_DATA_DIR;
|
|
|
|
const core = await import("../../src/lib/db/core.ts");
|
|
const providersDb = await import("../../src/lib/db/providers.ts");
|
|
const virtualFactory = await import("../../open-sse/services/autoCombo/virtualFactory.ts");
|
|
|
|
type VirtualComboResult = Awaited<ReturnType<typeof virtualFactory.createVirtualAutoCombo>>;
|
|
|
|
async function resetStorage() {
|
|
core.resetDbInstance();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetStorage();
|
|
});
|
|
|
|
test.after(async () => {
|
|
await resetStorage();
|
|
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
|
|
|
if (ORIGINAL_DATA_DIR === undefined) {
|
|
delete process.env.DATA_DIR;
|
|
} else {
|
|
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
|
}
|
|
});
|
|
|
|
test("createVirtualAutoCombo returns an executable auto combo for API-key connections", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "openai",
|
|
authType: "apikey",
|
|
name: "OpenAI",
|
|
apiKey: "sk-test-openai",
|
|
defaultModel: "gpt-4o-mini",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
|
|
|
|
assert.equal(combo.strategy, "auto");
|
|
assert.ok(combo.models.length >= 1);
|
|
const openaiModel = combo.models.find(
|
|
(model) => model.providerId === "openai" && model.model === "openai/gpt-4o-mini"
|
|
);
|
|
assert.ok(openaiModel, "the configured default must remain among registry candidates");
|
|
assert.equal(openaiModel.kind, "model");
|
|
assert.equal(combo.autoConfig.routerStrategy, "lkgp");
|
|
assert.ok(combo.autoConfig.candidatePool.includes("openai"));
|
|
});
|
|
|
|
test("createVirtualAutoCombo includes OAuth accessToken connections with real expiry fields", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "anthropic",
|
|
authType: "oauth",
|
|
email: "oauth@example.com",
|
|
accessToken: "oauth-access-token",
|
|
tokenExpiresAt: new Date(Date.now() + 60_000).toISOString(),
|
|
defaultModel: "claude-sonnet-4-5",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
assert.equal(combo.strategy, "auto");
|
|
assert.ok(combo.models.length >= 1);
|
|
assert.ok(
|
|
combo.models.some(
|
|
(model) => model.providerId === "anthropic" && model.model === "anthropic/claude-sonnet-4-5"
|
|
),
|
|
"the configured default must remain among registry candidates"
|
|
);
|
|
assert.ok(combo.autoConfig.candidatePool.includes("anthropic"));
|
|
});
|
|
|
|
test("createVirtualAutoCombo includes configured web-session providers without apiKey fields", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "qwen-web",
|
|
authType: "apikey",
|
|
name: "Qwen Web Session",
|
|
providerSpecificData: { token: "qwen-web-session-token" },
|
|
defaultModel: "qwen3-coder-plus",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
const qwenWeb = combo.models.find(
|
|
(model) => model.providerId === "qwen-web" && model.model === "qwen-web/qwen3-coder-plus"
|
|
);
|
|
assert.ok(qwenWeb, "the configured web-session model should be an auto candidate");
|
|
assert.ok(combo.autoConfig.candidatePool.includes("qwen-web"));
|
|
});
|
|
|
|
test("createVirtualAutoCombo excludes web-session providers with empty required token data", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "qwen-web",
|
|
authType: "apikey",
|
|
name: "Qwen Web Empty Session",
|
|
providerSpecificData: { token: " " },
|
|
defaultModel: "qwen3-coder-plus",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
assert.equal(
|
|
combo.models.some((model) => model.providerId === "qwen-web"),
|
|
false,
|
|
"web-session providers with empty required token data must not be auto-combo candidates"
|
|
);
|
|
assert.equal(combo.autoConfig.candidatePool.includes("qwen-web"), false);
|
|
});
|
|
|
|
test("createVirtualAutoCombo excludes web-session providers with irrelevant providerSpecificData", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "chatgpt-web",
|
|
authType: "apikey",
|
|
name: "ChatGPT Web Invalid Session",
|
|
providerSpecificData: { unrelated: "value" },
|
|
defaultModel: "gpt-4o",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
assert.equal(
|
|
combo.models.some((model) => model.providerId === "chatgpt-web"),
|
|
false,
|
|
"web-session providers with irrelevant providerSpecificData must not be auto-combo candidates"
|
|
);
|
|
assert.equal(combo.autoConfig.candidatePool.includes("chatgpt-web"), false);
|
|
});
|
|
|
|
test("createVirtualAutoCombo groups same-provider web sessions behind one logical model", async () => {
|
|
const connA = await providersDb.createProviderConnection({
|
|
provider: "qwen-web",
|
|
authType: "apikey",
|
|
name: "Qwen Web Session A",
|
|
providerSpecificData: { token: "qwen-web-session-token-a" },
|
|
defaultModel: "qwen3-coder-plus",
|
|
});
|
|
const connB = await providersDb.createProviderConnection({
|
|
provider: "qwen-web",
|
|
authType: "apikey",
|
|
name: "Qwen Web Session B",
|
|
providerSpecificData: { token: "qwen-web-session-token-b" },
|
|
defaultModel: "qwen3-coder-plus",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
const qwenWebModel = combo.models.find(
|
|
(model) => model.providerId === "qwen-web" && model.model === "qwen-web/qwen3-coder-plus"
|
|
);
|
|
assert.ok(qwenWebModel, "the provider model should remain in the candidate pool");
|
|
assert.equal(qwenWebModel.connectionId, null);
|
|
assert.deepEqual(
|
|
new Set(qwenWebModel.allowedConnectionIds),
|
|
new Set([connA.id, connB.id]),
|
|
"same-provider web sessions should remain available as account fallbacks"
|
|
);
|
|
assert.equal(
|
|
combo.autoConfig.candidatePool.filter((provider) => provider === "qwen-web").length,
|
|
1,
|
|
"provider pool remains provider-scoped while model entries preserve connection identity"
|
|
);
|
|
});
|
|
|
|
test("createVirtualAutoCombo includes cookie web-session providers with required cookie data", async () => {
|
|
await providersDb.createProviderConnection({
|
|
provider: "chatgpt-web",
|
|
authType: "apikey",
|
|
name: "ChatGPT Web Session",
|
|
providerSpecificData: { cookie: "__Secure-next-auth.session-token=chatgpt-session" },
|
|
defaultModel: "gpt-4o",
|
|
});
|
|
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding");
|
|
|
|
const chatgptWeb = combo.models.find(
|
|
(model) => model.providerId === "chatgpt-web" && model.model === "chatgpt-web/gpt-4o"
|
|
);
|
|
assert.ok(chatgptWeb, "the configured cookie web-session model should be a candidate");
|
|
assert.ok(combo.autoConfig.candidatePool.includes("chatgpt-web"));
|
|
});
|
|
|
|
test("createVirtualAutoCombo includes no-auth OpenCode Free without provider_connections rows", async () => {
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
|
|
|
|
const opencode = combo.models.find((model) => model.providerId === "opencode");
|
|
assert.ok(
|
|
opencode,
|
|
"OpenCode Free should appear in auto/* even when it has no provider_connections row"
|
|
);
|
|
assert.equal(opencode.connectionId, "noauth");
|
|
assert.equal(opencode.model, "oc/big-pickle");
|
|
assert.ok(combo.autoConfig.candidatePool.includes("opencode"));
|
|
});
|
|
|
|
test("createVirtualAutoCombo includes all chat-capable no-auth providers without connections", async () => {
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
|
|
|
|
// Each noAuth provider should have multiple models (not just the first)
|
|
const ddgwModels = combo.models.filter((m) => m.providerId === "duckduckgo-web");
|
|
assert.ok(ddgwModels.length >= 1, "duckduckgo-web should have at least one model");
|
|
assert.ok(
|
|
ddgwModels.every((m) => m.connectionId === "noauth"),
|
|
"all ddgw models should use noauth connection"
|
|
);
|
|
assert.ok(
|
|
ddgwModels.some((m) => m.model.startsWith("ddgw/")),
|
|
"ddgw models should have correct prefix"
|
|
);
|
|
|
|
const tllmModels = combo.models.filter((m) => m.providerId === "theoldllm");
|
|
assert.ok(tllmModels.length >= 1, "theoldllm should have at least one model");
|
|
assert.ok(
|
|
tllmModels.every((m) => m.connectionId === "noauth"),
|
|
"all tllm models should use noauth connection"
|
|
);
|
|
assert.ok(
|
|
tllmModels.some((m) => m.model === "tllm/GPT_5_4"),
|
|
"tllm should include GPT_5_4"
|
|
);
|
|
|
|
const chipotleModels = combo.models.filter((m) => m.providerId === "chipotle");
|
|
assert.ok(chipotleModels.length >= 1, "chipotle should have at least one model");
|
|
assert.ok(
|
|
chipotleModels.every((m) => m.connectionId === "noauth"),
|
|
"all chipotle models should use noauth connection"
|
|
);
|
|
|
|
assert.equal(
|
|
combo.models.some((model) => model.providerId === "veoaifree-web"),
|
|
false,
|
|
"video-only no-auth providers must not be inserted into chat auto-combos"
|
|
);
|
|
});
|
|
|
|
test("createVirtualAutoCombo keeps credential-required providers out when disconnected", async () => {
|
|
const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("fast");
|
|
|
|
assert.equal(
|
|
combo.models.some((model) => model.providerId === "openai"),
|
|
false,
|
|
"OpenAI should still require a real active connection"
|
|
);
|
|
});
|