diff --git a/open-sse/handlers/autoComboCandidates.ts b/open-sse/handlers/autoComboCandidates.ts index 40c3d9a174..c133032306 100644 --- a/open-sse/handlers/autoComboCandidates.ts +++ b/open-sse/handlers/autoComboCandidates.ts @@ -101,14 +101,12 @@ export async function getAutoComboCandidates( // model string in src/sse/handlers/autoRouting.ts). let virtualCombo; if (channel === "auto") { - const { createVirtualAutoCombo } = await import( - "@omniroute/open-sse/services/autoCombo/virtualFactory.ts" - ); + const { createVirtualAutoCombo } = + await import("@omniroute/open-sse/services/autoCombo/virtualFactory.ts"); virtualCombo = await createVirtualAutoCombo(undefined); } else { - const { createBuiltinAutoCombo } = await import( - "@omniroute/open-sse/services/autoCombo/builtinCatalog.ts" - ); + const { createBuiltinAutoCombo } = + await import("@omniroute/open-sse/services/autoCombo/builtinCatalog.ts"); virtualCombo = await createBuiltinAutoCombo(modelStr, channel); } @@ -116,11 +114,24 @@ export async function getAutoComboCandidates( ? await getExcludedConnectionIds(apiKeyId, modelStr).catch(() => new Set()) : new Set(); - const models: Array<{ providerId: string; connectionId: string; model: string }> = - Array.isArray(virtualCombo?.models) ? virtualCombo.models : []; + const models: Array<{ + providerId: string; + connectionId: string | null; + allowedConnectionIds?: string[]; + model: string; + }> = Array.isArray(virtualCombo?.models) ? virtualCombo.models : []; + // Routing keeps one logical provider/model candidate, but the management API + // remains account-oriented so operators can inspect and toggle each fallback. + const accountCandidates = models.flatMap((candidate) => { + if (candidate.connectionId) return [{ ...candidate, connectionId: candidate.connectionId }]; + return (candidate.allowedConnectionIds ?? []).map((connectionId) => ({ + ...candidate, + connectionId, + })); + }); const candidates = await Promise.all( - models.map(async (candidate) => { + accountCandidates.map(async (candidate) => { const decorated = await decorateCandidate({ provider: candidate.providerId, connectionId: candidate.connectionId, diff --git a/open-sse/services/autoCombo/candidateOverrides.ts b/open-sse/services/autoCombo/candidateOverrides.ts index 9c66f6dc8d..ca4ea547a9 100644 --- a/open-sse/services/autoCombo/candidateOverrides.ts +++ b/open-sse/services/autoCombo/candidateOverrides.ts @@ -10,7 +10,8 @@ */ interface OverridableCandidate { - connectionId: string; + connectionId: string | null; + allowedConnectionIds?: string[]; } /** @@ -24,5 +25,21 @@ export function filterExcludedCandidates( excludedConnectionIds: Set ): T[] { if (!excludedConnectionIds || excludedConnectionIds.size === 0) return pool; - return pool.filter((candidate) => !excludedConnectionIds.has(candidate.connectionId)); + + return pool.flatMap((candidate) => { + if (Array.isArray(candidate.allowedConnectionIds)) { + const allowedConnectionIds = candidate.allowedConnectionIds.filter( + (connectionId) => !excludedConnectionIds.has(connectionId) + ); + if (allowedConnectionIds.length === 0) return []; + if (allowedConnectionIds.length === candidate.allowedConnectionIds.length) { + return [candidate]; + } + return [{ ...candidate, allowedConnectionIds }]; + } + + return candidate.connectionId && excludedConnectionIds.has(candidate.connectionId) + ? [] + : [candidate]; + }); } diff --git a/open-sse/services/autoCombo/virtualFactory.ts b/open-sse/services/autoCombo/virtualFactory.ts index cdf8724373..bd48f8dbdf 100644 --- a/open-sse/services/autoCombo/virtualFactory.ts +++ b/open-sse/services/autoCombo/virtualFactory.ts @@ -51,7 +51,10 @@ type NoAuthProviderDefinition = { export interface VirtualAutoComboCandidate { provider: string; - connectionId: string; + /** A concrete connection for synthetic/no-auth candidates; null for a logical provider/model candidate. */ + connectionId: string | null; + /** Credentialed accounts that are eligible to serve this provider/model pair. */ + allowedConnectionIds?: string[]; model: string; modelStr: string; // e.g., 'openai/gpt-4o' costPer1MTokens: number; // from providerRegistry @@ -64,7 +67,8 @@ type VirtualAutoCombo = AutoComboConfig & { kind: "model"; model: string; providerId: string; - connectionId: string; + connectionId: string | null; + allowedConnectionIds?: string[]; weight: number; label: string; }>; @@ -315,31 +319,53 @@ export async function createVirtualAutoCombo( const validConnections = connections.filter(hasUsableConnectionCredential); const candidatePool: VirtualAutoComboCandidate[] = []; + const registry = getProviderRegistry(); + const connectionsByProvider = new Map(); for (const conn of validConnections) { - // #5873: custom OpenAI-/Anthropic-compatible providers have dynamic connection - // IDs (`*-compatible-*`) that are never keys of the static registry. Do NOT drop - // them from `auto/` routing — only fall back to the registry's first model when - // the connection has no explicit defaultModel. - const providerInfo = getProviderRegistry()[conn.provider]; + const providerConnections = connectionsByProvider.get(conn.provider) ?? []; + providerConnections.push(conn); + connectionsByProvider.set(conn.provider, providerConnections); + } - let modelId: string | undefined = conn.defaultModel; - if (!modelId && providerInfo) { - const firstModel = providerInfo.models[0]; - modelId = firstModel?.id; + // Build one logical candidate per provider/model and keep account fallback as an + // allowlist on that candidate. This avoids both the old "first registry model per + // connection" blind spot and a connections × models Cartesian candidate pool. + for (const [providerId, providerConnections] of connectionsByProvider) { + const providerInfo = registry[providerId]; + const registryModelIds = Array.isArray(providerInfo?.models) + ? providerInfo.models + .map((model) => (typeof model?.id === "string" ? model.id.trim() : "")) + .filter(Boolean) + : []; + const registryModelIdSet = new Set(registryModelIds); + const defaultModelIds = providerConnections + .map((conn) => (typeof conn.defaultModel === "string" ? conn.defaultModel.trim() : "")) + .filter(Boolean); + const modelIds = Array.from(new Set([...registryModelIds, ...defaultModelIds])); + const hiddenModels = hiddenModelsMap.get(providerId); + + for (const modelId of modelIds) { + if (hiddenModels?.has(modelId)) continue; + + const allowedConnectionIds = providerConnections + .filter((conn) => { + if (isModelExcludedByConnection(modelId, conn.providerSpecificData)) return false; + // Registry models are provider-wide. A non-registry default (for a custom + // or passthrough model) is scoped only to connections that selected it. + return registryModelIdSet.has(modelId) || conn.defaultModel?.trim() === modelId; + }) + .map((conn) => conn.id); + if (allowedConnectionIds.length === 0) continue; + + candidatePool.push({ + provider: providerId, + connectionId: null, + allowedConnectionIds, + model: modelId, + modelStr: `${providerId}/${modelId}`, + costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness) + }); } - if (!modelId) continue; // Skip providers without a resolvable model - - // Skip models that the user has hidden in the dashboard - const hiddenModels = hiddenModelsMap.get(conn.provider); - if (hiddenModels?.has(modelId)) continue; - - candidatePool.push({ - provider: conn.provider, - connectionId: conn.id, - model: modelId, - modelStr: `${conn.provider}/${modelId}`, - costPer1MTokens: 0, // Not used in virtual auto-combo (LKGP uses session stickiness) - }); } candidatePool.push( @@ -520,6 +546,9 @@ export async function createVirtualAutoCombo( model: candidate.modelStr, providerId: candidate.provider, connectionId: candidate.connectionId, + ...(candidate.allowedConnectionIds + ? { allowedConnectionIds: candidate.allowedConnectionIds } + : {}), weight: 1, label: candidate.provider, })); diff --git a/tests/unit/auto-candidate-overrides-regression-7819.test.ts b/tests/unit/auto-candidate-overrides-regression-7819.test.ts index 2d36a69ccc..ca1ba0c5ce 100644 --- a/tests/unit/auto-candidate-overrides-regression-7819.test.ts +++ b/tests/unit/auto-candidate-overrides-regression-7819.test.ts @@ -63,8 +63,14 @@ async function seedTwoGlmConnections() { return { first, second }; } -function connectionIds(combo: { models: Array<{ connectionId: string }> }) { - return combo.models.map((m) => m.connectionId).sort(); +function connectionIds(combo: { + models: Array<{ connectionId: string | null; allowedConnectionIds?: string[] }>; +}) { + return combo.models + .flatMap((model) => + model.connectionId ? [model.connectionId] : (model.allowedConnectionIds ?? []) + ) + .sort(); } test("#7819 regression: pool is IDENTICAL whether or not apiKeyId/autoChannel are passed, with no overrides configured", async () => { @@ -131,12 +137,7 @@ test("#7819: fail-open — a DB lookup failure never breaks routing (pool falls const db = core.getDbInstance(); db.exec("DROP TABLE auto_candidate_overrides"); - const combo = await virtualFactory.createVirtualAutoCombo( - undefined, - undefined, - "key-1", - "auto" - ); + const combo = await virtualFactory.createVirtualAutoCombo(undefined, undefined, "key-1", "auto"); const ids = connectionIds(combo); assert.ok(ids.includes(first.id)); assert.ok(ids.includes(second.id)); diff --git a/tests/unit/auto-candidate-pool-filter-7819.test.ts b/tests/unit/auto-candidate-pool-filter-7819.test.ts index 7947055c35..8b42b66e6f 100644 --- a/tests/unit/auto-candidate-pool-filter-7819.test.ts +++ b/tests/unit/auto-candidate-pool-filter-7819.test.ts @@ -38,3 +38,35 @@ test("#7819: excluding every candidate yields an empty pool (caller's empty-pool const result = filterExcludedCandidates(pool, new Set(["conn-a", "conn-b", "conn-c"])); assert.equal(result.length, 0); }); + +test("#7819: logical candidates retain only non-excluded account fallbacks", () => { + const logicalPool = [ + { + connectionId: null, + allowedConnectionIds: ["conn-a", "conn-b"], + model: "claude", + }, + ]; + + const result = filterExcludedCandidates(logicalPool, new Set(["conn-a"])); + assert.deepEqual(result, [ + { + connectionId: null, + allowedConnectionIds: ["conn-b"], + model: "claude", + }, + ]); +}); + +test("#7819: logical candidates are removed when every account fallback is excluded", () => { + const logicalPool = [ + { + connectionId: null, + allowedConnectionIds: ["conn-a", "conn-b"], + model: "claude", + }, + ]; + + const result = filterExcludedCandidates(logicalPool, new Set(["conn-a", "conn-b"])); + assert.deepEqual(result, []); +}); diff --git a/tests/unit/auto-combo-credentialed-model-pool.test.ts b/tests/unit/auto-combo-credentialed-model-pool.test.ts new file mode 100644 index 0000000000..1515b6a9b9 --- /dev/null +++ b/tests/unit/auto-combo-credentialed-model-pool.test.ts @@ -0,0 +1,157 @@ +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-auto-model-pool-")); +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 modelsDb = await import("../../src/lib/db/models.ts"); +const virtualFactory = await import("../../open-sse/services/autoCombo/virtualFactory.ts"); +const candidateHandler = await import("../../open-sse/handlers/autoComboCandidates.ts"); + +type VirtualComboResult = Awaited>; +type LogicalCandidate = { + providerId: string; + model: string; + connectionId: string | null; + allowedConnectionIds?: string[]; +}; + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); +} + +function antigravityCandidates(combo: VirtualComboResult): LogicalCandidate[] { + return (combo.models as unknown as LogicalCandidate[]).filter( + (candidate) => candidate.providerId === "antigravity" + ); +} + +async function seedConnections(firstExcludedModels?: string[]) { + const tokenExpiresAt = new Date(Date.now() + 60_000).toISOString(); + const first = await providersDb.createProviderConnection({ + provider: "antigravity", + authType: "oauth", + email: "antigravity-one@example.com", + accessToken: "fake-antigravity-access-token-one", + tokenExpiresAt, + ...(firstExcludedModels + ? { providerSpecificData: { excludedModels: firstExcludedModels } } + : {}), + }); + const second = await providersDb.createProviderConnection({ + provider: "antigravity", + authType: "oauth", + email: "antigravity-two@example.com", + accessToken: "fake-antigravity-access-token-two", + tokenExpiresAt, + }); + return { first, second }; +} + +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("credentialed providers expose one logical candidate per visible registry model", async () => { + const { first, second } = await seedConnections(); + + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + const candidates = antigravityCandidates(combo); + const modelStrings = candidates.map((candidate) => candidate.model); + const expectedConnectionIds = [first.id, second.id].sort(); + + assert.equal( + new Set(modelStrings).size, + modelStrings.length, + "provider/model candidates must not be duplicated per connection" + ); + for (const model of [ + "antigravity/claude-sonnet-5", + "antigravity/gemini-3.5-flash-low", + "antigravity/gemini-3.5-flash-medium", + "antigravity/gemini-3.5-flash-high", + ]) { + assert.ok(modelStrings.includes(model), `${model} should be eligible for auto routing`); + } + + for (const candidate of candidates) { + assert.equal(candidate.connectionId, null, "logical candidates must not pin one account"); + assert.deepEqual( + [...(candidate.allowedConnectionIds ?? [])].sort(), + expectedConnectionIds, + `${candidate.model} should share the provider's eligible account pool` + ); + } +}); + +test("candidate transparency expands a logical model into per-account rows", async () => { + const { first, second } = await seedConnections(); + + const result = await candidateHandler.getAutoComboCandidates("auto", null); + const sonnetRows = result.candidates.filter( + (candidate) => + candidate.provider === "antigravity" && candidate.model === "antigravity/claude-sonnet-5" + ); + + assert.deepEqual( + new Set(sonnetRows.map((candidate) => candidate.connectionId)), + new Set([first.id, second.id]), + "the management view should retain one row per account fallback" + ); +}); + +test("connection model exclusions narrow only that model's account allowlist", async () => { + const { first, second } = await seedConnections(["gemini-3.5-*"]); + + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + const candidates = antigravityCandidates(combo); + const geminiCandidates = candidates.filter((candidate) => + candidate.model.startsWith("antigravity/gemini-3.5-") + ); + + assert.ok(geminiCandidates.length >= 3, "Gemini 3.5 candidates should remain available"); + for (const candidate of geminiCandidates) { + assert.deepEqual(candidate.allowedConnectionIds, [second.id]); + } + + const sonnet = candidates.find((candidate) => candidate.model === "antigravity/claude-sonnet-5"); + assert.ok(sonnet, "Claude Sonnet 5 should remain in the candidate pool"); + assert.deepEqual([...(sonnet.allowedConnectionIds ?? [])].sort(), [first.id, second.id].sort()); +}); + +test("hiding the first registry model does not drop the credentialed provider", async () => { + await seedConnections(); + modelsDb.setModelIsHidden("antigravity", "claude-sonnet-5", true); + + const combo = await virtualFactory.createVirtualAutoCombo(undefined); + const modelStrings = antigravityCandidates(combo).map((candidate) => candidate.model); + + assert.equal(modelStrings.includes("antigravity/claude-sonnet-5"), false); + for (const model of [ + "antigravity/gemini-3.5-flash-low", + "antigravity/gemini-3.5-flash-medium", + "antigravity/gemini-3.5-flash-high", + ]) { + assert.ok(modelStrings.includes(model), `${model} should remain after Sonnet is hidden`); + } +}); diff --git a/tests/unit/autoCombo/provider-family-combos.test.ts b/tests/unit/autoCombo/provider-family-combos.test.ts index 27a30d8e4f..393b543a09 100644 --- a/tests/unit/autoCombo/provider-family-combos.test.ts +++ b/tests/unit/autoCombo/provider-family-combos.test.ts @@ -122,14 +122,26 @@ describe("auto/ materialization (#6453)", () => { assert.equal(combo.id, "auto/glm"); assert.equal(combo.strategy, "auto"); - const providerIds = combo.models.map((m) => m.providerId).sort(); + // Dedupe to the provider SET: since #7928 the candidate pool is a + // connections × models Cartesian product, so a provider that serves several + // glm-5.2-bearing models now contributes one candidate per model rather than + // exactly one row. The #6453 invariant is which providers span the family, + // not the per-provider candidate count. + const providerIds = [...new Set(combo.models.map((m) => m.providerId))].sort(); // Includes the always-on `auggie` no-auth candidate: its registry (v0.32.0 CLI // model ids) advertises a literal "glm-5.2" model, and — same as the // "degrades gracefully" test below documents for opencode/minimax — a // no-auth backend that genuinely serves a family model IS a legitimate // member of the family pool, not just credentialed provider_connections rows. assert.deepEqual(providerIds, ["auggie", "glm", "zai"]); - assert.ok(combo.models.every((m) => m.model.endsWith("glm-5.2"))); + // Every candidate must be a glm-family model (the Cartesian pool now surfaces + // each backend's full glm line-up, not only the glm-5.2 default), and the + // connected openai/gpt-4o-mini backend must be excluded — same family + // invariant the "degrades gracefully" case asserts for auto/minimax. + assert.ok( + combo.models.every((m) => detectModelFamily(m.model) === "glm"), + "every auto/glm candidate must be a glm-family model, never the connected openai one" + ); }); it("resolves auto/zai to ONLY the zai-provider connection (provider-override family)", async () => { @@ -152,7 +164,15 @@ describe("auto/ materialization (#6453)", () => { assert.equal(combo.id, "auto/zai"); const providerIds = combo.models.map((m) => m.providerId); - assert.deepEqual(providerIds, ["zai"]); + // The provider-override invariant: auto/zai must include ONLY the zai + // provider and exclude the connected-but-unrelated glm connection. Since + // #7928's Cartesian pool, zai contributes one candidate per zai model, so + // assert the set is exactly {zai} rather than a single row. + assert.ok(providerIds.length > 0, "auto/zai must materialize at least one zai candidate"); + assert.ok( + providerIds.every((p) => p === "zai"), + "auto/zai must include ONLY zai-provider models, never the connected glm connection" + ); }); it("degrades gracefully to the family subset, excluding connected-but-unrelated providers", async () => { diff --git a/tests/unit/virtual-auto-combo.test.ts b/tests/unit/virtual-auto-combo.test.ts index 598c8f502f..6383613097 100644 --- a/tests/unit/virtual-auto-combo.test.ts +++ b/tests/unit/virtual-auto-combo.test.ts @@ -49,9 +49,11 @@ test("createVirtualAutoCombo returns an executable auto combo for API-key connec assert.equal(combo.strategy, "auto"); assert.ok(combo.models.length >= 1); - assert.equal(combo.models[0].kind, "model"); - assert.equal(combo.models[0].model, "openai/gpt-4o-mini"); - assert.equal(combo.models[0].providerId, "openai"); + 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")); }); @@ -70,7 +72,12 @@ test("createVirtualAutoCombo includes OAuth accessToken connections with real ex assert.equal(combo.strategy, "auto"); assert.ok(combo.models.length >= 1); - assert.equal(combo.models[0].model, "anthropic/claude-sonnet-4-5"); + 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")); }); @@ -85,9 +92,10 @@ test("createVirtualAutoCombo includes configured web-session providers without a const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding"); - const qwenWeb = combo.models.find((model) => model.providerId === "qwen-web"); - assert.ok(qwenWeb, "configured web-session providers should be auto-combo candidates"); - assert.equal(qwenWeb.model, "qwen-web/qwen3-coder-plus"); + 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")); }); @@ -129,7 +137,7 @@ test("createVirtualAutoCombo excludes web-session providers with irrelevant prov assert.equal(combo.autoConfig.candidatePool.includes("chatgpt-web"), false); }); -test("createVirtualAutoCombo preserves multiple same-provider web-session candidates", async () => { +test("createVirtualAutoCombo groups same-provider web sessions behind one logical model", async () => { const connA = await providersDb.createProviderConnection({ provider: "qwen-web", authType: "apikey", @@ -147,18 +155,16 @@ test("createVirtualAutoCombo preserves multiple same-provider web-session candid const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding"); - const qwenWebModels = combo.models.filter((model) => model.providerId === "qwen-web"); - assert.equal( - qwenWebModels.length, - 2, - "same-provider web sessions must not collapse to one target" + 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(qwenWebModels.map((model) => model.connectionId)), + new Set(qwenWebModel.allowedConnectionIds), new Set([connA.id, connB.id]), - "same-provider web sessions should map back to their exact provider_connection rows" + "same-provider web sessions should remain available as account fallbacks" ); - assert.ok(qwenWebModels.every((model) => model.model === "qwen-web/qwen3-coder-plus")); assert.equal( combo.autoConfig.candidatePool.filter((provider) => provider === "qwen-web").length, 1, @@ -177,12 +183,10 @@ test("createVirtualAutoCombo includes cookie web-session providers with required const combo: VirtualComboResult = await virtualFactory.createVirtualAutoCombo("coding"); - const chatgptWeb = combo.models.find((model) => model.providerId === "chatgpt-web"); - assert.ok( - chatgptWeb, - "cookie web-session providers with required cookie data should be candidates" + const chatgptWeb = combo.models.find( + (model) => model.providerId === "chatgpt-web" && model.model === "chatgpt-web/gpt-4o" ); - assert.equal(chatgptWeb.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")); }); @@ -205,17 +209,32 @@ test("createVirtualAutoCombo includes all chat-capable no-auth providers without // 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"); + 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"); + 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.ok( + chipotleModels.every((m) => m.connectionId === "noauth"), + "all chipotle models should use noauth connection" + ); assert.equal( combo.models.some((model) => model.providerId === "veoaifree-web"),