@@ -6,9 +6,9 @@ import {
getAllCustomModels ,
getSettings ,
getCachedProviderNodes ,
getModelIsHidden ,
getModelAliases ,
getDatabaseSettings ,
getHiddenModelsByProvider ,
} from "@/lib/localDb" ;
import { createLazyConnectionView } from "@/lib/db/providers/lazyConnectionView" ;
import { extractAliasBackedModels } from "./aliasBackedModels" ;
@@ -133,7 +133,7 @@ export {
} from "./catalogCache" ;
export type { CachedCatalog } from "./catalogCache" ;
const BUILTIN_AUTO_YIELD_INTERVAL = 2 ;
const BUILTIN_AUTO_YIELD_INTERVAL = 8 ;
function yieldCatalogBuildTurn ( ) : Promise < void > {
return new Promise ( ( resolve ) = > setImmediate ( resolve ) ) ;
@@ -157,8 +157,6 @@ export async function getUnifiedModelsResponse(
try {
settingsForAuth = await getSettings ( ) ;
} catch { }
// #9147: yield before auth check to allow event loop tick
await yieldCatalogBuildTurn ( ) ;
const authRejection = await getModelCatalogAuthRejection ( request , settingsForAuth , {
. . . corsHeaders ,
. . . diagnosticHeaders ,
@@ -229,34 +227,7 @@ async function buildUnifiedModelsResponseCore(
corsHeaders : Record < string , string > = { }
) {
const diagnosticHeaders = getCatalogDiagnosticsHeaders ( { request } ) ;
// #9147: this builder walks connections + model registries at catalog scale with no
// event-loop yield, so a large deployment pins the single Node.js thread for the
// whole build (reporter: 183 connections / 2000+ models → 10.1s stall that blocks the
// dashboard WS heartbeat). Yield every `catYIELD_EVERY` items across the hot loops.
const catYIELD_EVERY = 20 ;
let catYieldCount = 0 ;
const maybeYieldCatalogBuild = async ( ) : Promise < void > = > {
catYieldCount ++ ;
if ( catYieldCount % catYIELD_EVERY === 0 ) {
await yieldCatalogBuildTurn ( ) ;
}
} ;
try {
// #9147: `getModelIsHidden()` is a SQLite read per call (custom row + compat list)
// and the build consults it ~16× per entry. Bulk-load the hidden-model map once
// (one query — `getHiddenModelsByProvider`) and resolve from memory for the whole
// build. A provider absent from the map has no hidden models at all — `false`,
// no on-demand fallback (that would reintroduce the per-call SQLite reads).
// Deliberately kept INSIDE this try block (not hoisted above it): the builder's
// own catch below is what converts a build-time failure into a sanitized 500
// Response instead of a rejected promise — hoisting this bulk read above the
// try would let a crash here propagate as an unhandled rejection instead
// (catalogCache.ts's in-flight coalescing does not fully consume rejections).
const hiddenModelsByProvider = getHiddenModelsByProvider ( ) ;
const isModelHiddenBulk = ( providerId : string , modelId : string ) : boolean = > {
const hiddenSet = hiddenModelsByProvider . get ( providerId ) ;
return hiddenSet ? hiddenSet . has ( modelId ) : false ;
} ;
let settings : Record < string , any > = { } ;
try {
settings = await getSettings ( ) ;
@@ -267,10 +238,6 @@ async function buildUnifiedModelsResponseCore(
. . . diagnosticHeaders ,
} ) ;
if ( authRejection ) return authRejection ;
// #9147: yield after auth check before DB initialization prologue
await yieldCatalogBuildTurn ( ) ;
const { aliasToProviderId , providerIdToAlias } = buildAliasMaps ( ) ;
const _qp = new URL ( request . url ) . searchParams . get ( "prefix" ) ;
const prefixMode =
@@ -355,7 +322,6 @@ async function buildUnifiedModelsResponseCore(
// Get combos
let combos = [ ] ;
await yieldCatalogBuildTurn ( ) ;
try {
combos = await getCombos ( ) ;
} catch ( e ) {
@@ -389,16 +355,7 @@ 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 < string , typeof connections > ( ) ;
const getConnectionsForProvider = ( . . . keys : Array < string | null | undefined > ) = > {
const cacheKey = keys . filter ( ( k ) : k is string = > Boolean ( k ) ) . sort ( ) . join ( "