From a9b4c3efff7115070001ebad02a1bcc47cbd8d21 Mon Sep 17 00:00:00 2001 From: Rahul sharma Date: Thu, 6 Aug 2026 08:38:59 +0530 Subject: [PATCH] fix(api): alias-backed models leak raw node UUID prefix in /v1/models (#8958) (#8961) Validated in local merge-train T5 (base49+contributors+pacocartones) --- src/app/api/v1/models/catalog.ts | 12 +- .../8958-alias-backed-node-prefix.test.ts | 144 ++++++++++++++++++ 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 tests/unit/8958-alias-backed-node-prefix.test.ts diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 1e0595a42f..8dc554f056 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -1328,7 +1328,16 @@ async function buildUnifiedModelsResponseCore( continue; } - const alias = providerIdToAlias[canonicalProviderId] || providerKey; + // #8958: honor the compatible-provider node prefix (as the synced/custom + // loops do) so an alias-backed entry publishes `prefix/model` instead of the + // raw provider-node UUID. Without the providerIdToPrefix lookup, `alias` fell + // through to `providerKey` (the UUID) and the dedupe below — which only checks + // `alias/model` and `providerKey/model`, both UUID-prefixed — never matched the + // correct `prefix/model` row already emitted, leaking a duplicate UUID entry + // even under MODELS_CATALOG_PREFIX_MODE=alias. + const nodePrefix = + providerIdToPrefix[providerKey] || providerIdToPrefix[canonicalProviderId]; + const alias = nodePrefix || providerIdToAlias[canonicalProviderId] || providerKey; if ( !activeAliases.has(alias) && !activeAliases.has(canonicalProviderId) && @@ -1370,6 +1379,7 @@ async function buildUnifiedModelsResponseCore( if ( includeCanonical && canonicalProviderId !== alias && + !nodePrefix && !isNoAuthProviderKey(canonicalProviderId) && prefixRoutesToProvider(canonicalProviderId, canonicalProviderId) ) { diff --git a/tests/unit/8958-alias-backed-node-prefix.test.ts b/tests/unit/8958-alias-backed-node-prefix.test.ts new file mode 100644 index 0000000000..f823d7dd62 --- /dev/null +++ b/tests/unit/8958-alias-backed-node-prefix.test.ts @@ -0,0 +1,144 @@ +/** + * Regression test for #8958 — /v1/models listed alias-backed models for a + * compatible provider node (openai-compatible / anthropic-compatible, whose id is a + * UUID) TWICE: once with the correct `prefix/model` id and once with the raw + * `/model` id. The duplicate UUID-prefixed entry appeared even under + * MODELS_CATALOG_PREFIX_MODE=alias, which should only ever emit alias ids. + * + * Root cause: the alias-backed loop in `catalog.ts` built its display prefix as + * `providerIdToAlias[canonicalProviderId] || providerKey` and never consulted + * `providerIdToPrefix` (unlike the synced-models / custom-models loops). For a + * compatible node the alias fell through to the raw UUID `providerKey`, and the + * dedupe (which only checks `alias/model` and `providerKey/model`, both + * UUID-prefixed) never matched the correct `prefix/model` row already emitted. + * + * Fix: `const alias = providerIdToPrefix[providerKey] || providerIdToAlias[...] || + * providerKey;` — the id then collapses to `prefix/model` and the existing dedupe + * skips the duplicate. + */ +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-8958-")); +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 aliasesDb = await import("../../src/lib/db/models/aliases.ts"); +const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); + +const NODE_ID = "openai-compatible-chat-550e8400-e29b-41d4-a716-446655440000"; +const UUID_SHAPE_RE = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i; +const CONFIGURED_PREFIX = "fta"; +const MODEL_ID = "opc/big-pickle"; + +async function resetStorage() { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); + fs.mkdirSync(TEST_DATA_DIR, { recursive: true }); + v1ModelsCatalog.__resetCatalogBuilderRunsForTest(); +} + +async function seedCompatibleNodeWithAlias() { + await providersDb.createProviderNode({ + id: NODE_ID, + type: "openai-compatible", + name: "fta (probe)", + prefix: CONFIGURED_PREFIX, + baseUrl: "https://proxy.example.com", + chatPath: "/v1/chat/completions", + modelsPath: "/v1/models", + }); + const connection = await providersDb.createProviderConnection({ + provider: NODE_ID, + authType: "apikey", + name: "fta-conn", + apiKey: "sk-test", + isActive: true, + testStatus: "active", + providerSpecificData: { + baseUrl: "https://proxy.example.com", + chatPath: "/v1/chat/completions", + modelsPath: "/v1/models", + }, + }); + + // Synced entry — produces the correct `fta/opc/big-pickle` row. + await modelsDb.replaceSyncedAvailableModelsForConnection( + NODE_ID, + (connection as { id: string }).id, + [{ id: MODEL_ID, name: "Big Pickle", source: "imported", supportedEndpoints: ["chat"] }] + ); + + // Alias row pointing at the node UUID — as combos/imports register unprefixed + // shortcuts. This is what triggered the duplicate `/model` entry. + await aliasesDb.setModelAlias("big-pickle", `${NODE_ID}/${MODEL_ID}`); +} + +test.beforeEach(async () => { + await resetStorage(); +}); + +test.after(async () => { + core.resetDbInstance(); + fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true }); +}); + +test("#8958: alias-backed model on a compatible node is not duplicated under the raw UUID prefix (alias mode)", async () => { + await seedCompatibleNodeWithAlias(); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models?prefix=alias") + ); + const body = (await response.json()) as { data: Array> }; + assert.equal(response.status, 200); + + const ids = body.data.map((m) => m.id) as string[]; + + // The correct prefixed id is present exactly once. + assert.equal( + ids.filter((id) => id === `${CONFIGURED_PREFIX}/${MODEL_ID}`).length, + 1, + `expected exactly one "${CONFIGURED_PREFIX}/${MODEL_ID}" in ${JSON.stringify(ids)}` + ); + + // No id leaks the raw provider-node UUID. + for (const id of ids) { + assert.equal( + id.startsWith(`${NODE_ID}/`), + false, + `id "${id}" must not be prefixed with the raw provider-node UUID` + ); + assert.equal( + UUID_SHAPE_RE.test(id.split("/")[0]), + false, + `id "${id}" must not carry a UUID-shaped provider prefix` + ); + } +}); + +test("#8958: alias-backed duplicate is absent in default and dual modes too", async () => { + await seedCompatibleNodeWithAlias(); + + for (const query of ["", "?prefix=dual"]) { + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request(`http://localhost/api/v1/models${query}`) + ); + const body = (await response.json()) as { data: Array> }; + const ids = body.data.map((m) => m.id) as string[]; + + assert.ok( + ids.includes(`${CONFIGURED_PREFIX}/${MODEL_ID}`), + `mode "${query || "default"}": expected "${CONFIGURED_PREFIX}/${MODEL_ID}"` + ); + assert.equal( + ids.some((id) => id.startsWith(`${NODE_ID}/`)), + false, + `mode "${query || "default"}": no id may carry the raw provider-node UUID prefix` + ); + } +});