Skip to content

fix(api): keep catalog builds responsive and hash cache keys (#9147, #10313) - #10538

Merged
diegosouzapw merged 5 commits into
release/v3.8.50from
fix/9147-10313-catalog-cache
Aug 19, 2026
Merged

diegosouzapw merged 5 commits into
release/v3.8.50from
fix/9147-10313-catalog-cache

Conversation

@diegosouzapw

@diegosouzapw diegosouzapw commented Aug 16, 2026 •

Copy link
Copy Markdown
Owner

Cluster fix: catalog build at scale (#9147) + cache-key secret leak (#10313)

Two independent catalog defects on the same /v1/models path, fixed together
because they touch overlapping code in the build/enrichment tail.

#9147 — catalog build pins the single Node.js event loop at scale

Reprodução (Hard Rule #18, TDD): novo teste tests/unit/9147-catalog-eventloop-yield.test.ts
seed 60 conexões / 720 synced models e mede o maior gap entre ticks de setTimeout(0)
num build frio do /v1/models. Antes da correção o build prendia o event loop
(multi-segundos de bloqueio contínuo) em prejuízo do dashboard LiveWS heartbeat;
depois, todo o build cede ao event loop a cada fração.

Causa-raiz: o builder getUnifiedModelsResponse → buildUnifiedModelsResponseCore
(src/app/api/v1/models/catalog.ts) e o tail de enrichment
(src/app/api/v1/models/catalogResponse.ts) rodavam como um único turno síncrono
no App Router single-thread: sem await/yield em nenhum dos loops do walk de
conexões + registries de modelos + combos + aliases + synced/custom, e com N+1
queries SQLite por entry (getModelIsHidden, getModelCapabilityOverride,
getResolvedModelContextOverride).

Correção:

  • Yields periódicos (setImmediate) em todos os loops quentes do builder
    (combos, static, codex, synced, openrouter, custom, fallback) e no enrichment
    map + post-filter passes — o event loop respira mesmo em catálogos de milhares
    de entries.
  • Bulk-load das tabelas de override/hidden uma vez por build
    (getHiddenModelsByProvider no catalog.ts; snapshot
    createModelCapabilityResolutionSnapshot no enrichment, [v3.8.50] fix(models): keep model catalogs responsive #9199) eliminando as
    ~N queries SQLite síncronas por entry.
  • applyCatalogPostFilters agora async, com yields entre os passes variantes.

#10313 — raw API key no Map key do catalog cache

Reprodução (TDD): tests/unit/10313-catalog-cache-key-hashing.test.ts captura
todas as chaves de Map.prototype.set durante um build e assere que (a) nenhuma
chave contém a bearer secret bruta, (b) a chave embute o digest sha256 da key,
(c) keys iguais particionam/colidem corretamente.

Causa-raiz: buildCatalogCacheKey() usava o apiKey bruto literal no key do
Map de cache — o segredo vazava para um heap dump / sessão --inspect /
log de debug. Correção: hash sha256 da key (idioma estabelecido hashKey do
apiKeys.ts; não é password hash, key é token de alta entropia).

Validação

  • Regressão fix(api): hash API keys in the model-catalog cache key #10313 + functional-gateway catalog tests: 5/5 verdes.
  • npm run test:vitest: 362/362 offline.
  • npm run typecheck:core: limpo.
  • npm run lint: limpo nos arquivos alterados.
  • npm run check:cycles: sem ciclos novos.
  • Testes de catálogo relacionados (catalog-order, hide-auto-no-think, synced-static,
    opencode-zen, functional-gateway, pricing-*): verdes.
  • npm run test:unit: executado sob carga concorrente do devbox; terminou com 31.091
    passes, 24 falhas e 22 skips. O run incluiu falhas preexistentes/ambientais em múltiplas
    áreas e o teste de timing fix(providers): Overload occurs when there are too many connections/models.  #9147 foi sensível à saturação; o PR não deve ser considerado
    merge-ready até a confirmação isolada em ambiente sem essa concorrência.

⚠️ base-red inherited: #9985

Comment thread src/app/api/v1/models/catalogCache.ts Fixed
@diegosouzapw

Copy link
Copy Markdown
Owner Author

This PR remains deferred pending current-head and ownership revalidation. The prior review recorded an ownership hold, but this round did not confirm that worktree as active.

@diegosouzapw diegosouzapw added deferred-v3.8.50 Adiada para o ciclo v3.8.50 (validacao VPS, refactor, ou escopo grande) merge-train-deferred PR ejetada do merge-train — triagem separada and removed merge-train-deferred PR ejetada do merge-train — triagem separada deferred-v3.8.50 Adiada para o ciclo v3.8.50 (validacao VPS, refactor, ou escopo grande) labels Aug 17, 2026
…0313-catalog-cache

# Conflicts:
#	src/app/api/v1/models/catalog.ts
#	src/app/api/v1/models/catalogCache.ts
…boundary

Post-sync-merge fixup for #9147/#10313 against release/v3.8.50:

- Resolve the catalog.ts/catalogCache.ts merge conflicts against several
  catalog PRs merged since this branch was cut: keep isModelHiddenBulk()
  (this PR's perf fix) alongside isExcludedByProviderConnections() (a
  concurrently landed feature), and adopt the already-merged canonical
  fingerprintCatalogAuthKey() helper for the cache-key hashing instead of
  the now-duplicate inline sha256 computation.
- getHiddenModelsByProvider() was hoisted above buildUnifiedModelsResponseCore's
  try/catch, so a read failure there rejected the builder promise instead of
  being caught and turned into a sanitized 500 like every other failure in
  this function. Combined with the pre-existing promise.finally() dangling
  chain in catalogCache.ts's in-flight coalescing, that produced a genuine
  unhandled rejection. Move the bulk-load call back inside the try block.
- Align tests/unit/models-catalog-route.test.ts and
  tests/unit/10313-catalog-cache-key-hashing.test.ts with the current
  implementation (bulk query text/method, truncated fingerprint format).
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.
@diegosouzapw
diegosouzapw merged commit d5e4c0f into release/v3.8.50 Aug 19, 2026
9 of 10 checks passed
@diegosouzapw
diegosouzapw deleted the fix/9147-10313-catalog-cache branch August 19, 2026 15:11
giauphan pushed a commit to giauphan/OmniRoute that referenced this pull request Aug 20, 2026
…uzapw#9147, diegosouzapw#10313) (diegosouzapw#10538)

* fix(catalog): hash API key in buildCatalogCacheKey so raw credentials never live in the key string (diegosouzapw#10313)

* fix(api): yield event loop and bulk-load override tables in catalog build (diegosouzapw#9147)

* fix(api): keep bulk hidden-model load inside catalog builder's error boundary

Post-sync-merge fixup for diegosouzapw#9147/diegosouzapw#10313 against release/v3.8.50:

- Resolve the catalog.ts/catalogCache.ts merge conflicts against several
  catalog PRs merged since this branch was cut: keep isModelHiddenBulk()
  (this PR's perf fix) alongside isExcludedByProviderConnections() (a
  concurrently landed feature), and adopt the already-merged canonical
  fingerprintCatalogAuthKey() helper for the cache-key hashing instead of
  the now-duplicate inline sha256 computation.
- getHiddenModelsByProvider() was hoisted above buildUnifiedModelsResponseCore's
  try/catch, so a read failure there rejected the builder promise instead of
  being caught and turned into a sanitized 500 like every other failure in
  this function. Combined with the pre-existing promise.finally() dangling
  chain in catalogCache.ts's in-flight coalescing, that produced a genuine
  unhandled rejection. Move the bulk-load call back inside the try block.
- Align tests/unit/models-catalog-route.test.ts and
  tests/unit/10313-catalog-cache-key-hashing.test.ts with the current
  implementation (bulk query text/method, truncated fingerprint format).

* perf(api): memoize getConnectionsForProvider in catalog builder

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.

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…uzapw#9147, diegosouzapw#10313) (diegosouzapw#10538)

* fix(catalog): hash API key in buildCatalogCacheKey so raw credentials never live in the key string (diegosouzapw#10313)

* fix(api): yield event loop and bulk-load override tables in catalog build (diegosouzapw#9147)

* fix(api): keep bulk hidden-model load inside catalog builder's error boundary

Post-sync-merge fixup for diegosouzapw#9147/diegosouzapw#10313 against release/v3.8.50:

- Resolve the catalog.ts/catalogCache.ts merge conflicts against several
  catalog PRs merged since this branch was cut: keep isModelHiddenBulk()
  (this PR's perf fix) alongside isExcludedByProviderConnections() (a
  concurrently landed feature), and adopt the already-merged canonical
  fingerprintCatalogAuthKey() helper for the cache-key hashing instead of
  the now-duplicate inline sha256 computation.
- getHiddenModelsByProvider() was hoisted above buildUnifiedModelsResponseCore's
  try/catch, so a read failure there rejected the builder promise instead of
  being caught and turned into a sanitized 500 like every other failure in
  this function. Combined with the pre-existing promise.finally() dangling
  chain in catalogCache.ts's in-flight coalescing, that produced a genuine
  unhandled rejection. Move the bulk-load call back inside the try block.
- Align tests/unit/models-catalog-route.test.ts and
  tests/unit/10313-catalog-cache-key-hashing.test.ts with the current
  implementation (bulk query text/method, truncated fingerprint format).

* perf(api): memoize getConnectionsForProvider in catalog builder

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.

---------

Co-authored-by: adevwithpurpose <adevwithpurpose@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants