Skip to content

fix(routing): table pricing with catalog fallback for off-table models, pooled latency bootstrap, fresh tier cache - #12792

Merged
diegosouzapw merged 9 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:fix/batch7-pricing-db
Sep 10, 2026
Merged

diegosouzapw merged 9 commits into
diegosouzapw:release/v3.8.51from
maxmad64bis:fix/batch7-pricing-db

Conversation

@maxmad64bis

@maxmad64bis maxmad64bis commented Sep 5, 2026 •

Copy link
Copy Markdown
Contributor

⚠️ base-red inherited: #12732

Summary

Free models outside the price table were billed as premium, and tier labels couldn't see prices an operator had overridden in settings. This PR fixes both sides of that: routing now checks the free-model catalog (the list of providers with a documented free tier) before falling back to premium prices, and tier classification reads live database prices with a freshness check so it can't drift silently.

Say you run a free model that isn't one of the 21 hardcoded entries — before this change it inherited $5/$15 per million tokens and got pushed to the bottom of cost-aware routing. Now it resolves to $0 and routes as free. The change is additive: classifyTier sync still serves table values, and generateRoutingHints now awaits classifyTierAsync per target (live DB pricing via getPricingForModel, sync fallback if unreachable).

It also stops guessing 1500ms for every unknown model's latency (the pool's observed median is used instead, with a counter tracking how often the guess path fires), scores real per-candidate costs instead of zero, and computes budget caps against actual estimated tokens rather than a fixed 1000.

Related Issues

Validation

  • Change type: 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

Tests Added Or Updated

  • tests/unit/pricing-fallback.test.ts (new, 4 tests): catalog free off-table → $0, prefixed id normalization, paid off-table → 5.0/15.0 fallback, table hits untouched
  • tests/unit/combo-bootstrap-latency.test.ts (new, 5 tests): median ignores corrupt entries, empty stats → undefined, counters, table-hit vs miss accounting, pure warn gate
  • tests/unit/tier-pricing-cache.test.ts (new, 4 tests): async tier reflects DB writes incl. resetAllPricing, DB-down fallback equals sync, sync path untouched by DB
  • tests/unit/virtualFactory-valuation.test.ts (new, 4 tests): reliability absent/null/0.2/errorRate, real table costs via helper
  • tests/unit/auto-combo-budget.test.ts (new, 2 tests): default math byte-identical, 32k tokens enforce 32x tighter budget via assert.throws
  • tests/unit/complexity-router.test.ts, tests/integration/manifest-routing.test.ts, open-sse/services/__tests__/manifestAdapter.test.ts: awaited for the new async tier lookups
  • scripts/check/check-pricing-freshness.mjs (new): freshness check failing when providerCostData.ts is untouched for 90+ days — now wired into CI (package.json + gate runner): green, full run passes except a pre-existing check:file-size failure unrelated to this PR
  • npm run test:combo:matrix: 11 failures identical before/after this change (pre-existing on base, verified via stash comparison)
  • npm run lint: exit 2 on base and branch alike — pre-existing unused-var errors in combo.ts/manifestAdapter.ts/tierResolver.ts (none introduced here; full-suite count unchanged). Note: lint needed npm install es-abstract@1.23.9 --no-save --no-package-lock locally to start at all (loader bug, noted per repo conventions).
  • No VPS round-trip needed: all changes are local pricing/scoring math covered by unit tests, no upstream behavior assumed.

Coverage Notes

  • Touches open-sse/ only: every new branch is covered by the tests above (lookup order in getModelPricing: provider-specific table entry, shared pricing, generic table entry, free catalog, then the premium fallback; median + counters + warn gate, async tier + fallback counter, reliability/cost/budget in the virtual factory and engine).
  • No coverage gate regression attributable to this PR: the only untested lines are the fail-open catch paths, exercised implicitly by the DB-down test.

Reviewer Notes

  • Additive by design: classifyTier sync is untouched; getTargetTier stays sync; classifyTierAsync is migrated onto one real call-site (generateRoutingHints, awaited by its 3 consumers). If the async path ever misbehaves, the sync fallback returns byte-identical values.
  • Known behavior change (intended): virtual candidate rankings shift now that costs are real — cost-saver mode actually discriminates. Worth a glance at the resolveVirtualCost helper (open-sse/services/providerCostData.ts).
  • The DEFAULT_MODEL_P95_MS table still has 8 entries (21 in the pricing table — separate tables, separate concerns); unlisted models now bootstrap from the pool median instead of a flat 1500ms, and the freshness gate keeps the pricing table honest going forward.
  • A provider-specific table entry still wins over a generic one (provider-cost-data.test.ts); the generic entry and the free catalog only apply after the shared pricing lookup. The snapshot reliability factor reuses reliabilityFactor from scoring.ts instead of a second copy of the formula.
  • Snapshot reliability weight defaults to 0, so scoring only discriminates once mode packs carry it (fix(auto-combo): give every mode pack quality and reliability weights #12731).

@maxmad64bis
maxmad64bis force-pushed the fix/batch7-pricing-db branch 4 times, most recently from a36a108 to a2da776 Compare September 5, 2026 14:36
…s, pooled latency bootstrap, fresh tier cache
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.
@diegosouzapw
diegosouzapw merged commit d88fc2b into diegosouzapw:release/v3.8.51 Sep 10, 2026
4 of 7 checks passed
@maxmad64bis
maxmad64bis deleted the fix/batch7-pricing-db branch September 24, 2026 21:12
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…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.
muhamadgalihsaputra pushed a commit to niyatna/NiyatnaRoute that referenced this pull request Sep 27, 2026
…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.
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