Skip to content

fix(pool): diagnose empty pools, align catalog filter, and surface drop reasons - #12795

Merged
diegosouzapw merged 9 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:fix/batch5-pool-diagnosis
Sep 10, 2026
Merged

diegosouzapw merged 9 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:fix/batch5-pool-diagnosis

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #12732

Summary

Empty auto/* pools now say why they're empty. When every candidate gets filtered out — paid-only backends under hidePaidModels, models locked on all connections, or free tiers without a documented hard stop under STRICT mode — the server logs which stage removed how many (open-sse/services/autoCombo/virtualFactory.ts), and combo health carries the reason on each provider entry (dropReason on quotaHealth.providers).

This also fixes three small defects found while wiring the diagnostics: uncapped recurring free quotas (real free access with no token cap, just rate limits) were treated as unknown instead of safe; an empty free-only import fell back to the stale synced model list instead of reporting empty; and the models listing used a looser paid check than routing (a payload isFree flag alone could keep a paid model listed).

The lockout filter (modelLockoutFilter.ts, wired in virtualFactory.ts) is always on for dispatch, flags off included; only paid (hidePaidModels) and STRICT (freeAccessPolicy=strict) are opt-in. The read-only candidate inspector skips it, so a locked account's row stays listed with reachable:false (#9133).

Related Issues

Validation

  • Change type: provider / routing
  • Focused tests and category gates from the golden path
  • npm run lint
  • Reconciled with the current active release base; focused checks rerun afterward
  • Production-code changes include a new or updated automated test in this PR

Focused loop (all green, after rebasing onto the current tip): Vitest on paid-model-filter-6512 and the three strict-zero-cost-* suites (38 passed), node --import tsx/esm --test on the new suites plus auto-combo-candidates-locked-model-visible (42 passed), check:api-typecheck and typecheck:core within baseline, targeted eslint on all touched files (exit 0, no new suppressions). Full unit matrix, Vitest suites, coverage gate, and production build run in CI on this PR.

Tests Added Or Updated

  • tests/unit/autoCombo/paid-model-filter-6512.test.ts (extended: 4 diagnosis tests)
  • tests/unit/virtualFactory-pool-diagnosis.test.ts (new: paid/lockout/STRICT wiring)
  • tests/unit/freeAccessQuota.test.ts (new: uncapped free-tier gate)
  • tests/unit/sync-models-freeFilterEmpty.test.ts (rewritten to exercise the new resolveEffectiveAvailableModels helper, 3/3, neighbors green)
  • tests/unit/catalog-hidePaid.test.ts (new: shared paid predicate)
  • tests/unit/comboHealth-dropReason.test.ts (new: no-snapshot value assertion)
  • tests/unit/auto-combo-candidates-locked-model-visible.test.ts (existing fix(providers): Only part of auto/* pool candidates were returned #9133 guard, green with the dispatch-only lockout)

Coverage Notes

Changes span open-sse/ and src/. Each production change is covered by its focused test above: the paid diagnosis (paidModelFilter.ts), the lockout filter (modelLockoutFilter.ts), the three aggregation logs (virtualFactory.ts), the uncapped-safe gate (freeAccessQuota.ts), the empty-import cut (sync-models/route.ts), the shared predicate (catalogPaidFilter.ts, used by catalog.ts), the STRICT exclusion count (strictZeroCostFilter.ts), and the health reason (comboHealth.ts, utilization.ts). No coverage movement measured locally; the CI coverage ratchet reports on this PR.

Reviewer Notes

The STRICT excluded N/M (no-hard-stop X) log counts with the filter's own verdict (countStrictExclusions, one pass over the pre-filter pool, no extra quota lookups since resolveFreeAccessState reads the same short-TTL cache), so a candidate the filter only narrows to fewer connections is never counted as excluded. The three stage logs share one warnPoolDrop helper, which keeps virtualFactory.ts under its 1200-line cap. decideHidePaid is stricter than the old listing closure: models previously kept listed on a bare isFree payload flag are now hidden unless the catalog backs them — intended, that's the routing behavior. FREE_TIERS.md gains one sentence documenting that STRICT mode only trusts hardStopGuaranteed entries. Merge after #12744: both touch the paid-model predicate in catalog.ts (decideHidePaid here follows the catalog-first rule from #12744).

  • Changelog fragment included. 2nd freeFilterEmpty ternary (syncedModelsCount) kept inline with its explicit guard (its 3rd branch isn't a plain synced).

@maxmad64bis
maxmad64bis force-pushed the fix/batch5-pool-diagnosis branch from b13ef44 to 3f3ac52 Compare September 10, 2026 11:50
diegosouzapw and others added 8 commits September 10, 2026 09:11
Contagem de migrations 171 → 172 após a `175_call_logs_provider_stats_indexes.sql` do diegosouzapw#12832. Medido com `ls src/lib/db/migrations/*.sql | wc -l`.

Falha minha de processo: depois da onda 1 desta leva eu medi file-size, api-typecheck, changelog-integrity e colisão de migration — não o `check:docs-counts`. O drift ficou vivo até a onda 2 esbarrar nele.

41 mirrors de `llm.txt` regenerados pelo script do projeto. Aprovado por você para tocar `AGENTS.md`, mesma classe do diegosouzapw#12970.
…thout a documented free tier (diegosouzapw#12744)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

Parar de honrar `isFree:true`, `:free` e `0/0` vindos do upstream **antes** de consultar o catálogo é a inversão certa: hoje um provider sem tier livre documentado consegue se declarar grátis e o listing diverge do roteador `auto/*`. Checar as heurísticas depois do hit de catálogo fecha a porta sem quebrar o caminho de linhas custom locais, que continuam confiáveis pelo caminho próprio.

O `isFreeModel("or", …)` com um alias que não existe é o tipo de bug que passa despercebido porque falha silenciosamente para o lado permissivo.

**Integração:** o `decideHidePaid` que o diegosouzapw#12795 extraiu passou a usar o seu `isFreeForProvider` por id, em vez de OR-ear os dois aliases num único `freeProvider`. A forma por id é a garantia que esta PR estabelece, então ela prevaleceu.
…nifest (diegosouzapw#12786)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

Metadado de display derivado da mesma fonte da decisão, com um gate STRICT que quebra o CI se a contagem do manifesto divergir do catálogo — é o detalhe que impede a tag de virar mentira daqui a três meses.

Registrar que 77 entradas de catálogo viram 76 no manifesto porque o `arcee-ai` ainda não tem entrada no registry é exatamente o tipo de discrepância que costuma virar bug fantasma.
…ty/breaker (diegosouzapw#12794)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

Tratar breaker aberto como equivalente a fechado no scoring de snapshot é pior que não pontuar: afirma saúde onde há falha conhecida. Trocar as três constantes neutras por valor observado é a correção, e manter preço e orçamento fora do escopo mantém a PR revisável.

**Integração:** o `computeSnapshotWeights` conflitou com o diegosouzapw#12731, que adiciona peso de `reliability` a partir de `failureRate`/`errorRate`. Os dois cobrem chaves diferentes e compõem — ficaram ambos: reliability do diegosouzapw#12731, health via breaker e quality desta PR, quota neutro nos dois. Nenhum dos dois lados foi descartado.
…diegosouzapw#12731)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

`reliability-first` que não pesava reliability é o defeito mais constrangedor possível num mode pack, e a causa é clara: `modePacks.ts:13` substituía os defaults por inteiro. Financiar os novos pesos com `quota`/`costInv`/`tierPriority` mantendo cada pack somando 1.0 é a parte que exige cuidado e você fez.

Manter `quality-first` em 0.03, igual ao default, para que ele não fique mais fraco que `balanced`, é o tipo de detalhe que só aparece quando se checa a tabela inteira.

**Integração:** conflitou com o diegosouzapw#12794 no `computeSnapshotWeights`; os dois compõem e ambos ficaram.
…iegosouzapw#12790)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

Duas cópias da mesma ordem de provider com um "keep in sync" implícito é dívida que cobra juros a cada provider novo. Uma definição com re-export nos dois lados resolve a classe.

O `xao/*` ordenando depois de todo provider conhecido em vez de junto do `xai-oauth` é um sintoma concreto de que a duplicação já estava divergindo.

**Integração:** `scripts/quality/run-all-gates.mjs` conflitou com o `check:pricing-freshness` que entrou pelo diegosouzapw#12792 na mesma onda. Aditivo — os dois gates coexistem.
…s, pooled latency bootstrap, fresh tier cache (diegosouzapw#12792)

Um modelo grátis fora da tabela herdando $5/$15 por milhão e afundando no roteamento cost-aware é o defeito mais caro desta onda: silencioso, e inverte exatamente a decisão que o operador quer.

Parar de chutar 1500ms de latência para modelo desconhecido e usar a mediana observada do pool — com contador de quantas vezes o chute dispara — é trocar heurística por medição do jeito certo. O contador é o que permite saber se valeu.

Revalidei após reconstruir a branch sobre o tip: **33/33** nas suítes da PR, typecheck:core limpo, `check-api-typecheck` OK (289).

**Duas integrações:**

1. `computeSnapshotWeights` conflitou com o diegosouzapw#12794 (health via breaker + quality), já mergeado. Os dois compõem e ambos ficaram: o seu termo de `reliability` — que era a única chave que o caminho de snapshot ainda ignorava — mais o health observado e o quality do diegosouzapw#12794.
2. `scripts/quality/run-all-gates.mjs` conflitou com o `check:provider-order-sync` do diegosouzapw#12790. Aditivo, os dois gates coexistem.

**Nota de dívida:** o `virtualFactory.ts` cruzou o teto de 1200 linhas pela primeira vez (1187 → 1207) somando esta onda. Congelei em vez de dividir e registrei os dois candidatos a extração na justificativa — `computeSnapshotWeights` (~85 linhas) e o grupo de elegibilidade de credencial (~70). Qualquer um dos dois volta o arquivo para baixo do cap.
@diegosouzapw
diegosouzapw merged commit 8ecdd88 into diegosouzapw:release/v3.8.51 Sep 10, 2026
4 of 7 checks passed
@maxmad64bis
maxmad64bis deleted the fix/batch5-pool-diagnosis branch September 24, 2026 21:12
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…thout a documented free tier (diegosouzapw#12744)

Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

Parar de honrar `isFree:true`, `:free` e `0/0` vindos do upstream **antes** de consultar o catálogo é a inversão certa: hoje um provider sem tier livre documentado consegue se declarar grátis e o listing diverge do roteador `auto/*`. Checar as heurísticas depois do hit de catálogo fecha a porta sem quebrar o caminho de linhas custom locais, que continuam confiáveis pelo caminho próprio.

O `isFreeModel("or", …)` com um alias que não existe é o tipo de bug que passa despercebido porque falha silenciosamente para o lado permissivo.

**Integração:** o `decideHidePaid` que o diegosouzapw#12795 extraiu passou a usar o seu `isFreeForProvider` por id, em vez de OR-ear os dois aliases num único `freeProvider`. A forma por id é a garantia que esta PR estabelece, então ela prevaleceu.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…op reasons (diegosouzapw#12795)

Um pool `auto/*` vazio que não diz por que está vazio é a pior forma de falha: o operador vê ausência e não sabe se é config, cota ou catálogo. Registrar qual estágio removeu quantos, e carregar `dropReason` em cada entrada de `quotaHealth.providers`, transforma silêncio em diagnóstico.

Os três defeitos achados de carona valem tanto quanto a feature — em especial a cota livre recorrente sem teto sendo tratada como desconhecida em vez de segura, que é justamente o caso que mais aparece.

Revalidei após reconstruir sobre o tip: **15/15**, typecheck:core limpo, `check-api-typecheck` OK (289).

**Uma mudança minha no `catalogPaidFilter.ts`.** Mantive a sua extração — ela é mais limpa que o predicado inline — mas troquei o corpo para usar `isFreeForProvider` por id. O módulo OR-eava `providerHasFreeModels(resolved) || providerHasFreeModels(canonical)` num único `freeProvider` e depois testava `isFreeModel` em cada um. Com isso, um id cujo próprio provider não documenta tier livre passa a ler como grátis sempre que o alias irmão documenta — que é exatamente o buraco que o diegosouzapw#12744 fechou e já está no tip. Por id preserva a garantia; nenhuma outra linha do módulo mudou.

**Dívida registrada:** o `virtualFactory.ts` foi de 1187 para 1219 somando esta onda e cruzou o teto de 1200 pela primeira vez. Congelado com os candidatos a extração nomeados na justificativa.
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.

2 participants