Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.
"Um checkpoint que nunca olhou o próprio resultado" é o tipo de defeito que só aparece quando o WAL fica maior que o banco. Ler a linha de retorno do pragma e diferenciar busy de sucesso é a correção; o retry `PASSIVE` um minuto depois é o que evita esperar as 6 horas do próximo tick.
Gostei da decisão de não persistir contador: streak em memória que zera no stop é o comportamento honesto para uma métrica de contenção.
Os 11 casos cobrem as formas de retorno que o pragma pode assumir — sentinela `-1`, objeto pelado, `undefined`/`null`/`[]` — que é onde esse tipo de parsing costuma quebrar em silêncio.
Contadores de docs fora de sincronia com o código, aprovado pelo dono em chat por tocar `AGENTS.md` e `skills/cli-tunnel/SKILL.md` (Hard Rule — superfície de instrução de agente). Nenhuma instrução mudou.
`Docs Gates` acusava 6 drifts STRICT. Dois vieram da minha leva de 16 PRs: migrations 169 → **171** (#12707 trouxe a 173, #12867 a 174) e estratégias de roteamento 19 → **20** (#12789 registrou a `quota-weighted`). Contei os arquivos em vez de confiar na memória: `ls src/lib/db/migrations/*.sql | wc -l` → 171.
Os 41 mirrors de `docs/i18n/*/llm.txt` foram regenerados com `scripts/i18n/sync-llm-mirrors.mjs` — o gate exige cópia exata da raiz.
O outro braço, `check:agent-skills-sync` acusando `GENERATED: + cli-tunnel`, era herdado (o corpo do #12866 já o registrava). O `SKILL.md` commitado documentava `tunnel create [type]`, um argumento que a CLI **não aceita** — conferido em `bin/cli/commands/tunnel.mjs:21`, que declara `.command("create")` puro. Saída do gerador, não escrita à mão.
| gate | antes | depois |
|---|---|---|
| `check:docs-counts` | 6 drifts STRICT | **0** |
| `check:docs-sync` | FAIL — 41 mirrors divergentes | **PASS** |
| `check:agent-skills-sync` | `+ cli-tunnel` | **UNCHANGED: 46 skills** |
Mid-cycle reconciliation of `## [3.8.51]` against the full cycle range
`release/v3.8.50..release/v3.8.51` (091589089c..d6f315018a, 696 non-merge commits):
- fold the 366 `changelog.d/` fragments into the section (features had been
landing under `[Unreleased]` because the aggregator appends at the FIRST
matching heading) and delete them
- drop 25 fragments that duplicate bullets already shipped in `[3.8.50]` /
`[Unreleased]` (six phantom fragments re-added by branches cut before the
v3.8.50 aggregation; 19 pre-cycle PR fragments)
- generate one bullet per cycle commit that had no fragment (350 commits:
44 features / 196 fixes / 91 maintenance, 17 Dependabot bumps rolled up),
carrying the merged PR link and `— thanks @author`
- credit audit: link every fragment to the PR that actually landed it
(`git log --diff-filter=A`), fix three `#PR_NUMBER` placeholders and two
misnumbered fragments (#11845, #11864), credit the recreated PRs to their
original authors (#11887–#11892 → @MumuTW, #12255 → @backryun,
#11771 → @Rahulsharma0810), the nine hartmark co-authored fixes, and the
deleted PR #11370 to @kriptoburak
- add the "Release by the numbers" block, the Top-25 ranking (mailmap +
GitHub login) and the mandatory `### 🙌 Contributors` hall
(111 external contributors + maintainer, generated by
`release:contributors --inject`)
- resync the 41 i18n CHANGELOG mirrors
Gates: check:changelog-integrity OK, check:docs-sync PASS.
Validado numa worktree combinada com as 16 PRs desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-file-size e check-changelog-integrity OK, complexity 2788/3218 e cognitive 1261/1437 (ambos sob a baseline), ESLint 0 erros nos 152 arquivos alterados, 771 testes unitários focados, 49 de integração e a suíte vitest:ui completa (2149) verdes.
A separação entre o que é do service worker e o que é do Caddy está certa e é o que torna a PR mergeável: o `respondWith` em navegação é defeito nosso, o `Alt-Svc` mentindo h3 é config de proxy reverso e não tem o que fazer aqui.
O `/dashboardfoo` casando com `startsWith("/dashboard")` é um achado à parte, e o bump de cache v2→v3 é o que faz o worker antigo sair do ar nos clientes que já estão presos.
opencode v2 loads plugins through a contract the existing
@omniroute/opencode-plugin cannot satisfy: v1 exports plugin factories with an
auth/provider/config/tool hook object, v2 expects a default define({id, setup})
carrying catalog and integration domains. One package would have to satisfy
both loaders from a single entrypoint. An opencode v2 install therefore has no
route to an OmniRoute gateway at all: no model discovery, no combos, no
enrichment.
This adds @omniroute/opencode-plugin-v2, a self-contained package. The v1
plugin is untouched, so v1 users see no move, no migration and no breaking
version. The two packages deliberately share no code and no release: the
mapping logic here began as a port of v1's and now lives in this package, which
keeps either one free to change without a coordinated publish.
The plugin publishes models, combos and auto-combos into the host catalog,
refreshes them lazily behind a 300s TTL, and keeps serving the last known
catalog from an on-disk snapshot when the gateway is unreachable. Publishing is
staged: models and combos are what a catalog is, so they go out as soon as they
are known, while auto-combos, the provider list and the enrichment overlay fold
into the snapshot when they land. Gating the publish on all of them made the
catalog hostage to the slowest source — a gateway that accepts the connection
and never answers /api/combos/auto left everything unpublished until that fetch
timed out, which is longer than a short-lived host stays alive.
Display names carry what the gateway knows about a model: the upstream provider
it routes to, whether it is free, and the budget that comes with it. Those parts
were already fetched and then dropped, so two connections selling the same model
looked identical in the picker. The provider prefix can be turned off with
`providerTag: false`.
The on-disk snapshot carries that overlay too, under a size cap, so a cold start
opens on named models rather than raw ids. The host is asked to reload only when
the catalog or the overlay actually moved, never once per refresh window.
The gateway key comes from the host credential store when one is connected, so
connecting the integration from opencode is enough and no secret needs to sit
in opencode.json; a plugin option and an environment variable remain as
fallbacks, and a host too old to expose a credential store still loads. Nothing
is silent when a key is missing or refused: an absent key is named once at
startup with the three ways to supply one, and an enrichment source the gateway
rejects is reported per endpoint with what the catalog loses. Those three
failures used to be empty catch blocks, which turned a management token the
gateway refuses into a catalog of raw model ids with no explanation.
Tool calling to Gemini keeps working. Gemini answers 400 INVALID_ARGUMENT for
an entire request whose tool declarations carry $schema, $ref or
additionalProperties. The v1 plugin handled it by wrapping fetch and rewriting
the JSON body; v2 does it on the language model, where the tools are still
structured data, and only for Gemini models of this provider. It can be turned
off with geminiSanitization: false, and a host exposing no aisdk domain loads
without it.
The catalog contract itself is a moving target, so the plugin adapts to the
host instead of assuming one shape. The released CLI keeps the aisdk package,
the endpoint (as settings.baseURL), the request headers and the variant options
directly on the model and provider; the current SDK types keep the same
information inside an api block. Writing only the api block yields a catalog
the released CLI lists but cannot route. Rather than key off a version list
that goes stale on the next release, the plugin reads the shape the host seeds
into the catalog draft and publishes accordingly: a seed with a top-level
package and no api block gets both field sets, a seed with an api block gets
that block alone, and an undisclosed seed gets both. None of the legacy keys
collide with a key of the current types, so the two shapes coexist on one
object, variants included.
Four v1 behaviours are deliberately not carried over, because v2 either owns
them or no longer needs them: the plugin-side debug log (the host has its own
logging), the compression-metadata suffix on combo names, the MCP auto-emit
(the v2 host owns MCP), and the omni-sync command plus its background timer
(the TTL and a content fingerprint drive catalog.reload instead).
A refresh never downgrades what is already published: the previous overlay is
carried forward until the new one lands, so names, pricing and the usable
filter no longer drop out for the length of every TTL window. The disk snapshot
is read after the credential is resolved, because it is keyed by that
credential — reading it earlier looked up the identity the options carry rather
than the one in use, and rejected a perfectly good catalog exactly when the
gateway was down.
The tool-schema cleaner now knows where a schema ends and a property name
begins. Stripping keywords by name anywhere in the tree deleted a tool
parameter called `ref` while leaving it in `required`, handing the model a
schema it could not satisfy; a `$ref` it cannot resolve now forwards the tool
untouched instead of widening it to accept anything. Gemini detection is
anchored on the model family, so `gemini-compatible-proxy` is no longer treated
as a Gemini model.
A source the gateway refuses is reported on the library entry point as well,
not only through the plugin, so the usable-provider filter can no longer disable
itself in silence. `providerId` is bounded to a safe character set because it
reaches a filesystem path, `hiddenModels` covers combos as it already covered
models, the Anthropic block gets the gateway root rather than a doubled `/v1`, an unparseable tool schema forwards the tool instead
of failing the request, and the package typechecks under the same settings as
the v1 plugin.
CI mirrors the existing plugin workflow: install, build and test on Node 22 and
24, for both packages. The plugin SDK stays pinned, and the host-shape assertions carry the risk of
a contract move rather than a check against a rolling upstream tag.
Co-authored-by: Max <maxmad64@gmail.com>
Merged, with the markdown repaired.
Checked every row against `src/lib/gamification/xp.ts:138` — the table now matches `XP_REWARDS` exactly, keys and values, and the descriptions are the JSDoc lines verbatim. The old table was documenting actions that do not exist (`badge_earned`, `streak_milestone`, `referral`, `model_diversity`, `compression_use`, `skill_use`) and missing the three that do (`model_switch`, `invite_redeem`, `streak_bonus`). Good catch.
Two formatting fixes before merge: the action names were padded inside the code spans (`` `request ` ``), which renders the trailing spaces as part of the identifier; and the unrelated MCP-tools table below had its header row flattened, losing the column alignment. Restored both and ran Prettier — the file is clean now.
Thank you for reconciling this against the source instead of guessing.
Merged, with the threat model kept and two unverifiable claims dropped.
The core warning is correct and worth having in both files: `/var/run/docker.sock` is a host-root trust boundary, the `cli` profile must not be published beyond `127.0.0.1`, and no extra host mounts belong in it. That is now in `docker-compose.yml` next to the mount and in the DOCKER_GUIDE.
Two things I changed before merging, both `AGENTS.md` documentation-accuracy calls:
1. **The stated purpose.** The socket is not mounted so OmniRoute can "launch short-lived codex/claude-code/droid/openclaw containers" — I could not find any container-spawn path. It is there for the in-container auto-updater: `src/lib/system/autoUpdate.ts:236` probes for `/var/run/docker.sock` and skips the Docker path when it is absent, and the mount sits right beside `AUTO_UPDATE_HOST_REPO_DIR`. Rewrote the sentence around that and cited the file.
2. **Item 3, the audit log.** "recorded in the server log with the called tool, the prompt digest (not content), and the spawned image SHA" — no such logging exists (`grep -rn "prompt digest\|promptDigest\|imageSha" src/ open-sse/` is empty). A security doc promising forensics that are not implemented is worse than one that stays quiet, so I removed the item rather than soften it.
The `MITM-TPROXY-DECRYPT.md` and `SUPPLY_CHAIN.md` cross-references both resolve and stayed.
Thanks — the docker.sock boundary genuinely was undocumented.
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral
* fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages
* fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test
* docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts
* chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header
* feat(catalog): eligibilityGate field and gatedRecurringTokens total
* docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts
* feat(check): docs-counts gate validates the eligibility-gated free-tier figure
* fix(docs): budget card reads computeFreeModelTotals() instead of regex-parsing the catalog
* docs(free-tiers): retire the stale Gemini onboarding quota text
* feat(radar): carry eligibilityGate through the feed schema, the merge and the summary API
* feat(dashboard): show the eligibility-gated free-tier figure apart from the headline
* docs(free-tier): refresh catalog-entry counts to 442 after base sync
* feat(catalog): ModelScope as the first eligibility-gated pool; document the gated bucket and how we count
* docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts
* docs(free-tiers): correct the unsummed-catalog comparison figure to the current catalog
* docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream)
* docs(free-tiers): re-sync numbers after merging PR1 (Cerebras reclassified upstream)
* fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3)
* fix(catalog): keep eligibility-gated rows out of every headline-adjacent figure
The eligibility gate was honored by the steady headline and the pool count, but
three adjacent figures still counted gated rows: the credit reductions feeding
steadyWithRecurringCreditsTokens/firstMonthRealisticTokens, the uncappedProviders
list ("permanently free, no cap"), and the docs gate's free-forever provider set,
which was built from freeType alone.
- computeFreeModelTotals: filter !isGated in the recurring-credit, one-time-credit
and uncapped predicates; gatedProviders semantics unchanged (steady rows only).
- check-docs-counts-sync: exclude eligibility-gated rows from the FOREVER set,
which moves the live free-forever count 53 -> 52 (the base's value). README,
promise-pillars.svg and FREE-TIERS-GUIDE re-synced.
- gen-budget-card-svg: skip gated one-time credits like the totals do, and fail
loudly on `--out` without a path.
- Tests: gated one-time credit does not move firstMonthRealisticTokens; a gated
uncapped row is not in uncappedProviders; shipped gated rows carry no credit
tokens; the committed budget card is byte-identical to a fresh generation.
* test(catalog): allow eligibilityGate in the no-per-row-rating key allowlist
The allowlist landed on the base with #12318, after this branch's field was designed;
eligibilityGate says who may claim a quota, not how much a row can be trusted.
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw24@gmail.com>
Validado em lote numa worktree combinada com os 4 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **119/119** nos 9 arquivos de teste que trazem.
Três dos quatro conflitavam apenas no `config/quality/file-size-baseline.json`, todos de forma aditiva (chaves `_rebaseline_` distintas que devem coexistir); resolvidos com validação de JSON a cada passo.
Registro que o **#12637 não é duplicata do #12566**, apesar do título quase idêntico: o autor documenta que aquele escopou o cooldown de preflight por família e este cobre o `genericQuotaFetcher`, que é o que o roteamento reset-aware efetivamente chama. Traz também validação ao vivo em VPS (imagem X500, `onmi-gemini3.6` → HTTP 200), satisfazendo a Hard Rule #18.
Obrigado, @HouMinXi.
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.
A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.
O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.
Obrigado, @RaviTharuma.
Validado em lote numa worktree combinada com os 10 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check-file-size` OK e **241/242** nos 29 arquivos de teste que os PRs tocam.
A única "falha" não é falha: `tests/unit/autoCombo/strict-zero-cost-filter.test.ts` é um teste em estilo Vitest que eu incluí por engano na invocação do runner nativo do Node — ele quebra no import (`@vitest/runner`), não numa asserção. Ao investigar, descobri que esse arquivo não roda em nenhum dos dois runners hoje (o glob do `test:unit` não lista `autoCombo` e o `include` do Vitest só pega `.tsx` nessa pasta); é um problema pré-existente do repositório, sem relação com esta leva, e vou registrá-lo separadamente.
O #12636 conflitava apenas na lista de testes do `@omniroute/opencode-plugin/package.json`, de forma aditiva: o tip já tinha `models-fetcher.test.ts` (do #12607, irmão desta mesma leva) e o #12636 acrescenta `telemetry.test.ts`. Fiz a união dos dois lados (25 arquivos contra 24 de cada) em vez de escolher um, o que teria removido um arquivo da suíte do plugin em silêncio.
Obrigado, @RaviTharuma.
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral
* fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages
* fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test
* docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts
* chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header
* docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts
* docs(free-tiers): retire the stale Gemini onboarding quota text
* docs(free-tier): refresh catalog-entry counts to 442 after base sync
* docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts
* docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream)
* fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3)
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Validado sobre o tip de `release/v3.8.51` depois de reconciliar com o #12620, que entrou primeiro nesta mesma sessão e ataca a mesma classe de problema por outra arquitetura.
**A colisão e como foi resolvida.** O #12620 consertou o GHSA-qv45-56jc-4wmj adicionando `RAW_CREDENTIAL_PATTERNS` a `error.ts` e importando-os em `upstreamErrorPassthrough.ts`. Este PR resolve o mesmo problema quebrando `error.ts` em `errorSanitization.ts` + `errorPathRedaction.ts`. Mantive a divisão em módulos deste PR, porque ao comparar os dois vocabulários o dele já era mais amplo: o `STRONG_CREDENTIAL_TOKEN` daqui cobre `sk-`/`sk_` **com lookbehind e uma variante para a forma embutida** (que pega `sk-proj-…`), mais Slack `xox-`, AWS `AKIA`/`ASIA`, `github_pat_`/`ghp_`/`glpat-` e JWT de três segmentos.
A única forma que o #12620 carregava e este conjunto não tinha era a chave do Google (`AIza…`) — adicionada aqui, com o mesmo quantificador limitado que os irmãos usam (AGENTS.md → PII §1, já que isso roda sobre corpos upstream não confiáveis).
**A verificação não foi por inspeção.** Rodei as suítes do próprio #12620 contra esta estrutura: **48/48** em `error-sanitizer-sk-key-qv45`, `bifrost-relay-response-leak-9m72`, `search-baseurl-client-override-3f8g` e `search-baseurl-ssrf-guard` — incluindo a asserção anti-drift daquela suíte, que é o oráculo certo aqui: *para todo corpo que a camada de passthrough recusa como vazante, o sanitizador de fallback não pode devolvê-lo intacto*. Ela passa, então a propriedade de segurança dos três GHSAs sobrevive à troca de arquitetura.
Os 21 arquivos de teste deste PR: **259/259**. `typecheck:core` limpo.
Validado sobre o tip de release/v3.8.51 — e o PR ficou completo depois que o autor adicionou a tabela de preços.
O que fazia o teste falhar antes não era a lista free (que o PR já tinha corrigido em `LEGACY_FREE_PROVIDERS` e `tierDefaults.json`), e sim que `classifyTier` cai no ramo cost-based e todos os modelos Cerebras estavam declarados com `input: 0, output: 0` — $0/M ≤ threshold devolve 'free' de qualquer jeito. A tabela de preços resolve isso na raiz.
Confirmei o `gpt-oss-120b` a $0,35/$0,75 de forma independente contra a fonte pública, o que corrobora o resto da tabela. **4/4** no teste-guarda e **25/25** somando free-tier-catalog e free-models; typecheck:core limpo. Obrigado, @HouMinXi.
Validado em lote numa worktree combinada com os 14 PRs desta campanha de error-boundary sobre o tip de `release/v3.8.51`: `typecheck:core` limpo e **120/120** nos 23 arquivos de teste que os PRs trazem.
Um ponto que só apareceu no tree combinado: **#12465 e #12466 criam o mesmo arquivo novo** `open-sse/utils/streamReadiness.ts` (que não existe no tip) com desenhos divergentes de cancelamento — `cancelled` + `releaseLock` imediato num, `readInFlight`/`cancelRequested` com `cancelReader` fire-and-forget no outro. Adotei a versão do #12466, que difere e defere o release do lock para quando a leitura em voo termina, e validei a escolha rodando as suítes dos **dois** PRs contra ela: 21/21 no readiness compartilhado e 22/22 incluindo o boundary do Perplexity.
17 de 18 checks verdes, **zero falhas** — incluindo os quatro shards de unit, Vitest, CodeQL, semgrep, Docs Gates, Merge integrity e o **Fast Quality Gates**, que era exatamente o que o rebaseline do `RoutingTab.tsx` (1606→1607, a linha do seletor que acompanha a nova versão de identidade) veio consertar.
O único check restante, `No new ESLint warnings`, ficou enfileirado sem iniciar (duração 0) atrás da saturação de runners de hoje. Cobri os dois comandos que ele executa, localmente e sobre este HEAD:
- `npm run check:codeql-ratchet` → **0 alertas abertos** contra baseline 6 (sem regressão).
- `npx eslint --max-warnings 0` nos 9 arquivos de código que esta branch altera → **exit 0**, nenhum warning.
Conteúdo: os dois commits do #12402 que não estavam subsumidos, com autoria do @ggiak preservada pelo cherry-pick `-x`, mais o rebaseline e o fragmento de changelog que faltava. `typecheck:core` limpo e **138/138** nos testes focados. As três versões (`claudeCodeClient.ts`, `Dockerfile`, `compose.yml`) conferem em 2.1.258, e `npm view @anthropic-ai/claude-code@2.1.258` resolve — o Dockerfile instala esse pin exato.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check:provider-consistency` OK (273 entradas REGISTRY, **356** providers canônicos), `check-docs-counts-sync` exit 0 e **300/300** nos testes que a leva toca.
O crescimento de arquivo que os PRs empilham uns sobre os outros foi rebaselinado num único registro datado (`_rebaseline_2026_09_03_houminxi_batch`), com a decomposição por arquivo: `providers/page.tsx` +18 (import CSV do #12504 + busca do #12495 no mesmo painel), `accountFallback.ts` +6 (o #12566 sobre o rebaseline que o #12590 já registrou — os dois tocam `checkFallbackError`) e `chatCore.ts` +3 (invalidez de cache de quota no 429 do #12325). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check:provider-consistency` OK (273 entradas REGISTRY, **356** providers canônicos), `check-docs-counts-sync` exit 0 e **300/300** nos testes que a leva toca.
O crescimento de arquivo que os PRs empilham uns sobre os outros foi rebaselinado num único registro datado (`_rebaseline_2026_09_03_houminxi_batch`), com a decomposição por arquivo: `providers/page.tsx` +18 (import CSV do #12504 + busca do #12495 no mesmo painel), `accountFallback.ts` +6 (o #12566 sobre o rebaseline que o #12590 já registrou — os dois tocam `checkFallbackError`) e `chatCore.ts` +3 (invalidez de cache de quota no 429 do #12325). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check:provider-consistency` OK (273 entradas REGISTRY, **356** providers canônicos), `check-docs-counts-sync` exit 0 e **300/300** nos testes que a leva toca.
O crescimento de arquivo que os PRs empilham uns sobre os outros foi rebaselinado num único registro datado (`_rebaseline_2026_09_03_houminxi_batch`), com a decomposição por arquivo: `providers/page.tsx` +18 (import CSV do #12504 + busca do #12495 no mesmo painel), `accountFallback.ts` +6 (o #12566 sobre o rebaseline que o #12590 já registrou — os dois tocam `checkFallbackError`) e `chatCore.ts` +3 (invalidez de cache de quota no 429 do #12325). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51`: `typecheck:core` limpo, `check:provider-consistency` OK (273 entradas REGISTRY, **356** providers canônicos), `check-docs-counts-sync` exit 0 e **300/300** nos testes que a leva toca.
O crescimento de arquivo que os PRs empilham uns sobre os outros foi rebaselinado num único registro datado (`_rebaseline_2026_09_03_houminxi_batch`), com a decomposição por arquivo: `providers/page.tsx` +18 (import CSV do #12504 + busca do #12495 no mesmo painel), `accountFallback.ts` +6 (o #12566 sobre o rebaseline que o #12590 já registrou — os dois tocam `checkFallbackError`) e `chatCore.ts` +3 (invalidez de cache de quota no 429 do #12325). As violações restantes (`codex.ts`, `stream.ts`) foram medidas também no tip puro e são drift da base, não desta leva.
`source.config.ts` feeds `docs/reference/**/*.md` to fumadocs-mdx, whose default
schema requires a `title`. #12478 added `docs/reference/REMOVED_PROVIDERS.md`
with no frontmatter block at all, so every production build died with:
[MDX] invalid frontmatter in docs/reference/REMOVED_PROVIDERS.md:
- title: Invalid input: expected string, received undefined
That single missing block is what turns three release-green gates red at once —
`Package artifact (npm pack policy)` fails on the build, and both
`Tarball boot-smoke` and the packaged CLI checks are skipped for lack of a
valid `dist/`.
Fixes:
- add the frontmatter block, matching the convention of its sibling reference
docs (`title` / `version` / `lastUpdated`).
- add `check:docs-frontmatter`, wired into `check:docs-all`, so the next doc
added without a title fails in milliseconds instead of costing a full Next
build and a red release branch. The gate reads its globs from
`source.config.ts` rather than duplicating them, so a new docs directory
cannot silently escape the check.
Verified: the gate reports OK across all 122 compiled docs, fails (exit 1) when
the frontmatter is removed, and `npm run check:docs-all` passes.
* fix(authz): hard-gate every credential export and CLI-config write
GHSA-5926-2w35-7h4q: `POST /api/providers/{id}/claude-auth/export` and
`.../codex-auth/export` gate on `requireManagementAuth(request)` with no
`alwaysRequireAuth`, and neither path was in ALWAYS_PROTECTED_API_PATHS. Under
`requireLogin=false` — the local-first default — both fail open, so anyone who
knows a connection id downloads the operator's raw Claude/Codex OAuth
access_token / refresh_token (plus the Codex id_token).
This is the third recurrence of one class. GHSA-mghq-58h3-qcqj added
/api/db-backups; GHSA-v7g9-7f55-5g46 added the /api/settings/*-json siblings
mghq had missed; these two are the siblings both missed. So the fix is written
against the class, not the two reported routes.
Sweeping every route that hands out stored credentials, dumps captured traffic,
or writes the operator's CLI config turned up four more on the fail-open tier:
- GET /api/logs/export — dumps call_logs (prompts and responses) and proxy_logs
for up to 168h.
- /api/cli-tools/codex-profiles — GET leaks the operator's account label; PUT
writes attacker-supplied auth.json and config.toml straight into the host's
Codex CLI config. Its only guard is ensureCliConfigWriteAllowed() with no
targetPath, which checks CLI_ALLOW_CONFIG_WRITES — default true. Paired with
the POST that stores an arbitrary profile, that is: save a profile holding the
attacker's auth.json, apply it, and the operator's CLI now runs on attacker
credentials (or, via config.toml, an attacker base URL).
- {claude,codex}-auth/apply-local and providers/agy-auth/apply-local — write a
stored credential into ~/.codex/auth.json and
~/.gemini/antigravity-cli/antigravity-oauth-token.
The traffic-inspector HAR exports were already covered by LOCAL_ONLY.
Routes with a dynamic segment cannot be expressed in the exact/prefix list — a
`/api/providers/` prefix would hard-gate the whole provider surface and break
every keyless install — so this adds ALWAYS_PROTECTED_API_PATTERNS, mirroring
the existing LOCAL_ONLY_API_PATTERNS, and `isAlwaysProtectedPath` consults both.
The apply-local routes get ALWAYS_PROTECTED rather than LOCAL_ONLY on purpose:
it closes the anonymous hole without breaking an operator driving the dashboard
through a tunnel.
Deliberately NOT adding `{ alwaysRequireAuth: true }` at the handlers. Tier 2 is
the architecture's designated mechanism and the guard runs before the handler; a
second copy of the same decision inside each route is exactly the kind of
duplicate that drifts out of sync (cf. the dashboardCsrf prefix scan that had to
be unified in #11417).
tests/unit/authz/credential-export-always-protected.test.ts — 5 tests, red
before the fix. Written as an inventory of the whole class rather than two more
assertions, plus negative cases: the neighbouring provider routes must stay on
MANAGEMENT, and a connection id containing a slash must not slip past `[^/]+`.
openapi.yaml marks the seven newly-gated operations `x-always-protected`, and
openapi-security-tiers.test.ts now resolves `{param}` placeholders so it can
validate the pattern entries too.
Reported by @skeletonsec.
Closes GHSA-5926-2w35-7h4q
* chore(quality): register the credential-export authz test in stryker tap.testFiles
The new tests/unit/authz/credential-export-always-protected.test.ts covers
src/server/authz/routeGuard.ts, so check:mutation-test-coverage --strict fails
until it is listed — its mutant kills would not count otherwise.
Inserted in place (no re-serialization: a JSON round-trip on this file reorders
~10 curated entries that are already out of alphabetical order, cf. #11438).
Validado sobre o tip de release/v3.8.51 após reconciliar o base-drift.
O pool de browsers ganhou um caminho **headed** (`headedBrowser`/`headedLaunching`, `resolvePlainBrowserLaunchOptions`, estado de launch por modo) depois que esta branch forkou. O PR reescrevia `launchBrowser()` no modelo de browser único contra o qual foi escrito, o que teria **removido o suporte headed**. Em vez disso, reapliquei a preferência pelo Obscura dentro do ramo headless de `launchBrowserInstance()`, à frente do cloakbrowser e do Chromium puro — um browser headed precisa ser um Chromium com janela real, então a preferência de engine é só do caminho headless. `state.engine` alimenta `isStealth` e `getBrowserPoolStatus()`, e o shutdown zera o engine sem matar o servidor Obscura compartilhado (dono: `./obscura.ts`).
typecheck:core limpo e 3/3 em tests/unit/obscura-integration.test.ts. Obrigado, @opensource-elearning.
Validado em lote numa worktree combinada com #12524, #12538, #12277 e #12367 sobre o tip de release/v3.8.51: os quatro boardaram sem conflito (áreas disjuntas — zai-web, nvidia, clova, cursor/devin/fable). typecheck:core limpo, check:provider-consistency OK (272 entradas REGISTRY, 355 providers canônicos), check:known-symbols OK, e 305/305 nos testes tocados pelos quatro PRs. Os IDs de modelo adicionados foram conferidos individualmente. Obrigado, @backryun.
* feat(api): conductor task creation route (repeat support)
* feat(a2a): record memoryHits consulted per task (observability, 2.7)
* feat(dashboard): repeat action in orchestration drawer (2.6 — repeat only)
* fix(dashboard): require every field each repeat contract needs before enabling the action
* feat(dashboard): memory-used drawer section + fase2 i18n/changelog (2.7)
* fix(dashboard): validate memoryHits shape before rendering + locale wording fixes
* fix(dashboard,a2a): stop memoryHits leaking into repeats, harden drawer guard + status clamp
Final whole-branch review fix wave for the Orchestration Canvas Fase 2 PR-C.
- a2a: `createTask` stores a COPY of `input.metadata` instead of aliasing it, so the
observability `memoryHits` written by `executeA2ATaskWithState` no longer leak into
`task.input.metadata`, into the persisted `a2a_tasks.input_json`, or into the drawer's
"Repeat" body (a repeated task was born carrying the previous run's memory snippets,
even with the `OMNIROUTE_A2A_MEMORY_HITS=0` kill-switch on).
- dashboard: `repeatReqFor` strips `memoryHits` from the a2a repeat metadata, so tasks
persisted before the copy-fix do not propagate them either.
- dashboard: the "Memory used" section now requires all four rendered fields (id, key,
type, snippet) to be strings — `{ id: "x", key: { a: 1 } }` used to throw "Objects are
not valid as a React child" and take the whole drawer down.
- dashboard: an `/a2a` action answered with a JSON-RPC error under HTTP 200 is reported as
a failure (`RPC <code>`, code only — never the upstream message) instead of a success
toast; the secured-deployment rejection keeps surfacing the sanitized `HTTP 400`.
- api: the conductor task-creation route clamps a hub status outside 400-599 to 502, so an
out-of-range status can no longer turn a hub refusal into a `RangeError`.
- dashboard: the History tab's `onActionDone` keeps the drawer mounted (and refreshes the
range) instead of closing it, so the repeat/cancel confirmation is actually visible.
- a2a: documented the recall owner-id limitation — `task.owner` is a SHA-256 key prefix
while memory rows are keyed by the DB api-key id, and no hash-to-id lookup exists today,
so recall only resolves under the keyless posture.
* refactor(dashboard): split drawer repeat helpers and test file under the size/complexity gates
---------
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
* fix(video): re-anchor log-redaction fullText from the finished guardrail payload so PII/credential maskers can't reopen the transcript leak
* docs(video): clarify P1 transcript-retention scope and re-anchor; list P2 surfaces (#12430)
Adds docs/reference/REMOVED_PROVIDERS.md (policy + register: puter #10210,
the keyless provider removed in #12440), links it from the docs index and
AGENTS.md's provider checklist, and adds a regression test that fails if any
registered id, alias or domain shows up again in the provider catalogs, the
executor map or the registry/executor sources.
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
* feat(db): a2a task history module over migration-002 tables
* feat(a2a): persist task lifecycle to a2a_tasks with 30d retention purge
* feat(api): a2a task history listing + historical detail fallback
* feat(dashboard): orchestration History tab (Airflow-grid) over persisted runs (2.2)
* fix(dashboard): assert history preset window + loading and time axis in the grid
* chore(dashboard): history i18n + changelog
* fix(dashboard): explicit history purge cascade + keep live drawer off the History tab
* docs: document OMNIROUTE_A2A_HISTORY_RETENTION_DAYS
* refactor(dashboard): split HistoryTab helpers under the complexity ratchet
---------
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
Migrates the Claude, Grok, LMArena, Notion and Perplexity web-cookie transports from the tls-client-node/Koffi sidecar to the exactly pinned wreq-js 3.2.0 runtime, keeping the per-provider browser/OS profiles, making request cookies ephemeral, bounding and generation-protecting the shared native transport pool, removing the legacy downloader and repair path, and carrying the native binding and license evidence through the npm, standalone, Electron, Docker and Bun packaging surfaces.
This is the consolidation of the two competing migrations, and the consolidation was decided by evidence rather than by preference. #11753's six suites were installed over this implementation and run as an independent specification: 31 of 36 passed. All five failures are artefacts of #11753 being the older design, not coverage gaps —
- two hardcode the 3.0.0 pin in their assertions (this branch pins 3.2.0, which is what the release tip already resolves; #11753's 3.0.0 would have conflicted);
- one reads open-sse/services/chatgptTlsClient.ts, deleted when #11754 retired ChatGPT Web, so the test is stale against the current tip;
- two import WREQ_JS_NATIVE_BINARY_NAMES / resolveWreqJsNativeBinaryName, which this branch redesigned into WREQ_JS_NATIVE_BINDINGS / resolveWreqJsNativeBinding plus WREQ_JS_VERSION — a rename from modelling natives as file names to modelling them as package bindings, verified as an API difference rather than a lost capability (the linux-x64-gnu .node is present and serviceable).
This branch is also the strict superset by scope: 7 files exclusive to it, including the wreq-js Rust license inventory and notices, .trivyignore, open-sse/utils/tlsClient.ts and assembleStandalone.mjs. #11753 had one exclusive file, its changelog fragment. Nothing needed porting, so #11753 is superseded rather than merged, and the changelog entry credits both.
Reconciled on merge: clean against the tip. The new migration suite (tests/unit/tls-client-wreq-migration.test.ts, 1374 lines, 31 cases) is frozen at its exact LOC with the rationale — it shares one native-transport harness, so splitting it mid-merge would duplicate that harness for no coverage gain. Verified that no existing cap moves.
Verified: 182/182 across the eight TLS, native-manifest, postinstall, standalone-bundle, pack-artifact and provider-validation suites, typecheck:core clean, check:cycles OK, check-changelog-integrity OK, check-file-size OK, and every changed TypeScript file parses.
Restores ChatGPT Web on a clean-room browser transport, merged on the operator's explicit decision.
Worth stating precisely, because this touches a provenance decision: the PR does not revert #11754. It narrows RETIRED_COMMON_CHATGPT_WEB_PROVIDER_IDS to the single GPL-derived alias cgpt-web and registers chatgpt-web as a separate clean-room id. The old implementation stays retired and blocked; the retirement machinery, its error code and its 410 contract are untouched. All four retirement suites agree with that distinction and pass unchanged.
The 44 protected agent-instruction surfaces this PR touches (AGENTS.md, llm.txt and its 42 mirrors, README) were verified rather than trusted: masking digits and comparing the removed and added line sets gives 264 lines on each side, identical — every change is a provider-count substitution, with no sentence added, removed or reworded.
Reconciled on merge: clean against the tip, with the two chat chokepoints this PR grows (src/sse/handlers/chat.ts +40, open-sse/handlers/chatCore.ts +30) recorded in the file-size baseline under an annotation. Verified that exactly those two caps move and nothing else, so the #12411 ratchet holds. The rebaseline is carried on this branch rather than left in a validation worktree — the propagation mistake that put the 2026-09-02 merge waves base-red in #12434.
Verified: 504/504 across the PR's 44 test files plus all four chatgpt-web retirement suites, check:provider-consistency OK (272 REGISTRY entries, 355 canonical providers), check-file-size OK, and every changed TypeScript file parses.
Thanks @backryun — separating the clean-room id from the retired alias, instead of reopening the old one, is what made this reviewable.
P1 of #12150: transcript text no longer reaches call logs or durable memory in the clear.
Reconciled on merge — the two persisted-requestBody assertions were failing, and the failure signature was misleading enough to be worth recording. They reported "expected: true, actual: false", which reads like the redaction not applying. It was not: pollForCallLog waited at most 120 tries x 20ms = 2.4s for the asynchronous SQLite write, then returned null, so assert.ok(row) failed before any redaction assertion ran. The observed durations were 5253ms and 4467ms against that 2.4s ceiling — a starved runner, not a leak. The control test failing alongside the positive one was the tell: a real redaction defect would break one direction, not both.
Replaced the fixed try count with a 30s wall-clock deadline: far past any healthy write, still bounded, and a fast machine still returns on the first pass. A privacy test should not depend on how busy the box is.
Verified: 4/4 three consecutive times under synthetic load, and 3/3 unloaded beforehand. Note the synthetic load reached ~7, below the ~38 where the original failure appeared, but the budget is now 12.5x larger and deadline-based rather than count-based.
The service operator asked in writing (2026-08-30) that their service be
removed from OmniRoute entirely: executor, registry entry, no-auth catalog
entry and alias, icon mapping, env var, docs rows, dedicated tests and
snapshots, and every passing mention in comments, fixtures and CHANGELOG
entries. Provider count drops from 355 to 354 on every canonical surface.
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
GET /v1/models with MODELS_CATALOG_PREFIX_MODE=canonical dropped every chat row of a provider whose registry alias is undefined (antigravity) or equal to its own id (agy, most built-ins). Each emission loop pushes alias/model only when includeAlias, and canonicalProviderId/model only when the ids differ — for a self-aliased provider both are the same string, so neither fired. #11918 fixed the class for custom nodes but not built-ins, and not the static loop. The alias row is now treated as the canonical row whenever the ids coincide, across the static, synced, custom and alias-backed loops; the canonical branch's !== alias guard is untouched, so dual and alias output cannot double up. Docs that described the omission as intended are corrected.
Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green.
Thanks @pacocartones.
The retryable chat_admission_busy 503 advertised a fixed Retry-After of 1s or 2s while the heavyweight lease it waits on is held for the entire SSE lifetime. Clients that honour the header — Codex CLI, agent fan-out — re-sent the same ~1 MiB /v1/responses body every second into a gate that could not have cleared, producing the queue_timeout retry storm that persisted even after OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT was raised.
ChatAdmissionController now tracks each live heavy lease's acquisition time and derives the hint from observed occupancy: the larger of the queue window the waiter already exhausted and the age of the youngest live lease, rounded up and capped at 60s. Both builders floor it at the historical 1s / 2s, so an idle gate answers exactly as before. Using the youngest rather than the oldest lease avoids a pessimistic hint when several slots are in flight.
Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green.
Thanks @pacocartones.
Four documentation claims contradicted the code: .env.example called OMNIROUTE_USE_TURBOPACK dev-only and said the production build still uses webpack (it reads the same flag and defaults to Turbopack); the README's Bun section said `bun run build` auto-detects Bun and switches to Webpack (only `bun run dev` does — the production bundler is decided by the flag alone); TROUBLESHOOTING.md gave OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT a default of 1 when unset means no request-count cap; and it quoted the pre-#12223 wording of the structural 503 chat_admission_busy message. The Retry-After bullet in the same section is deliberately untouched because #12395 rewrites it.
Validated in a combined worktree with all 25 PRs of this batch boarded together: typecheck:core clean, 443/443 node-runner tests plus 14/14 vitest across every test file the batch touches, and check-changelog-integrity, check:cycles (418 files), check:provider-consistency (272 REGISTRY entries, 355 canonical providers), check:docs-counts, check:docs-sync (42 locales) and check-file-size all green.
Thanks @pacocartones.
Adds uncensored.com as two OpenAI-compatible providers mirroring UC's own surfaces: uc, the persona/subscription side over WebSocket with a durable Clerk credential minting a short-lived per-connect token (no API key, un-metered), as a full multimodal port — chat, tools, vision, doc-RAG, image, video, TTS; and uc-direct, the metered Developer API over REST with X-api-key. Same underlying models, two billing surfaces.
Reconciled on merge. 57 files conflicted; only seven carried UC content, the rest was drift from the older release line and took the tip's side.
- executors/index.ts: the tip has since refactored the executor map to lazy dynamic imports, so uc is registered in that shape. uc-direct needs no entry — it routes through the default OpenAI-compatible executor.
- imageGeneration.ts: the branch still carried the retired designerWeb import alongside ucImage; kept only the UC one.
- config/providers/index.ts, webSessionCredentials.ts and web-cookie.ts resolved additively against the MaxAI entries #11461 put on the tip an hour earlier.
- web-cookie.ts: the uc entry declared no serviceKinds, required since #11392, so provider validation would have thrown at load. Declared ["llm"]. uc-direct already declared it at the end of its own entry — an earlier pass of mine added a second one after id and TypeScript caught the duplicate (TS1117); the author's placement is what shipped.
Every count was measured against the merged tree rather than taken from the branch, and all three would have been wrong: reserved prefixes are 406, not 399; APIKEY_PROVIDERS is 237, not 234; providers are 355. PROVIDER_REFERENCE.md regenerated, the count updated across README/AGENTS.md/llm.txt and its 42 mirrors, package.json and 6 SVGs — every changed line in the protected surfaces is a digit substitution and nothing else, verified by masking digits and comparing the removed and added sets (90 lines each, identical). The executor-map golden snapshot went 134 -> 135.
The branch's file-size-baseline.json predates #12411's ratchet re-tightening and was discarded rather than merged; imageGeneration.ts (+12 for the uc-image format branch) was entered against the current baseline under a _rebaseline annotation, and no other cap moves.
Verified: typecheck:core clean, check:provider-consistency OK (271 REGISTRY entries, 355 canonical providers), check:docs-counts exit 0, check-file-size OK, check:cycles OK, 119/119 across the PR's test files, and 2/2 executor-map-golden.
Thanks @arminanton — two providers for two real billing surfaces, rather than one entry pretending to be both, is the right modelling.
MaxAI joins as a first-class signed provider: 13 chat models discovered live from /models/get_config plus 6 image models, routed through the standard /v1 endpoints with per-request X-Authorization signing, browserless onboarding, prompted tool-calling, vision input, image generation and document RAG.
Reconciled on merge — worth reading, because the branch forked 227 commits back and 77 files conflicted. Only five carried MaxAI content; the rest was drift from the older release line and took the tip's side, taking the diff from 113 files to 37 (then 93 as counted against the current base).
- executors/index.ts: the tip has since refactored the executor map to lazy dynamic imports, so MaxAI is registered in that shape rather than the branch's static import.
- imageRegistry.ts: kept only the maxai block. The branch still carried microsoft-designer-web, which #11754 retired.
- models/route.ts: the conflicting hunk was an unrelated Vertex/Anthropic URL change, not MaxAI — tip's side.
- volcengine agent-plan/coding-plan registries: git auto-merged both sides and produced a duplicated supportsVision key, which TypeScript rejects (TS1117). Removed.
One real integration break that only the combined state shows: the MaxAI entry declared no serviceKinds, which #11392 made required a few hours ago. Provider validation threw at load time and check:provider-consistency crashed outright. Declared ["llm"] — the image kinds derive from imageRegistry, per the convention in that PR's backfill.
Every count was measured rather than taken from the branch, and each would have been wrong: reserved prefixes are 402, not the 397 the branch computed from its stale 395 base; providers are 353, not 354. PROVIDER_REFERENCE.md regenerated, the count updated across README/AGENTS.md/llm.txt and its 42 mirrors, package.json and 6 SVGs — every changed line in those files is a digit substitution and nothing else, verified by masking digits and comparing the removed and added sets (90 lines, identical). The executor-map golden snapshot was regenerated: keyCount 133 -> 134.
The branch's file-size-baseline.json predates #12411's ratchet re-tightening, so it was discarded rather than merged — taking it would have silently undone that. The three files this PR grows (proxyFetch.ts +20 for the Windows/firefox_150 TLS profile, imageGeneration.ts +12, models/route.ts +48) were entered against the current baseline under one _rebaseline annotation; no other cap moves.
Verified: typecheck:core clean, check:provider-consistency OK (269 REGISTRY entries, 353 canonical providers), check:docs-counts exit 0, check-file-size OK, check:cycles OK, and 79/79 across the MaxAI suites plus 21/21 reserved-prefix and 2/2 executor-map-golden.
Thanks @arminanton — the provider work itself is thorough; it was the 227 commits of base that needed the attention.
* feat(usage): devin-cli agentic quota + openrouter credits in Provider Limits
Two provider families with live quota APIs were missing from the Provider
Limits dashboard because their list entries were absent:
- devin-cli: new usage leaf querying the Codeium seat-management Connect API
(exa.seat_management_pb.SeatManagementService/GetUserStatus, protobuf over
POST with the raw `Basic <token>-<token>` auth header the CLI itself uses).
Surfaces the plan name plus daily/weekly agentic quota percentages with
reset timestamps from the GetUserStatus plan_status payload, via a minimal
hand-rolled protobuf encoder/reader (no proto dependency warranted for two
fixed messages).
- openrouter: the /key + /credits quota fetcher (#6842) was already wired
into the dispatcher but gated out of the bulk sync — add it to
USAGE_SUPPORTED_PROVIDERS and PROVIDER_LIMITS_APIKEY_PROVIDERS so key
limits and account credits actually surface.
* fix(build): externalize tiktoken so tiktoken_bg.wasm resolves at runtime
The vendored ChatGPT Web connector v4.0.7 (#12181) imports tiktoken
(get_encoding) at module level. tiktoken's node build reads
tiktoken_bg.wasm via a __dirname-relative fs.readFileSync during import;
when Next bundles the package the wasm asset is not traced into the server
chunk, and page-data collection for every route reaching the tokenizer
(e.g. /api/providers/[id]/chatgpt-web-codex-doctor) aborts with
"Missing tiktoken_bg.wasm" — breaking the whole standalone build.
Externalize it like the other runtime-resolved native/wasm packages
(sql.js, sqlite-vec, better-sqlite3): the require stays at runtime, where
node_modules/tiktoken/tiktoken_bg.wasm resolves normally.
* fix(openrouter): /credits balance survives a /key failure
OpenRouter is credit-based, not subscription-based: the authoritative
remaining-credits signal is GET /api/v1/credits (total_credits -
total_usage, the documented "get remaining credits" endpoint), while the
/key limit fields are optional per-key caps that most accounts never set.
fetchOpenrouterQuota previously treated /key as mandatory — any /key
failure (429 rate limit, transient error, unexpected shape) discarded the
whole payload and the Usage dashboard showed "OpenRouter (usage endpoint
unreachable)" even though /credits was reachable. Now:
- /key unavailable + /credits OK → credits-only quota (creditBalance =
total_credits - total_usage) instead of null
- /key 401/403 alone no longer means an invalid token; only a double
auth-rejection (both endpoints) does
- null is returned only when both endpoints fail, and the dashboard label
reflects that ("credits endpoint unreachable")
* fix(openrouter): render AI Credits as a USD credit count in Provider Limits
The Provider Limits card's dollar renderer only activates on
isCredits/creditCount rows (QuotaCardExpanded), but openrouter went through
parseGeneric — which drops `currency` and never sets those flags — so the
credits balance rendered as a meaningless "100% left" (the unlimited-credits
row is always 100%) instead of the actual credit count.
Route openrouter's `credits` quota through buildCreditsQuota() like the
DeepSeek/AgentRouter credits rows: label "AI Credits", dollar-formatted
balance. Free-tier request windows keep the generic percentage treatment.
* fix(usage): document DEVIN_SEAT_API_URL and split quota parsers
Keep fetchOpenrouterQuota and decodeProtoFields under the complexity
ratchets, and add the seat-management URL to the env/docs contract.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* test(usage): drop duplicated GLM quota-ordering test in provider-limits-ui
* test(usage): drop stale openrouter ACCEPTED_DIVERGENCE
OpenRouter is now in both USAGE_FETCHER_PROVIDERS and
USAGE_SUPPORTED_PROVIDERS, so the recorded aggregator divergence
is no longer real. Add the changelog fragment.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* perf(compression): memory and OOM mitigations for large payload hashing and token estimation
* fix(compression): implement getMemoStats observability for result memo (#7847)
Adds the missing memo observability layer referenced by
tests/unit/compression/oom-memo-memory.test.ts and the monitoring API:
- resultMemo.ts: lifetime hit/miss counters + bounded time-ordered ring
buffer (10k entries, ~90KB) powering 1m/5m/15m/1h hit-rate windows;
getMemoStats() reports size/capacity/hits/misses/hitRate + windows.
- memoLookup() tags served results with stats.memoHit = true.
- clearMemoStore() also resets counters and the ring.
- compression/index.ts re-exports getMemoStats for the monitoring route.
- types.ts: optional memoHit field on CompressionStats.
- New GET /api/monitoring/compression route exposing the stats snapshot
(lightweight, no DB) for operators to track cache-hit efficiency.
* fix(compression): align memo contract with upstream #11727 — return caller object, reset lookup counter in clearMemoStore
* fix(compression): restore unwrapEventEnvelope in stream payload collector summaries
The OOM-mitigation commit accidentally replaced unwrapEventEnvelope(evt.data)
with asRecord(evt.data) in the summary builders and live push, breaking
translate-mode {event, data} envelope unwrapping (clientPayload type detection)
and failing 2 stream-payload-collector tests. Restored upstream semantics;
kept the jsonLength OOM optimization as the only delta in this file.
* refactor(compression): break down writeValue and writeEncodedString to pass complexity ratchets
Refactors jsonSha256 internal helpers (writeValue, writeEncodedString)
into small, single-responsibility sub-functions under the complexity
threshold (max cyclomatic 15, max cognitive 15). Preserves exact
JSON.stringify parity, circular reference guards on both arrays and
plain objects, and escape behavior (all 530 relevant tests pass).
* test(compression): make oom-memo heap assertion robust without expose-gc
The CI unit-test shard runner does not pass --expose-gc, so global.gc is
undefined and heapUsed can still momentarily hold GC-pending transients
(observed 53.4 MiB after a 3MiB body). Gate the retained-heap assertion
on forced collection being available (3 forced cycles for array buffers)
instead of skipping it silently, and keep it fully active when
--expose-gc is present.
* fix(compression): restore worker-pool offload path in runCompressionAsync
The OOM-mitigation refactor dropped the isCompressionWorkerEligible /
runCompressionInWorker dispatch at the top of runCompressionAsync, silently
removing the base's worker-thread offload for eligible large payloads.
Restore the block exactly as on release/v3.8.51, ahead of the result-memo
path, keeping the memoization and hashing improvements intact.
* docs(api): document GET /api/monitoring/compression and log route errors via pino
Add the new monitoring endpoint to docs/openapi.yaml following the
neighboring System entries, and replace the route's console.error with
the repo-standard pino logger.
* fix(skills): regenerate omni-resilience and add changelog fragment
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Andrian Balanescu <AndrianBalanescu@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
The provider plugin manifest already exposed usage-fetch (40 providers, #11903); this publishes the second capability, usage-supported, so integrators can tell without reading TypeScript whether the server usage routes accept a provider. #11903closed#11722 after shipping only half of it and said so at the time — this is the follow-up it promised.
The two scopes genuinely differ and the docs now say how: usage-fetch resolves on id or alias (the dispatcher accepts both), usage-supported on id alone, because the runtime guard does a plain USAGE_SUPPORTED_PROVIDERS.includes(providerId) with no alias resolution. 42 providers carry both tags, 4 carry only usage-fetch (opencode, opencode-zen, openrouter, xai) and 3 only usage-supported (adobe-firefly, firefly, xiaomi-mimo-token-plan) — 7 measured differences, so neither tag implies the other. No list mutation, no new route, schemaVersion stays 1.
USAGE_SUPPORTED_PROVIDERS moved out of src/shared/constants/providers.ts into an import-free leaf at open-sse/services/usage/supportedProviders.ts, keeping the manifest's import graph light — the same move fetcherProviders.ts got in #11903, landed on the correct side of the workspace boundary.
Base note: the branch forked 46 commits before kilocode joined the list, so a wholesale take of its providers.ts would have silently dropped that id. Verified against the current release tip before merging — both sides hold the same 46 ids, nothing lost.
Verified on the current tip: typecheck:core clean, check:cycles OK across 417 files (the import-free-leaf claim holds), and 63/63 focused tests across provider-plugin-manifest, usage-fetcher-registration-coverage, adobe-firefly and agentrouter-quota-dashboard-rendering.
Thanks @maxmad64bis — and for finishing the half of #11722 that was left open rather than letting it sit.
The weight table said stability accounts for "low latency stdDev / error rate". Grep errorRate in scoring.ts and you find it declared on ProviderCandidate and read nowhere — while combo.ts pulls 24 hours of usage history behind a ten-sample floor, falls back to real-time metrics, and hands every candidate an errorRate the scorer ignores. Two candidates, one failing 1% of calls and one failing 99%, scored identically at 0.459486.
This declares reliability as a sixteenth factor: 1 - failureRate, using the same formula, field precedence and rate-bounding speedRanking.ts already applies, so a corrupt reading means "nothing observed" rather than "fails every call". It ships at weight 0, leaving the ranking unchanged to the digit — the honest default, since which weight this deserves is a product call backed by traffic the author does not have. Two declared-but-silent factors already ship (cacheAffinity, resetWindowAffinity), so the pattern is not new. The stability row now describes what that factor actually computes: latency variance.
The rest is the mechanical 15 → 16 across nineteen documents and the forty-two llm.txt mirrors — sourced from check:docs-counts rather than a grep, the first real use of the gate #12316 extended.
Protected-surface note: this PR touches AGENTS.md, llm.txt and its 42 mirrors, and skills/omni-combos-routing/SKILL.md. Every changed line in those 45 files is a digit substitution and nothing else — masking all digits makes the removed and added lines identical, with no sentence added, removed or reworded. Reviewed and approved on that basis before merging.
Verified on the author's rebased head: check:docs-counts green (the gate that now enforces the count this PR moves), typecheck:core clean, and 71/71 focused tests across scoring-reliability-factor, combo-scoring-weights-schema-coverage, check-docs-counts-sync, lkgp-enabled-context, intelligent-routing-options and the combo-matrix auto integration suite.
Thanks @maxmad64bis — shipping the factor at weight 0 and saying plainly that the weight is someone else's call is the right way to land this.
docs/reference/FREE_TIERS.md said its numbers were "gathered by web research (confidence tagged per row)". No entry carries one: grep -c confidence on the catalog returns 0, the type does not declare the field, and the API serves nothing of the sort. A reader looking for "how much can I trust this figure" was pointed at a per-row signal that never existed.
Replaced with the two counts the data actually supports — 7 of 446 entries carry hardStopGuaranteed (the field with the strictest sourcing rule in the repo: set only when the provider's own terms document that exceeding the free allowance refuses the request, source in a comment, never defaulted to true) and 13 carry a prompt-training disclosure. check:docs-counts reads both from the catalog at runtime, as required claims, so a reworded or deleted sentence fails rather than passing as "no claim in this file".
The PR deliberately does not add a confidence field — curating one is a product call, and it says so instead of inventing it.
Reconciled on merge: #12316 landed the gate extension underneath, so scripts/check/check-docs-counts-sync.mjs and its test took the tip's side plus this PR's own required-claim additions. Verified afterwards: check:docs-counts green, 48/48 across check-docs-counts-sync and free-catalog-no-confidence-field.
Thanks @maxmad64bis — checking the gate against a number it should reject (7 swapped for 99) is the right way to prove a gate works.
docs/routing/AUTO-COMBO.md documented four mode packs; six ship. It printed 0.14 where modePacks.ts says 0.1333. And nothing was watching: four documents stated a scoring-factor count and check:docs-counts covered none of them. Wiring them up turned the gate red on seven real drifts — ARCHITECTURE.md and REPOSITORY_MAP.md at "9-factor" (code: 15) and "4 mode packs" (code: 6), RESILIENCE_GUIDE.md and SKILL.md at 13, AUTO-COMBO-GUIDE.md at both 5 and 13. ARCHITECTURE.md did not merely have the wrong number: it named nine factors that are not the engine's, and its four "mode packs" were the auto/* request prefixes.
A product fact fell out of writing the table: no pack sets quality, and applying a pack replaces the weight map wholesale (weights = pack in engine.ts, not a merge), so quality carries 0.03 by default and normalizes to 0 under any pack — pick a mode pack and the observed-quality signal stops voting. Documented, not changed.
The gate reads pack names from the module through the tsx subprocess that already reads every other code-derived count, matching the three spellings the docs actually use; on the reference document a missing claim now fails rather than passing. The dashboard was behind too (four of six packs offered); the count is dropped from the strategy label rather than corrected, since nothing reads selector labels and a right-today number goes stale unnoticed.
Verified in a combined batch worktree: 174/174 focused tests across all 11 PRs of this batch, typecheck:core clean, and check:docs-counts green with the four newly-wired documents.
Thanks @maxmad64bis — finding the quality-under-a-pack behaviour while writing a docs table is the kind of thing a table is for.
With freeAccessPolicy: "strict" the read-only candidate listing silently dropped rows, so an operator could not tell "no free allowance left" from "the quota fetcher is broken" — in a listing whose own module header promises a candidate the routing path would skip "is never dropped". #9133 settled the same question for the resilience filter via a skip opt-out; the zero-cost guard never got one.
It gets it now: the guard is disabled for the inspector build exactly as the resilience filter already is, and every candidate carries freeAccessExclusion — null when satisfied, otherwise one of seven named reasons. The last three (exhausted, state-unknown, no-connection) are the point: they used to look identical because the row just disappeared. STRICT_ZERO_COST.md documents what each asks the operator to do.
Dispatch is untouched and a test pins that. The three existing guard suites were not modified — their 31 cases are the net and still pass. excludeTosAvoid still drops candidates without a reason; documented as a separate question rather than widened into here.
Verified in a combined batch worktree: 174/174 focused tests across all 11 PRs of this batch, typecheck:core clean, complexity 2706/3218, cognitive-complexity 1221/1437, check:cycles and check:docs-counts green.
Thanks @maxmad64bis — the reason table and the honest note about the order change (freshness before status) made this easy to review.
* feat(sse): add it/ru/zh caveman output instructions
* fix(sse): expose the dormant terse-prose translations through the catalog
* feat(sse): translate less-code to es/de/fr/it/ru/zh
* feat(sse): translate ponytail to es/de/fr/it/ru/zh
* feat(sse): translate i-have-adhd to es/de/fr/it/ru/zh
* test(sse): anchor wave-2 output-style translations to their own language
* feat(dashboard): offer every output-style language in the default-language selector
* feat(sse): let autoDetect pick the output-style instruction language
* docs(compression): consolidate the output-style tables and record full language parity
The 1proxy marketplace integration was decommissioned in v3.8.4; the code
survived only through the localDb barrel, deleted in #12055. Everything below
had zero consumers (grep-proven across src/, open-sse/, bin/, electron/,
scripts/ and tests/):
- src/lib/oneproxySync.ts and src/lib/oneproxyRotator.ts deleted.
- src/lib/db/oneproxy.ts: upsertOneproxyProxy, getOneproxyProxyById,
getOneproxyProxyForRotation and markOneproxyProxyFailed removed (their only
consumers were the two deleted modules); listOneproxyProxies and the record
interface stay — open-sse/utils/proxyFallback.ts still uses them.
- src/shared/validation/oneproxySchemas.ts and the unmounted
settings/components/OneproxyTab.tsx deleted (no importer anywhere; the live
UI is the FreePool* tabs over /api/settings/free-proxies).
- ONEPROXY_ENABLED feature flag removed (readerless since oneproxySync died —
the toggle no longer controlled anything); flag-count contract test aligned
54 → 53.
- Docs: PROXY_GUIDE (component rows, env rows, the three omniroute/
oneproxyRotator snippet sections), CODEBASE_DOCUMENTATION, REPOSITORY_MAP,
ENVIRONMENT (ONEPROXY_* rows), FEATURE_FLAGS, .env.example — canonical +
pl/zh-CN/zh-TW mirrors.
- The 308 compat redirects under /api/settings/oneproxy/ stay (deliberate API
compat), as do the live free-proxy provider and proxy_registry rows.
check:dead-code drops 424 → 417 (baseline kept at the velocity-phase 500 —
banking shrinks is paused until v4.0, headroom grows to 16.6%).
check:docs-all, check:env-doc-sync, typecheck:core and the 8 free-proxy/
proxy-fallback test files are green.
Closes#12091