From b4616e4316c8607de17b20ebc0debc2a1a733b63 Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Thu, 10 Sep 2026 19:12:06 -0300 Subject: [PATCH] fix(catalog): keep operator custom models out of the live-sync reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #12934 unioned customModels into getAllActiveSyncedModels() alongside the dispatch-time readers it was actually fixing (#12597: getActiveSyncedCatalog, reconcileProvidersWithActiveSyncedCatalog, getActiveProvidersWithSyncedModel). getAllActiveSyncedModels() is not a dispatch reader. Its three consumers read it as "what the provider's live sync reported": /v1/models feeds its synced-emission loop (and has a separate custom-model pass right after, which owns the specialty-registry dedupe, hidePaid and the vision overrides), /api/models uses it to decide whether an exclusive-listing provider suppresses a static row, and getSyncedAutoAliases derives tier aliases from it. Blurring custom rows into "synced" made a custom embedding/rerank model on jina-ai come out as the alias row `jina/` (parentless primary) with the registry's canonical `jina-ai/` demoted to its child — the inverse of the identity every other specialty model of that provider carries, and it also dropped the registry's `dimensions`. Drop the union there only; the dispatch trio keeps it, so #12597's tests and the 400-on-picker-model fix are untouched. Locked by a new case in custom-models-live-catalog-12597.test.ts and by both jina cases in models-catalog-route.test.ts ("does not duplicate imported/custom Jina specialty models"), which encode the two identities side by side. --- src/lib/db/models/activeSyncedCatalog.ts | 27 +++++++++++++------ .../custom-models-live-catalog-12597.test.ts | 23 ++++++++++++++-- 2 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/lib/db/models/activeSyncedCatalog.ts b/src/lib/db/models/activeSyncedCatalog.ts index 4e68a17d2b..9c34f685dd 100644 --- a/src/lib/db/models/activeSyncedCatalog.ts +++ b/src/lib/db/models/activeSyncedCatalog.ts @@ -184,10 +184,12 @@ async function unionCustomModels( */ async function loadConnectionCatalog(storedProviderId: string): Promise { const [connections, modelsByConnection] = await Promise.all([ - getRawProviderConnections({ provider: storedProviderId, isActive: true }, undefined, undefined, [ - "id", - "provider", - ]), + getRawProviderConnections( + { provider: storedProviderId, isActive: true }, + undefined, + undefined, + ["id", "provider"] + ), getSyncedAvailableModelsByConnection(storedProviderId), ]); @@ -248,6 +250,18 @@ export async function getActiveSyncedCatalog(providerId: string): Promise`) and demote the + * embedding/rerank registry's canonical `jina-ai/` to a child — the inverse of + * the identity every other specialty model of that provider carries. */ export async function getAllActiveSyncedModels(): Promise> { try { @@ -277,10 +291,7 @@ export async function getAllActiveSyncedModels(): Promise 0) { diff --git a/tests/unit/custom-models-live-catalog-12597.test.ts b/tests/unit/custom-models-live-catalog-12597.test.ts index 4e6bb35068..b81745aeb9 100644 --- a/tests/unit/custom-models-live-catalog-12597.test.ts +++ b/tests/unit/custom-models-live-catalog-12597.test.ts @@ -14,10 +14,14 @@ process.env.DATA_DIR = TEST_DATA_DIR; process.env.API_KEY_SECRET = process.env.API_KEY_SECRET || "custom-live-12597-test-secret"; const core = await import("../../src/lib/db/core.ts"); -const { addCustomModel, replaceSyncedAvailableModelsForConnection, getActiveProvidersWithSyncedModel } = - await import("../../src/lib/db/models.ts"); +const { + addCustomModel, + replaceSyncedAvailableModelsForConnection, + getActiveProvidersWithSyncedModel, +} = await import("../../src/lib/db/models.ts"); const { getActiveSyncedCatalog, + getAllActiveSyncedModels, catalogContainsModel, reconcileProvidersWithActiveSyncedCatalog, } = await import("../../src/lib/db/models/activeSyncedCatalog.ts"); @@ -124,6 +128,21 @@ test("#12597 sparse custom overlay does not wipe synced capability fields", asyn assert.deepEqual(match?.supportedThinkingEfforts, ["low", "high"]); }); +test("#12597 the union is dispatch-only — getAllActiveSyncedModels stays live-sync-shaped", async () => { + await addCustomModel(PROVIDER, PICKER_MODEL, "DeepSeek R1 via picker"); + + const allActive = await getAllActiveSyncedModels(); + const ids = (allActive[PROVIDER] ?? []).map((model) => model.id); + + assert.deepEqual( + ids, + [SYNCED_MODEL], + "getAllActiveSyncedModels reports what the provider's live sync returned; its consumers (/v1/models' synced loop, /api/models' exclusive-listing suppression, getSyncedAutoAliases) each handle custom rows on their own" + ); + // The dispatch-time readers still see it — that is what #12597 fixed. + assert.equal(catalogContainsModel(await getActiveSyncedCatalog(PROVIDER), PICKER_MODEL), true); +}); + test("#12597 without a custom row the picker id is still absent (lock the old contract)", async () => { const catalog = await getActiveSyncedCatalog(PROVIDER); assert.equal(catalogContainsModel(catalog, PICKER_MODEL), false);