From 2e2301a5e4578ffdace56b86bffaeb2153c35590 Mon Sep 17 00:00:00 2001 From: adevwithpurpose Date: Tue, 18 Aug 2026 12:27:20 -0300 Subject: [PATCH] perf(api): memoize getConnectionsForProvider in catalog builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Combining this PR's own bulk hidden-model optimization with the already-merged isExcludedByProviderConnections() check (from a different PR) reintroduced an O(connections) scan per model inside the catalog builder's hot loop, regressing the exact single-stretch event-loop budget tests/unit/9147-catalog-eventloop-yield.test.ts enforces (was passing on this PR's own commit before the merge). Memoizing getConnectionsForProvider() by its (unordered) key-set substantially reduces the redundant per-model connection scans (measured ~497ms -> ~210-300ms worst single stretch across repeated runs), but does NOT fully close the gap to the 150ms budget — still red. Committing this as a real, safe improvement; flagging for further investigation (likely getConnectionsForProvider's first-call cost per provider, or hasEligibleConnectionForModel) before this PR merges. NOT deciding to relax the test threshold myself. --- src/app/api/v1/models/catalog.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 8f2121dec8..e086cf1870 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -389,7 +389,16 @@ async function buildUnifiedModelsResponseCore( if ("alias" in p && typeof p.alias === "string") activeAliases.add(p.alias); } + // #9147 follow-up: this is called ~1-3x per model at catalog scale (providerSupportsModel, + // isExcludedByProviderConnections). Connections do not change mid-build, so memoize per + // unique (unordered) key-set instead of rescanning connectionsByProvider on every call — + // otherwise the O(models) hot loop regains an O(connections) cost per model and blows the + // single-stretch event-loop budget this file's own yield mechanism is meant to protect. + const connectionsForProviderCache = new Map(); const getConnectionsForProvider = (...keys: Array) => { + const cacheKey = keys.filter((k): k is string => Boolean(k)).sort().join(""); + const cached = connectionsForProviderCache.get(cacheKey); + if (cached) return cached; const seen = new Set(); const collected: typeof connections = []; for (const key of keys) { @@ -400,6 +409,7 @@ async function buildUnifiedModelsResponseCore( collected.push(connection); } } + connectionsForProviderCache.set(cacheKey, collected); return collected; };