fix(catalog): keep operator custom models out of the live-sync reader

#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/<id>` (parentless primary) with the registry's canonical
`jina-ai/<id>` 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.
This commit is contained in:
diegosouzapw
2026-09-10 19:12:06 -03:00
parent b3d3d9524c
commit b4616e4316
2 changed files with 40 additions and 10 deletions

View File

@@ -184,10 +184,12 @@ async function unionCustomModels(
*/
async function loadConnectionCatalog(storedProviderId: string): Promise<SyncedAvailableModel[]> {
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<Active
/**
* Return non-empty synced catalogs grouped by provider, restricted to active
* connections. This is the authoritative live source for /v1/models.
*
* Deliberately does NOT union `customModels` the way the dispatch-time readers
* above do (#12597). Its three consumers all read this as "what the provider's
* live sync reported": /v1/models emits synced rows through their own loop and
* has a separate custom-model pass right after it (with the specialty-registry
* dedupe, hidePaid and vision overrides that pass owns); /api/models uses it to
* decide whether an exclusive-listing provider's live catalog suppresses a static
* row; getSyncedAutoAliases derives tier aliases from it. Blurring operator custom
* rows into "synced" made /v1/models emit a custom specialty model under the
* provider's ALIAS as the parentless primary row (`jina/<id>`) and demote the
* embedding/rerank registry's canonical `jina-ai/<id>` to a child — the inverse of
* the identity every other specialty model of that provider carries.
*/
export async function getAllActiveSyncedModels(): Promise<Record<string, SyncedAvailableModel[]>> {
try {
@@ -277,10 +291,7 @@ export async function getAllActiveSyncedModels(): Promise<Record<string, SyncedA
const models = enrichCursorCatalog(
providerId,
await unionCustomModels(
providerId,
collectModelsForConnections(modelsByConnection, connectionIds)
)
collectModelsForConnections(modelsByConnection, connectionIds)
);
if (models.length > 0) {

View File

@@ -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);