The ALWAYS_PROTECTED two-arm read itself landed in #12605; what was still
missing is a regression guard. This runs the real gate and asserts it exits 0
with no "NOT covered" line, so the LOCAL_ONLY-arm defect (#12350) cannot
silently reappear on the ALWAYS_PROTECTED arm. Also fails the parse guard when
ALWAYS_PROTECTED_API_PATTERNS comes back empty, instead of reporting every
regex-covered route as an annotation mismatch.
Merged — one line, zero risk, and it costs nothing to have.
One caveat recorded so nobody later reads this as "#11544 is solved": npm resolves config from the *installing* project's directory, the user config and the global config — it does not read the `.npmrc` shipped inside a dependency's tarball. So `legacy-peer-deps=true` traveling in the package will not change how `npm install -g omniroute` resolves peers on the consumer side. Our own `scripts/build/postinstall.mjs` does shell out to `npm rebuild` / `npm install better-sqlite3`, but with cwd set to `dist/`, so the package-root `.npmrc` is not in scope there either.
Keeping it anyway: it makes the published tree self-documenting, and someone debugging inside an extracted package gets the same retry budget we use in CI. But #11544 (`npm install -g omniroute` failing on Windows, "root cause unclear from log") still needs the actual `npm-debug.log` from the reporter before it can be closed.
Rebased onto `release/v3.8.51`; `package.json` re-parses and the `files` array kept both `config/i18n.json` and the new entry. Thanks.
Merged. One line in `overrides`, low blast radius, and pinning a transitive that every build tool reads is defensible on its own.
Validated on `release/v3.8.51`: `package.json` re-parses, `typecheck:core` clean, `check-file-size` OK.
For future dependency pins, a line in the body about what the floating range actually broke (a specific build failure, a CVE, a resolution conflict) makes these reviewable without guessing. Thanks.
Merged. Verified all four pins resolve on npm before landing:
```
@openai/codex@0.153.2 0.153.2
@anthropic-ai/claude-code@2.1.260 2.1.260
droid@0.212.0 0.212.0
openclaw@2026.9.1 2026.9.1
```
The reproducibility argument holds — a floating `@latest` in a cached Docker layer means two builds of the same commit can ship different toolchains, and that is exactly the class of drift that makes a CI failure unattributable.
Worth flagging for whoever maintains this next: pinning trades drift for staleness, so these four now need a periodic bump or the image ships increasingly old CLIs. The comment block you added explains the why, which makes that bump a safe mechanical change instead of a judgment call.
Rebased onto `release/v3.8.51` (the PR was cut from `main`, ~3695 commits behind). Thanks.
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.
Merged, with one sentence removed.
The `socket.yml` half checks out: the file exists at the repo root, is `version: 2`, and its `projectIgnorePaths` really do list `tests/`, `_tasks/`, `_references/`, `_ideia/`, `_mono_repo/`, `docs/` — so the paragraph describes the config accurately.
The closing sentence did not: there is no `.github/workflows/socket-dev.yml` in this repo (`ls .github/workflows | grep -i socket` is empty), and nothing auto-opens `supply-chain-review/` issues. Per the documentation-accuracy rule in `AGENTS.md` — every path and workflow named in docs has to survive an `rg`/`ls` — I replaced it with what is actually true: the scan is driven by the Socket GitHub App reading `socket.yml`, not by a workflow here.
Everything else merged as written. Thanks — pointing readers of SECURITY.md at the scanner config was a real gap.
Merged, with the fix moved to where the bug actually lives — and thank you, because the issue analysis in #12505 is what made that possible.
The diagnosis was right: `FeatureFlagsGrid.tsx:422-428` renders descriptions through a plain `t()`, so next-intl compiles the value as ICU and a bare `<name>` parses as an unknown rich-text tag. But the branch changed `src/shared/constants/featureFlagDefinitions.ts` — the TS default, which is the `flag.description` **fallback** rendered raw, never through ICU. Two consequences: the reported bug stayed live (all 42 locale files still carried the raw tag — `grep -l "profiles/<name>/settings.json" src/i18n/messages/*.json` returned 42, and 0 for the escaped form), and the quotes would have shown up literally in the one place that string does render.
So this merge reverts the TS default to the raw path and applies the ICU escape to the 42 locale files instead — follow-up 1 from your issue, inverted to hit the file that matters.
I also added follow-up 2 as a real guard: `tests/unit/feature-flag-description-icu-parse-12505.test.ts` compiles every `featureFlags.definitions.*` message in every locale through `intl-messageformat` (the parser next-intl uses) and asserts the placeholder renders as a literal `<name>`. Verified red-then-green — reverting `en.json` alone fails both cases; restored, 2/2 pass.
Validated on `release/v3.8.51`: all locale files re-parse as valid JSON, `typecheck:core` clean, `check-file-size` OK. `i18n:check` drift is pre-existing on the tip, unrelated.
Closes#12505.
Merged, with one adjustment.
Confirmed the bug end to end: `PricingTab.tsx:369` types the payload as `{ error?: string }` and feeds it to `new Error(errorPayload.error || ...)`, so the `{ message, details }` object landed in the toast as `[object Object]` — exactly what #12494 reported.
The one change I made before merging: `validation.error.message` is the fixed constant `"Invalid request"` (see `validateBody` in `src/shared/validation/helpers.ts:44`), so it would have swapped an unreadable toast for an uninformative one. The repo already has `formatValidationMessage()`, added in #10849 for precisely this case — it returns `"field: reason"` naming the first offending field. Merged with that instead, so a bad pricing value now says which field it was.
Validated on `release/v3.8.51`: `typecheck:core` clean, `check-file-size` OK. Rebased onto the release branch — the PR was cut from `main`, which is ~3695 commits behind the active branch.
Thank you for the report and the fix.
Merged. Clean, surgical fix with its own regression guard.
`ensureProviderConnectionsColumns()` reconciles the base columns that later data migrations assume, but `last_ping_at` / `last_pinged_reset_key` were only ever created by `123_quota_auto_ping` — so a lineage that skipped it kept a table that the quota auto-ping writes cannot target. Adding them to the reconciliation list is exactly the right place.
Validated on `release/v3.8.51`: `tests/unit/db-schema-columns-split.test.ts` 10/10, including your new `back-fills last_ping columns on a pre-123 lineage` case and the idempotency re-run. `typecheck:core` clean, `check-file-size` OK. The `changelog.d/fixes/` fragment was already correct.
Thank you — this is the shape a fix should have: root cause named, minimal diff, test that fails without it.
* fix(ci): mirror isLocalOnlyPath in the security-tier gate and rebaseline four merged-growth file caps
Two base-reds on release/v3.8.51 (#12581), both drained at the source.
1) check:openapi-security-tiers reported six CORRECTLY annotated routes as
unprotected and demanded the removal of their x-loopback-only annotation —
pushing the fix in the unsafe direction. The gate re-reads routeGuard.ts as
text (it cannot import the module: routeGuard pulls the server runtime and
the gate runs on plain node), but it only read the FIRST half of
isLocalOnlyPath():
LOCAL_ONLY_API_PREFIXES.some(...) || LOCAL_ONLY_API_PATTERNS.some(...)
so every route gated by a regex (/api/providers/volcengine-plan/connect/*)
or by an imported constant (VNC_ROUTE_PREFIX, which the text parse turned
into the literal string "VNC_ROUTE_PREFIX") looked open. Proven with
isLocalOnlyPath() at runtime: all six return true; the control
/api/providers/{id}/refresh stays false.
New scripts/check/routeGuardConstants.mjs reads BOTH arrays, resolves
imported identifiers by following the import, and THROWS on an unresolvable
token instead of silently degrading it into a literal. Its array scanner is
hand-rolled because regex literals carry the brackets and commas a
\[([^\]]+)\] capture plus a naive comma split break on ([^/] and {1,3}).
The reverse pass (missing-annotation warnings) now uses the same predicate.
2) check:file-size: four frozen files grew past their cap through merged PRs —
chat.ts +10 (#12427/#12503 video-transcript redaction, derived from the
post-guardrail payload at the single dispatch point) and stream.ts /
accountFallback.ts / codex.ts +17 total (#12179 hot-path regex hoisting,
bounded caches, quadratic-buffering fix). All cohesive at existing
chokepoints; rebaselined with the rationale recorded in the baseline file.
Refs #12581
* fix(ci): security-tier gate must honor ALWAYS_PROTECTED_API_PATTERNS too
#12350 fixed the LOCAL_ONLY half of the checker (prefixes + patterns +
imported consts). isAlwaysProtectedPath() is two-armed the same way:
ALWAYS_PROTECTED_API_PATHS.some(...) || ALWAYS_PROTECTED_API_PATTERNS.some(...)
but the checker still read only the path array, so the four credential
routes gated by the GHSA-5926-2w35-7h4q pattern (#12600) —
/api/providers/{id}/{claude,codex}-auth/{export,apply-local} — reported as
'has x-always-protected but is NOT in ALWAYS_PROTECTED_API_PATHS', asking for
the removal of a CORRECT annotation on a credential-export route.
Verified with the real predicate: all four isAlwaysProtectedPath() → true;
control /api/providers/{id}/models → false.
tests/unit/openapi-security-tiers.test.ts already checks BOTH arrays (#12600
updated the test but not the gate script) and stays green — this commit makes
the gate agree with the test and with the runtime.
Also carries the file-size rebaseline for four caps grown by merged PRs
(chat.ts +10 from #12427/#12503; stream.ts / accountFallback.ts / codex.ts
+17 from #12179), rationale recorded in the baseline file.
Refs #12581
* fix(ci): re-anchor the zcodeProtocol public-creds allowlist entry (302 -> 313)
The check:public-creds allowlist pins each frozen literal by FILE:LINE, so
#12179 (hot-path regex hoisting in the same file) shifted the ZCode handshake
id from L302 to L313 and broke the gate twice over: the old entry went stale
('a violação foi corrigida; REMOVA a entrada') while the literal itself, now
at L313, was no longer covered.
The literal is unchanged and still not a credential: `omniroute-${process.pid}`
is a per-process handshake id for the local ZCode app-server, already audited
and frozen with that justification. Only the anchor moves.
Refs #12581
* test(ci): re-anchor the ZCode allowlist test to L313 alongside the gate entry
The allowlist key is file:LINE:value, so the synthetic source in this test
pads to the exact line the entry pins. Re-anchoring the entry 302 -> 313
(previous commit) without moving the padding left the test asserting the old
line — caught by Unit Tests fast-path (4/4) on #12605.
Both halves now sit at 313, and the test still proves the allowlist does NOT
weaken detection: swapping the value for 'upstream-client-' is still flagged.
Refs #12581
* docs(ci): changelog fragment for #12605
* chore(ci): trim #12605 to the one fix the base still needs
The base drained fast while this PR was open. Re-verified on 008da6d19a and
dropped everything already covered there:
- check-public-creds.mjs: the base already re-anchors the ZCode entry to L313
(my commit only added a comment on top) -> reverted to the base version.
- file-size-baseline.json: the base rebaselined chat.ts/codex.ts/
accountFallback.ts to HIGHER caps than mine, and stream.ts measures 3064
against the base cap of 3072 — my 3078 bump would have loosened a cap for
no reason -> reverted to the base version.
What the base still does NOT have, verified on its current tip:
node scripts/check/check-openapi-security-tiers.mjs -> EXIT=1, 4 mismatches
so the ALWAYS_PROTECTED_API_PATTERNS half stays, plus its changelog entry.
Refs #12581
Validado sobre o tip de `release/v3.8.51`, com duas coisas resolvidas antes do merge.
**A falha de CI era stale.** O job `No new ESLint warnings` deste PR apontava `react-hooks/set-state-in-effect` em `src/app/(dashboard)/dashboard/combos/page.tsx:774` — arquivo que este PR não toca, e o mesmo erro aparecia em #12668 e #12672, que também não o tocam. A linha do tempo: o #12355 introduziu a violação de manhã, os CIs rodaram nessa janela, e o #12607 acrescentou a entrada de supressão à tarde. Medido no tip atual com o comando exato do job: **0 ocorrências não suprimidas**. A supressão sobrevivente é "unpruned", e o script passa `--pass-on-unpruned-suppressions` justamente para isso não bloquear.
**Faltava o teste que a regra do projeto exige** para mudanças em `src/`. Acrescentei `tests/unit/ui/log-detail-conversation-link-12646.test.tsx`, verificado **RED-then-GREEN** em vez de escrito contra o código pronto: revertendo `RequestLoggerDetail.sections.tsx` para o tip, 2 dos 3 casos falham; com a mudança deste PR, 3/3 passam.
Detalhe que valeu a pena descobrir: a seção curto-circuita em `allTurns.length === 0`, então o fixture precisa de um `requestBody` que normalize em pelo menos um turno — sem isso o cabeçalho inteiro nunca monta e as asserções passariam pelo motivo errado. O teste fixa três coisas: o href para um `sessionTag` simples, o percent-encoding para um que não é URL-safe, e a ausência de link quando não há `sessionTag`.
Obrigado, @hartmark.
* 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 3 PRs desta leva sobre o tip de `release/v3.8.51`: os três boardaram sem conflito, `typecheck:core` limpo e **22/22** nos arquivos de teste que trazem.
O crescimento de `src/sse/handlers/chat.ts` (2450 → 2454) é do #12641 e vai num PR de rebaseline próprio.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 3 PRs desta leva sobre o tip de `release/v3.8.51`: os três boardaram sem conflito, `typecheck:core` limpo e **22/22** nos arquivos de teste que trazem.
O crescimento de `src/sse/handlers/chat.ts` (2450 → 2454) é do #12641 e vai num PR de rebaseline próprio.
Obrigado, @hartmark.
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 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 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 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.
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.
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.
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.
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.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
Validado em lote numa worktree combinada com os 6 PRs destas duas levas sobre o tip de `release/v3.8.51`: os seis boardaram **sem um único conflito**, `typecheck:core` limpo e **54/54** nos 7 arquivos de teste que trazem.
O drift de `i18n:check` (`docs/security/GUARDRAILS.md`, `STEALTH_GUIDE.md` — source-changed) foi medido também no tip puro e é idêntico: base-red pré-existente, não desta leva.
* 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.
Rebaseline medido no tip com os 14 PRs da campanha mergeados. A anotação registra que 4 das 6 linhas do codex.ts são drift anterior à campanha, não crescimento dela. Não toca stream.ts.
Validado após reconciliar com o #12465, que entrou primeiro e criou o mesmo arquivo novo `open-sse/utils/streamReadiness.ts` com desenho divergente de cancelamento.
Mantive a versão desta branch, que defere o release do lock para quando a leitura em voo termina e faz `reader.cancel()` fire-and-forget — assim uma promise de provider que nunca resolve não torna o cancelamento ilimitado. A escolha não foi por preferência: rodei as suítes dos **dois** PRs contra ela, 21/21 no readiness compartilhado e **22/22** incluindo o boundary do Perplexity do próprio #12465. typecheck:core limpo.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Validado em worktree combinada sobre o tip de release/v3.8.51: os dois boardaram sem conflito, typecheck:core limpo, check-file-size sem violação nova (as duas restantes — codex.ts e stream.ts — são drift anterior) e 51/51 nos 5 arquivos de teste que os PRs trazem.
Validado em worktree combinada sobre o tip de release/v3.8.51: os dois boardaram sem conflito, typecheck:core limpo, check-file-size sem violação nova (as duas restantes — codex.ts e stream.ts — são drift anterior) e 51/51 nos 5 arquivos de teste que os PRs trazem.
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.
`check:public-creds` has been failing on every open PR against release/v3.8.51,
twice over for the same literal:
✗ 1 entrada(s) obsoleta(s) na allowlist — zcodeProtocol.ts:302
✗ 1 credencial(is) pública(s) como string literal — zcodeProtocol.ts L313
Both are the same `clientId: \`omniroute-${process.pid}\`` in the local ZCode
handshake. Nothing regressed: the allowlist key is `file:line:value`, so an edit
that shifted the statement from 302 to 313 invalidated the frozen key and the
gate reported the entry as stale AND the literal as new.
Re-pointed the key and its comment. The literal itself is unchanged and still
frozen — the entry is not removed and the detector is not weakened (the gate's
own test still asserts that renaming the value to `upstream-client-` is flagged).
`tests/unit/check-public-creds.test.ts` synthesizes the source with a newline
count to land the statement on the allowlisted line; that count moves with it,
302 -> 313, so the test keeps pinning the real contract instead of a stale one.
Also documented the sharp edge inline: keying by line number means any edit near
this statement breaks the gate in two places at once, and the fix is to re-point
the line, never to drop the entry. Tightening the key to `file:value` would
remove the trap but widens what the entry freezes, so it is left as a note
rather than folded into a base-red drain.
check:public-creds OK (3 frozen literals), check-public-creds tests 20/20,
check:tracked-artifacts OK, prettier clean.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Validado em lote numa worktree combinada com os 9 PRs desta leva sobre o tip de `release/v3.8.51` (já com a leva anterior dentro): os nove boardaram **sem um único conflito**, `typecheck:core` limpo e **80/80** nos 6 arquivos de teste que os PRs trazem.
O crescimento de arquivo próprio da leva foi rebaselinado num registro datado (`_rebaseline_2026_09_03_hartmark_batch`): `combos/page.tsx` 5012→5018 (#12355, tratar o estado degradado quando o bundling de tiktoken de um provider sem relação falha) e `open-sse/services/combo.ts` 4023→4036 (#12338, os fixes do universal-handoff). 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.
Obrigado, @hartmark.
Rebaseline medido no tip com os 9 PRs da leva mergeados. Desfaz o vermelho de file-size que os PRs empilhados deixaram; não toca codex.ts nem stream.ts, que já violavam antes da 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.
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.
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.
* feat(video): redact transcript fields in the in-memory pending-request snapshot (#12430 item 6)
trackPendingRequest (open-sse/handlers/chatCore.ts) stored the raw client
body (with video transcript/audioTranscript cues) under `clientRequest`,
live-exposed via /api/usage/call-logs (pendingDetails), /api/logs/[id] and
/api/conversations while a request is in-flight. P2a redacted the persisted
detailed-log snapshot but not this in-memory copy.
Add redactPendingBody() to videoBridgeSnapshotRedaction.ts (sibling to
logClientRawRequestRedacted from P2a): when videoBridgeObserved, returns the
redacted clone from redactVideoTranscriptFieldsForLog; otherwise returns the
exact same reference. Wire it into the trackPendingRequest call site
(chatCore.ts:934), keeping the file within its frozen 5976-line budget
(5971 -> 5974).
* feat(video): substring-redact transcript in derived-prompt dispatch logs (#12430 item 4)
Extend applyVideoBridgeLogRedaction with a string-content branch:
pipeline-strategy stages, smart-auto-pipeline, and context-handoff
summaries embed the transcript as a substring of a rendered prompt
string rather than an exact array part, so the existing exact
part-array match silently skipped them. Adds a mutually-exclusive
string branch (Array.isArray vs typeof === "string") that does a
replaceAll of the trusted fullText literal against a lazily cloned
message, reusing the existing rootClone/clonedContainers/clonedMessages
clone-on-write pattern so siblings keep original references and the
input is never mutated.
Aprovado pelo operador. Adiciona o OmniRouteTray (@zoispag) ao README — app de menu-bar para macOS, rotulado com honestidade como projeto da comunidade e não release oficial. Mudança só de markdown, sem tocar nada executável; inclui também dois ajustes de alinhamento na tabela de contatos. Obrigado, @ggiak.
* 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 numa worktree sobre o tip de `release/v3.8.51`, medindo o gate dos dois lados: **red no tip** (dezenas de rotas `volcengine-plan`/`vnc-session` reportadas como "has x-loopback-only but is NOT covered") e **PASS com este PR**, exit 0.
Como é um gate de segurança, confirmei que o fix torna o checker *preciso* e não *frouxo*. A afirmação central do PR — que uma rota é coberta se casar com um prefixo resolvido **ou** com um pattern — bate exatamente com o runtime (`src/server/authz/routeGuard.ts:252-255`):
```ts
return (
LOCAL_ONLY_API_PREFIXES.some((p) => path === p || path.startsWith(p)) ||
LOCAL_ONLY_API_PATTERNS.some((re) => re.test(path))
);
```
O checker antigo enxergava só o primeiro braço, e nem isso por completo: a captura `[^\]]+` quebrava no `]` dentro de classes de regex, então `LOCAL_ONLY_API_PATTERNS` não era parseado, e `VNC_ROUTE_PREFIX` (const importada, não literal) não era resolvido. Resultado: rotas efetivamente protegidas em runtime apareciam como desprotegidas. Nenhum achado real foi silenciado — as 95 linhas de `WARN — missing x-loopback-only annotation` continuam saindo, são explicitamente não-fatais e pré-existentes.
Fecha um dos HARDs do base-red #12335. Obrigado, @ggiak.
Dependabot alerts #196–#199 — four HIGH advisories on fast-uri
(GHSA-jqff-g426-hqxp, GHSA-fph4-wmhf-6fwf, GHSA-f65p-4m7j-42xc,
GHSA-5jgf-p345-68v8), all patched in 3.1.6.
The root package-lock.json was already on a patched fast-uri (3.1.7) — those
alerts close on their own with the next scan. `electron/package-lock.json` is a
second lockfile and was still pinning 3.1.5, which is what these four alerts are
actually reporting.
Transitive, one copy, pulled by ajv (`^3.0.1`), so a package-lock-only update
lifts it without touching any manifest. The diff is three lines: version,
resolved and integrity for that single entry.
check:lockfile and check:tracked-artifacts pass.
Not fixed here: extract-zip (#191, HIGH, <= 2.0.1) has no published patch. It
comes in through @openai/codex-security and is dev-scope; it needs either an
upstream release or a decision to drop/replace the dependency, neither of which
belongs in a lockfile bump.
Validado numa worktree sobre o tip de `release/v3.8.51`. Verifiquei as premissas em vez de aceitar a justificativa:
**Exceções de licença** — todas as afirmações batem. Os três pacotes estão instalados exatamente nas versões travadas citadas (`@eloqnt/config@0.0.2`, `@eloqnt/format-json@0.0.3`, `@eloqnt/format-po@0.0.3`), os três **omitem `package.json#license` e não têm arquivo LICENSE**, e `npm ls` confirma que são transitivos de `next-intl@4.14.1` (MIT). No registry, o SPDX das três é MIT e o `@eloqnt/config@0.1.0` existe, como o texto diz. Uso de `exceptions` (com `risk`/`reviewAt`) em vez de `allowed: UNKNOWN` é o mecanismo certo.
`check:licenses` verde: **0 violações de política**, 948 permitidos, as três novas entradas aparecendo como exceções sinalizadas não bloqueantes ao lado das duas já existentes. Teste-guarda `tests/unit/build/check-licenses.test.ts`: **36/36**.
**Vitest do A2A lifecycle** — as duas seams usadas já existiam antes deste PR: `constructor(ttlMinutes = 5, persistence: A2APersistence = defaultPersistence)` e o 4º parâmetro opcional `deps?: MemoryHitsDeps` de `executeA2ATaskWithState`. O padrão é idêntico ao que `tests/unit/a2a-task-persistence.test.ts` já fazia. Nenhuma asserção foi removida ou enfraquecida — e como o comportamento de persistência tem suíte dedicada, injetar no-ops aqui tira acoplamento incidental, não cobertura.
Ganho medido nos dois lados: corpo dos testes **784ms no tip → 233ms com o PR**, sem nenhuma linha `[DB] SQLite database ready`. Registro honesto: **no tip o arquivo passa localmente** (4/4) — o red era do CI, sob o thread pool com as 167 migrações; localmente dá para comprovar o mecanismo e a aceleração, não a falha em si.
Suíte vitest completa: **51/51 arquivos, 465/465 testes**.
CI verde: 18 checks passando (4 shards de unit, Vitest, CodeQL, Fast Quality Gates, No new ESLint warnings, semgrep). Local: 8/8 no teste atualizado e 165/165 nos 16 arquivos tests/unit/electron-*.test.ts.
Validado sobre o tip de `release/v3.8.51` após reconciliar quatro arquivos que driftaram. Em parte o tip já tinha absorvido a intenção deste branch por abstrações melhores, então mantive a forma do tip e trouxe os ganhos que ainda eram reais:
- **`sseCollect.ts`** — o tip extraiu `stripObfuscationZeroWidth()` para `utils/zeroWidth.ts`, o que supera o `ZERO_WIDTH_RE` local (removido). A içada de `TEXTUAL_TOOL_CALL_RE` foi mantida: essa regex ainda estava inline num caminho quente.
- **`resultMemo.ts`** — o `memoStore()` do tip devolve o clone armazenado para que o idiom comum `memoStore(k, r); return memoLookup(k)!` evite um segundo deep-clone de vários MB. Esse contrato foi preservado (o branch o revertia para `void`), e o round-trip `JSON.parse(JSON.stringify())` virou `structuredClone()` nas duas pontas — que era o ponto de performance real do branch.
- **`browserPool.ts`** — o tip agora tem caminho headed e o engine Obscura (#12286). Ambos preservados, mais a varredura de TTL do `pendingContexts` deste branch, adaptada ao nome `poolKey` do tip.
- **`executeAttempt.ts`** — mantido o comentário explicativo do tip.
Também corrigi **quatro erros de typecheck que o branch introduzia**: `hasUnsupportedSignal` estava tipado `boolean` mas avaliava para `string | boolean`, e o fast-path de `extractUsage()` indexava `c.response`/`c.message` como `unknown`.
`typecheck:core` limpo e **482/482** nos testes de antigravity + compressão na própria branch. Obrigado, @opensource-elearning.
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 sobre o tip de release/v3.8.51: boardou sem conflito, typecheck:core limpo e 17/17 em tests/unit/combo-context-window-filter.test.ts. Obrigado, @opensource-elearning.
* feat(video): redact raw client-snapshot transcript fields in the detailed log (#12150 P2)
clientRawRequest.body is captured before the guardrail chain runs and persisted
verbatim by reqLogger.logClientRawRequest, so it retained the client's raw
transcript/audioTranscript cue text on video parts even after P1's description
redaction. Add redactVideoTranscriptFieldsForLog (new, dependency-light module)
and wire it at the logClientRawRequest call site, gated on videoBridgeObserved:
redacts the structured transcript fields in the LOGGED copy only, never the
body sent to the provider or returned to the client.
* refactor(video): extract the guarded client-snapshot log call to keep chatCore within its size budget
Fast Quality Gates check:file-size flagged chatCore.ts growing past its
frozen ceiling (5985 > 5976) from the P2a wiring. Move the guarded
logClientRawRequest call into logClientRawRequestRedacted (new export
in videoBridgeSnapshotRedaction.ts, which already owns the redaction),
collapsing the inline if-block at the chatCore.ts call site to a single
call. Net -4 lines vs the pre-P2a base. Behavior unchanged: non-observed
still logs the exact same clientRawRequest.body reference; observed
still logs the redacted clone.
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.
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.
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.
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.
Bump MAJOR electron 43.4.1 → 44.0.0 (Chromium 152, Node 24.18.1, V8 15.2), aprovado pelo operador após análise dos breaking changes contra o código real:
- **Remoção dos builds 32-bit (Windows ia32, Linux armv7l)** — sem impacto: `electron/package.json` só declara alvos x64 e arm64.
- **libEGL/libGLESv2 deixam de ser distribuídos (ANGLE estático)** — sem impacto: nenhuma referência em `electron/`, nos scripts de build ou no afterPack.
- **`clipboard` deixa de ser exposto ao renderer** — sem impacto: sem uso no projeto.
- **`net.request` passa a rejeitar `Sec-Fetch-Dest` document/frame/iframe/fencedframe sem `Sec-Fetch-Mode: navigate`** — sem impacto: `net.request`/`net.fetch` não são usados.
- **`openAsHidden` e `wasOpenedAsHidden` removidos de `app.set/getLoginItemSettings()`** — usados em `electron/main.js:1116` e `electron/lib/windowLifecycle.js:7`, mas sem regressão em plataforma suportada: o caminho vivo do autostart oculto é `args: ["--hidden"]` combinado com a checagem `argv.includes("--hidden")`, que `shouldStartHidden()` avalia primeiro; Linux nem chega nessa API (usa `enableLinuxDesktopAutostart`). `openAsHidden` só funcionava em macOS ≤ 12, que esta própria versão deixa de suportar.
- **macOS 12 (Monterey) sai do suporte** — é o único efeito voltado ao usuário. Registrado no CHANGELOG em PR de acompanhamento.
O smoke de empacotamento (`electron-package-smoke`) não roda em PR para branch de release — `ci.yml` dispara em `main` — então a validação de empacotamento acontece no merge → main, que é o modelo do repositório. Um PR de acompanhamento remove o código morto de `openAsHidden`/`wasOpenedAsHidden`. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
Validado em lote numa worktree combinada com os 7 bumps da raiz sobre o tip de release/v3.8.51: npm install reconciliou o lock sem diff (11/11 pacotes na versão alvo), typecheck:core e typecheck:noimplicit:core limpos, lint exit 0, e 149/149 testes nos 11 arquivos que exercitam zod diretamente. As falhas de CI do PR foram discriminadas como estado da base, não do bump. Obrigado, Dependabot.
* 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)
Bump do override `@xmldom/xmldom` 0.9.10 → 0.9.12 no workspace `electron/` (grupo npm_and_yarn). Escopo isolado: só `electron/package.json` + lock, sem interseção com o install da raiz; a redução de ~199 linhas no lock é dedupe da própria resolução. Obrigado, Dependabot.
Bump de action pinada por SHA para codeql-action v4.37.9. Validado: os pins de `init` e `analyze` (#12345/#12346) apontam para o mesmo commit `cdf488f595d80d6e07e03d4674febd5ab45fa938`, consistente com a tag v4.37.9; nenhum código de aplicação afetado. Obrigado, Dependabot.
Bump de action pinada por SHA para codeql-action v4.37.9. Validado: os pins de `init` e `analyze` (#12345/#12346) apontam para o mesmo commit `cdf488f595d80d6e07e03d4674febd5ab45fa938`, consistente com a tag v4.37.9; nenhum código de aplicação afetado. Obrigado, Dependabot.
Bump de action pinada por SHA para codeql-action v4.37.9. Validado: os pins de `init` e `analyze` (#12345/#12346) apontam para o mesmo commit `cdf488f595d80d6e07e03d4674febd5ab45fa938`, consistente com a tag v4.37.9; nenhum código de aplicação afetado. Obrigado, Dependabot.
Drains 7 of the 13 open CodeQL alerts that put the `codeql-ratchet` gate into
regression (13 > baseline 11) on every open PR. The alerts arrived with the
recent provider/media merges (#11461 MaxAI, #11513 UC, #12365 prefix shadowing),
not with the work they are currently blocking.
Production fix (js/biased-cryptographic-random):
- open-sse/executors/maxai/signing.ts: the 6-digit `X-Random` wire slot was
drawn as `randomBytes(4).readUInt32BE(0) % 900000`. 2^32 does not divide
evenly by 900000, so the low ~4772 values of the range came out marginally
more often. Extracted as `maxaiRandomSlot()` over `crypto.randomInt`, which
rejection-samples internally. The emitted shape is unchanged (6 digits).
Test assertions strengthened (never weakened):
- tests/unit/helpers/ucClerkUrl.ts (new): `isUcClerkMintUrl()` matches the Clerk
mint call by parsed origin (against `UC_CLERK_FAPI`) plus the
`/v1/client/sessions/{sid}/tokens` path shape.
- tests/unit/uc-image.test.ts, tests/unit/uc-video.test.ts: the mock fetch
routers dispatched on `url.includes("clerk.uncensored.com")`, so any host
merely embedding the name was served the mint response — a malformed URL
built by the executor could not fail the test
(js/incomplete-url-substring-sanitization x4).
- tests/unit/maxai-image.test.ts: `new RegExp(PATH.replace(/\//g, "\\/"))`
escaped only slashes (which need no escaping) and matched the path anywhere in
a wrong URL; replaced by exact URL equality (js/incomplete-sanitization).
- tests/unit/custom-provider-prefix-shadowing-11943.test.ts: the expected node
mention was a RegExp with only `()` hand-escaped; replaced by an exact
substring check (js/incomplete-sanitization).
- tests/unit/maxai.test.ts: regression guard for the X-Random slot (6 digits,
in range, spread across both halves of the range).
The remaining 6 alerts are not defects and are left for an operator dismissal
with justification (Hard Rule #14): the MaxAI HMAC-SHA1/SM3 signature and the
CryptoJS `EVP_BytesToKey(MD5)` derivation are wire-protocol requirements —
changing either breaks the provider — and `open-sse/utils/error.ts:749` already
routes through `sanitizeErrorMessage()` (documented CodeQL sanitizer blind spot,
docs/security/ERROR_SANITIZATION.md).
Co-authored-by: Markus Hartung <diegosouzapw@users.noreply.github.com>
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>
* chore(quality): register video-bridge memory suppression test in stryker tap.testFiles
* fix(ci): point the node_modules cache key at wreqJsNative after the tls-client removal
---------
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.
Reduced on merge rather than closed, because the useful half is not subsumed.
The production change is: #12423 landed first and reached the same end state for scripts/build/pack-artifact-policy.ts — one volatileEnvPath.mjs entry, keeping the #11437 comment that explains why it is REQUIRED (bin/omniroute.mjs calls describeVolatileEnvWarning on every CLI boot, and bin/cli/ is only an allowlist prefix, so its absence would otherwise be silent). This PR's base carried three occurrences and reduced them to one; the tip is already there, so that file takes the tip's side.
What survives is the guard test, which does not exist on the tip: it asserts the four artifact path policy arrays contain no duplicate entries, so the class of defect cannot come back quietly. Verified by proof rather than assumption — re-introducing the duplicate makes it fail, removing it makes it pass again.
Verified: 18/18 in pack-artifact-policy after the reduction.
Thanks — the duplicate was real and the guard is the part worth keeping.
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.
Applies the repository Prettier style to tests/unit/grok-web.test.ts. No production code, no assertion changes.
The AST-identical claim was verified independently rather than taken on trust: minifying both sides through esbuild produces byte-identical output.
The reformat expands the file from 2436 to 2713 lines, past its 2437 cap, so the baseline carries a new annotated entry at 2985 — the real LOC plus ~10% headroom, per the operator's instruction, so routine additions to this suite do not re-trip the gate on formatting alone. It is recorded as a deliberate exception to the down-only ratchet #12411 re-tightened; no other entry moves.
Verified: 68/68 in the reformatted suite, check-file-size OK.
Closes the two remaining unresolved provider-asset records without weakening the provenance boundary: public/providers/nimble-search.svg is removed because no sufficient redistribution evidence is recorded, and Nimble Search renders with the internal generic icon before any CDN tier while staying fully functional; Opper is registered in the local SVG resolver with its existing bytes proven against opper-ai/provider-omniroute at immutable commit 9aacef7d6a, recording the MIT repository license from that same commit while keeping trademarkClearance null.
Validated in a combined worktree with the batch's ready set boarded onto the current tip: parse sweep clean on every changed TypeScript file, typecheck:core clean, check:dashboard-typecheck OK (207 pre-existing errors, all within baseline), check:cycles OK, check-file-size OK, 203/205 focused node tests and 94/94 vitest — the two failures belong to #12427, which is held back.
The current release dependencies already resolve the ChatGPT Web vendor diagnostics the allowance covered, so the stale open-sse typecheck allowance is removed and any future diagnostic becomes a blocking regression again. No vendor source, package manifest, checker script or runtime behaviour changes.
Validated in a combined worktree with the batch's ready set boarded onto the current tip: parse sweep clean on every changed TypeScript file, typecheck:core clean, check:dashboard-typecheck OK (207 pre-existing errors, all within baseline), check:cycles OK, check-file-size OK, 203/205 focused node tests and 94/94 vitest — the two failures belong to #12427, which is held back.
Closes the remaining noImplicitAny gap in global system-prompt injection without changing valid OpenAI or Claude request behaviour: injectSystemPrompt gets a caller-preserving generic type, unknown bodies/message entries/content are narrowed before access, malformed entries are skipped instead of throwing, and request/message/content immutability is preserved.
Validated in a combined worktree with the batch's ready set boarded onto the current tip: parse sweep clean on every changed TypeScript file, typecheck:core clean, check:dashboard-typecheck OK (207 pre-existing errors, all within baseline), check:cycles OK, check-file-size OK, 203/205 focused node tests and 94/94 vitest — the two failures belong to #12427, which is held back.
The API-route and Open-SSE typecheck gates treated every top-level baseline value as a diagnostic map, so policy metadata such as _relax_velocity_2026_08_30 was iterated character by character and reported as fabricated numeric TypeScript improvements. Both gates now share one fail-closed baseline boundary: underscore-prefixed top-level keys are reserved for metadata and skipped, and real file entries must be plain objects.
Worth a follow-up: check-dashboard-typecheck still prints the same fabricated entries, so it looks like a third gate with the same defect that this PR's scope does not cover.
Validated in a combined worktree with the batch's ready set boarded onto the current tip: parse sweep clean on every changed TypeScript file, typecheck:core clean, check:dashboard-typecheck OK (207 pre-existing errors, all within baseline), check:cycles OK, check-file-size OK, 203/205 focused node tests and 94/94 vitest — the two failures belong to #12427, which is held back.
The rerank-provider listing route dynamically imported @/lib/localDb — the barrel Hard Rule #2 forbids — and the stale path meant local rerank-capable provider nodes never appeared in GET /api/memory/rerank-providers. Now imports the specific @/lib/db/readCache module, with a route-level regression test through the public GET handler.
Validated in a combined worktree with the batch's ready set boarded onto the current tip: parse sweep clean on every changed TypeScript file, typecheck:core clean, check:dashboard-typecheck OK (207 pre-existing errors, all within baseline), check:cycles OK, check-file-size OK, 203/205 focused node tests and 94/94 vitest — the two failures belong to #12427, which is held back.
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>
* chore(lint): adopt eslint-plugin-react-hooks 7.1.1
The #12146 migration (284 react-hooks compiler-rule violations resolved in 8
batches) completed on 2026-09-01, unblocking the 7.1.1 adoption the pin test
was holding back. Exact pin kept in both devDependencies and overrides; the
pin test moves to 7.1.1 (the dependabot-level ignore from #12329 stays — a
lint plugin coupled to the compiler rules always bumps via its own reviewed
PR, never riding a group).
* chore(lint): lockfile for the react-hooks 7.1.1 adoption
Generated with a bare 'npm install --package-lock-only' (naming the package
on the CLI rewrites the devDependency with a caret, which npm 11 then rejects
against the exact override). Validated on the .113 with a fresh npm ci +
cold NODE_OPTIONS=8G lint:json --max-warnings 0 → exit 0 (zero new
violations from the 7.1.1 rule set) and the re-pinned version test green.
Drains the two remaining Fast Quality Gates reds the #11513 (UC) merge left
on the tip:
- error-helper: ucTts.ts and uc/ws.ts built error payloads from raw
err.message (Hard Rule #12) — now wrapped in sanitizeErrorMessage(),
behavior otherwise identical (uc suites 51/51).
- model-lifecycle: the UC catalog registers the vendor-retired gpt-5.2-codex
(bare id; only the prefixed openai/gpt-5.2-codex was allowlisted). Added to
allowedRetiredInCatalog per its policy — forwarding globally would rewrite
the just-approved provider's model. Tracking: Refs #12436.
file-size, the third red of this window, was already drained by #12434.
check-file-size was red on release/v3.8.51 with nine violations — seven source files and two test files that the 2026-09-02 merge waves grew at existing chokepoints (#12359-#12404, #11461, #11513, #12423).
The growth itself was reviewed: each file was measured and justified while validating those batches. What went wrong is the propagation — the rebaseline was computed in the throwaway combined validation worktree, and the PRs were then merged individually through their own branches, so the code landed and the caps did not. A shared-file edit made only in the validation tree reaches nothing.
This records the caps against the merged state, each entry attributed to the PR that grew it, under one _rebaseline annotation. Verified mechanically: 9 caps recorded, 0 raised beyond the file's real merged LOC, 0 unrelated entries moved — the ratchet #12411 re-tightened is intact.
Verified: check-file-size OK (135 frozen source entries across 4515 files; 39 frozen test entries across 5365), prettier clean.
The #11461 × #11513 merge ate the closing '],' + '},' of the maxai entry in
webSessionCredentials.ts — 11 syntax errors (TS1005/1137/1128) on the tip,
which also masked one real TS2322 the MaxAI block introduced in the models
route (providerSpecificData is unknown on the connection; cast to the exact
shape resolveMaxaiCredential already takes, zero runtime change).
API Route Typecheck gate: OK — 289 pre-existing, all baselined. typecheck:core: 0.
Three regressions inherited by every PR rebased onto release/v3.8.51, caught and documented with the exact failing output.
The one that mattered most: src/shared/providers/webSessionCredentials.ts did not parse. The UC merge (#11513) inserted the uc: entry inside maxai.storageKeys and lost the array's closing ], plus the entry's }, leaving `ERROR: Expected "]" but found ":"` at line 351. That module is imported by the provider API routes, bulk-web-session, autoCombo's virtualFactory, keepaliveThreshold and dashboard components, so the break was live on the tip and flooded unrelated catalog tests with transform failures. That was my conflict resolution, not the contributor's code — thank you for catching it and for tracing it to the root commit rather than patching around the symptom.
Also fixed: the duplicate bin/cli/utils/volatileEnvPath.mjs entry in PACK_ARTIFACT_REQUIRED_PATHS (findMissingArtifactPaths reported it twice), and UC image models made prefix-addressable without letting them claim historical bare model ids belonging to other providers.
Reconciled on merge: #12394 landed the busy_timeout/probe work first, so src/lib/db/core.ts takes the tip's side. probeUtils.ts is the union of both rather than either side — this PR's message regex is wider (SQLite also reports "database table is locked", "database schema is locked" and "database is busy"), while #12394 added the driver code/errcode path that keeps a transient lock from being classified as corruption and renaming the database away. Taking either alone would have dropped the other half; this PR's own ENOENT test is what surfaced it.
Verified: 76/76 across uc-image, probe-9541-repro, web-session-contract, pack-artifact-policy, bulk-web-session-import and exclusive-connection-leases, and every changed .ts file parses.
Thanks @backryun.
GET /v1/providers/gemini-business/models returned nothing because gemini-business had no RegistryEntry: the listing route resolves the provider through getRegistryEntry and filters the unified catalog by owned_by, and open-sse/config/providers/index.ts only registered gemini and gemini-web.
Adds a registry entry mirroring gemini_webProvider — id gemini-business, alias gembiz, cookie auth — with the twelve ids from the executor's MODEL_CATEGORY_MAP. Each model is declared toolCalling: false, supportsReasoning: false, the same live-behaviour contract applied to gemini-web in #9356: the executor returns plain text, hard-wires the thinking mode and parses no tool calls.
Reconciled on merge: the only conflict was the reserved-prefix count assertion, which the tip had moved. Took the tip's text and measured the real value with this PR applied — 406 to 408, the gemini-business id plus its gembiz alias — rather than carrying the branch's number.
Validated in a combined worktree with all 25 PRs of this batch boarded together (typecheck:core clean, 443/443 node-runner plus 14/14 vitest, all static gates green), and re-verified standalone on the current tip after the other 24 landed: 33/33 across provider-node-reserved-prefix, gemini-business-model-registry-12107 and web-cookie-validation-fallback, with check:provider-consistency OK at 272 REGISTRY entries and 355 canonical providers.
Thanks @pacocartones.
The gamification anomalies page had hard-coded English for its loading state, Status column header and Suspicious badge, and was the only standalone non-redirect dashboard page without a sidebar entry. Both are fixed: the strings come from the common catalog, and the page joins the Gamification sidebar group as a hideable section item shown only by the "all" preset, like its siblings. The loading and empty states also become role="status" aria-live="polite" live regions with aria-busy, matching profile/page.tsx and health/page.tsx. Three new keys in en.json, propagated to the other 42 locales with the __MISSING__ sentinel.
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 API key permissions modal silently dropped allowedCombos entries its Combo picker cannot render — routing-rule names such as rt-*, which matchesComboAccessRule() already honours. Stored entries rendered as zero selected, and clicking All then Restrict then Save persisted allowedCombos: [], which is deny-all for combo requests.
Those entries now survive the All toggle, are listed read-only under the combo list so the header count and the list agree, and are saved back verbatim. The UI does not learn routing-rule semantics (option 1 from the issue). The Allowed Combos section moves out of the frozen ApiManagerPageClient.tsx into its own component following the UsageLimitSettings pattern.
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.
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 response de-obfuscation stripped the whole U+200B..U+200D range, so Persian/Kurdish half-spaces (U+200C), Arabic/Indic shaping and emoji ZWJ sequences (U+200D) were deleted from every assistant response — text, reasoning and tool-call arguments, streaming and non-streaming, every provider: ارائهدهنده came back as ارائهدهنده.
The request side only ever inserts a U+200D between two ASCII word characters, so the new stripObfuscationZeroWidth() removes a joiner only there, or at a string edge next to one so a word split across streaming deltas is still cleaned; U+200B and U+FEFF keep their unconditional removal. All seven copies of the old regex now go through the helper.
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.
CircuitBreaker.execute() treated every resolved promise as a success, but handleChatCore() reports most upstream failures by resolving with { success: false, status: 5xx }. On the chat path that spurious _onSuccess() decayed failureCount right before the call site's _onFailure() for the same attempt, so a provider answering 503s indefinitely stayed CLOSED at failureCount: 1 and kept receiving traffic — the breaker was structurally unable to open. Combo dispatches hit the same cancellation through the shared per-provider breaker.
execute() now takes an optional per-call classifyResult; without it the resolved-means-success contract every throw-based caller relies on is unchanged. executeChatWithBreaker() passes ignore and the chat path accounts for the outcome exactly once where the request context lives, so a combo success is no longer counted twice.
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.
The Leaderboard rendered apiKeyId.slice(0, 8)… under a column translated as "name". The route now enriches each entry with the key's display name — route-local, so the shared getTopN helper and the federation leaderboard stay id-only — and the page renders name ?? shortId with the full id in a title attribute. The lookup selects only id and name from api_keys, chunked at 200 ids, with unknown ids and blank names omitted; no key material leaves the DB layer.
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 Profile page already rendered a streak card but fed it a hard-coded useState(0) with a "streak data comes from future API" note — while streaks.ts tracked per-key streaks all along and the MCP gamification_profile tool already returned them. GET /api/gamification/level now returns streak: { current, longest } next to level: the key's own streak with apiKeyId, the operator-wide maximum otherwise, matching the aggregate mode getAggregateXp uses (#3484). No new route, no OpenAPI change, no new i18n keys; a missing or zero streak keeps the card hidden exactly as before.
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.
A thinking content part arriving with no signature — typical after a cross-provider hop where reasoning_content was converted into a thinking block — was stamped with DEFAULT_THINKING_CLAUDE_SIGNATURE. prepareClaudeRequest treats any non-empty signature on the latest assistant turn as genuine and preserves it verbatim, so the fabricated one reached Anthropic and the replay failed with "Invalid signature". A missing signature is now treated the same as an empty one, aligned with the stricter check claudeHelper.ts already used: the block is dropped rather than fabricated. Real signatures are still preserved verbatim and redacted_thinking is unchanged.
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.
A proxy assigned to an opencode / opencode-go connection is pinned by the chat handler as the ambient proxy context before the executor runs. OpencodeExecutor only reads per-account proxies from providerSpecificData.accountProxies, so an API-key connection with none took the single-account fast path — which wrapped the dispatch in runWithDirectFetchContext(), and that direct sentinel makes patchedFetch bypass the ambient context and hit native fetch. The assigned proxy was discarded and the request egressed from the host IP, giving `403 This model is not available in your country` on geoblocked hosts. The fast path now applies the direct pin only when no ambient context exists.
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.
POST /v1/images/generations through a combo returned a bare array instead of the OpenAI {created, data} payload: executeImageCombo() unwrapped one level too many, and the n used for cost calculation read the same double-nested shape, so it was always 0. The combo path now returns the handler payload unchanged, matching the direct-model path.
Second half: Codex image results emitted a data: URI in url whenever response_format was not b64_json, but OpenAI returns b64_json for the gpt-image-* family — clients that omit the field, Codex CLI's built-in image_gen among them, could decode neither shape. Codex now defaults to b64_json; an explicit response_format: "url" keeps its previous behaviour. Both land together because fixing one leaves Codex CLI failing at the other.
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.
When a built-in provider's id or alias reserves the prefix of an existing OpenAI/Anthropic-compatible node — v3.8.50 added openference with alias of, shadowing nodes created earlier with prefix of — the runtime error `No active credentials for provider: openference` gave the operator nothing to act on. It now explains that the prefix routed to the built-in, names the shadowed node, and logs an AUTH warning. Precedence is unchanged and the lookup runs only on the credential-failure path when no connection was tried, so the hot routing path is byte-identical.
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.
getDbInstance() ran PRAGMA journal_mode = WAL as the connection's first statement, before PRAGMA busy_timeout, and openSqliteDatabase() passes no driver-level timeout. A process opening the database while another closed its WAL connection — checkpoint plus WAL delete hold an EXCLUSIVE lock for a few hundred microseconds — therefore died with `database is locked` instead of waiting. That is the flake behind exclusive-connection-leases.test.ts on release/v3.8.51 runs 33525300898 and 33493797519 and on unrelated PR runs.
The second half is worse than the flake: isTransientProbeError matched /SQLITE_BUSY/ against error.message, but both drivers report the plain text `database is locked` and put the code in .code / .errcode. A transient lock during the corruption probe therefore took the corrupt-database path and renamed the file to storage.sqlite.probe-failed-… with "Manual recovery required". The probe now recognises the drivers' real BUSY/PROTOCOL/IOERR signals.
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.
validateScoreChange() — the documented 1000 XP/min per-API-key limit plus the velocity anomaly check — was exported but never called, so the award path applied every XP delta unconditionally. It now runs before addXp; a rejected award is logged at warn level and skipped, and the fire-and-forget path never throws.
The second finding is the one that made the first invisible: getRecentXp's window query was inert. created_at is stored by the table default as YYYY-MM-DD HH:MM:SS and was compared lexically against a JS ISO string, so same-day rows never matched and the limit could not have tripped even if it had been wired. The window start is now computed in SQLite, matching the style computeZScore already used.
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.
Four files embedded the U+0000 separator of a memo/group key as a raw NUL byte rather than the \\0 escape the codebase uses for the same idiom elsewhere. The runtime value is identical, but the raw byte trips the binary heuristics of git, GitHub and ripgrep: git diff --numstat reported `- -`, the introducing PRs rendered three of the files as "Binary file not shown", and rg silently skipped them in recursive mode. Rewritten as escapes, with a guard test keeping raw NUL bytes out of src/, open-sse/ and tests/.
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.
Under Bun the server child is spawned with --preload <path>/open-sse/utils/setupPolyfill.ts, and all three spawn sites built that path next to the server bundle — but the polyfill only ships at the package root and nothing copies it into dist/. Every `bun install -g omniroute` start died with `error: preload not found`. The preload now resolves from the supervisor module's own location and is shared by the two serve.mjs spawns, with the child argv moved into a pure buildServerSpawnArgs() so both branches are directly assertable (same seam as #8131). Node users are unaffected.
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.
getBestVisionModel() validates a configured fixedModel with hasUsableCredentialsForModel() before short-circuiting (#8430). auto / auto/* ids are virtual combos with no provider row, so that check always reported a confirmed false and the combo was silently discarded in favour of global auto-selection — it never got the chance to rotate its members. This mirrors the exemption the reroute guard in visionBridge.ts already carries; concrete fixedModel ids keep the #8430 fall-through unchanged.
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 opt-in Codex quota auto-ping pinned gpt-5.1-codex-mini. OpenAI shut that model down on 2026-07-23 and the repo's own lifecycle registry already rejects it on the request path, but the scheduler never consulted that gate — every window slide sent a dead id, hit the 15-minute failure cooldown, and retried the same id forever. The ping model now resolves per tick from the provider catalog through isModelSelectable(), the same gate chatCore uses, with the registry import kept lazy because this module sits on the instrumentation boot path (#12074). When nothing is selectable the provider is paused before any throttle slot, usage read or executor call, with one warning per state change.
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 #10265 rewrite of command-code-executor.test.ts (b6412c6fe) deleted the two regression tests #10986 added for reasoning-only Command Code output, while the production fallback in createJsonResponse / createStreamResponse survived — leaving it unguarded. Both are restored, now routed through the /alpha/generate fallback that is the only way to reach the CLI translator since #10265, via a shared goPlanFallbackFetch() helper.
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 least-used strategy ranked candidates by lastUsedAt alone, so after a 429 excluded the active account the replacement could be one that was merely oldest while still carrying its own backoff — it served a single request before the next one settled on a healthy account, the one-request detour with two cache misses reported on Codex. least-used now applies the backoffLevel tie-break the round-robin fallback branch already had, ahead of the existing never-used / oldest / priority order.
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.
When every combo target is excluded because the request's max_tokens exceeds each target's known output limit, the terminal 400 now says so — requested max_tokens against the pool's highest known ceiling — instead of the unrelated "supports structured output for this request". Diagnostics (unmet, excluded[].reason, terminalReason) are unchanged; only the message for the output_tokens primary reason moves.
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.
groq/compound and allam-2-7b were absent from the curated Groq registry, so the capability heuristic defaulted them to reasoning-capable and forwarded reasoning_effort verbatim — Groq answers HTTP 400. Declaring supportsReasoning: false makes applyThinkingBudget() strip reasoning_effort, output_config.effort and thinking, same class as #3258. The gpt-oss reasoning models keep the field.
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 playwright:v1.62.0-noble base ships Chromium as a Chrome for Testing build, which extracts to chrome-linux64/chrome. The CMD's find -path '*/chrome-linux/chrome' matched nothing, $chrome_path came out empty, and the container crash-looped on `exec: --headless=new: not found`. Widening the glob to '*/chrome-linux*/chrome' resolves both the legacy and the Chrome for Testing layout.
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.
* fix(memory): point the rerank-providers dynamic import at the real db module
#11390 landed with a dynamic import of the localDb barrel, which #12052 had
already removed from the base (and which Hard Rule #2 forbids) — the API
Route Typecheck gate reds on the tip with TS2307. getCachedProviderNodes
lives in src/lib/db/readCache.
* chore(quality): ratchet the api-typecheck baseline down (163 stale entries gone)
Regenerated with --update on a faithful npm ci environment (the .113 box)
against the current tip plus the rerank-providers import fix — the gate now
reads OK at 289 pre-existing errors, all baselined. No new entries added.
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.
The +30% loosening of 2026-08-10 (fbbef4eaaf) left this gate inert: combo.ts carried a 5,691-line cap against 4,023 real lines and chatCore.ts 7,895 against 5,946. Both god-files grew roughly 600 lines in two weeks without the gate ever firing.
Mechanical check:file-size --update against the tip. No source touched. combo.ts 5,691 -> 4,023, chatCore.ts 7,895 -> 5,946, frozen source entries 178 -> 135 (43 already fit the 1,200 cap), frozen test entries 49 -> 39. From here every 3.8.52 decomposition slice lowers the cap again.
Reconciled on merge, and worth recording because neither PR could see it alone: #11460 (flat-rate cost estimates) landed first and grew CostOverviewTab.tsx from 1,282 to 1,319 lines. This PR had frozen that entry at 1,283 — measured before #11460 existed — so the two together would have turned the tip red while each was green on its own. --update correctly refuses to raise a cap, so the entry was set to the real post-merge LOC with a _rebaseline_2026_09_02_11460_flat_rate_estimates annotation naming #11460 as the growth, following the own-growth precedent already in the file (_rebaseline_2026_08_20_10531_freebuff_provider).
The ratchet invariant is intact and was checked rather than assumed: across the whole baseline, 45 caps decrease and 0 increase; CostOverviewTab.tsx still falls 2,002 -> 1,319.
Verified: check-file-size OK (135 frozen source entries, 4,481 files checked; 39 frozen test entries, 5,338 checked), and prettier clean on the baseline.
Claude Code (claude / cc) is correctly classified as a flat-rate subscription, so the analytics API reports $0 — accurate as billed cost, and useless as a view of what the subscription actually consumed. Neither the Costs nor the Analytics dashboard had a token-price-equivalent view.
The fix keeps both meanings rather than picking one: ordinary analytics callers keep billed-cost semantics ($0 for flat-rate), /dashboard/costs and /dashboard/analytics opt in explicitly via includeFlatRateEstimates=true, the response reports whether estimates were included so a caller cannot mistake them for vendor billing records, and the figures on /dashboard/costs are labelled as flat-rate estimates rather than presented as spend. Omitted, false and unknown values all retain the existing behaviour.
Scope note carried from the description: this is a checkpoint on #11459, not its full closure — the issue stays open.
Verified in a combined worktree with three sibling PRs of this batch: typecheck:core clean, 134/134 focused tests (4 skipped), and i18n UI coverage PASS across all 42 locales for the 43-file locale pass.
One cross-PR interaction worth recording, since it is invisible from either side: this grows CostOverviewTab.tsx from 1282 to 1318 lines, which is fine against the tip's current 2002 cap but exceeds the 1283 that #12411 (file-size ratchet re-tightening) would freeze. Neither PR fails alone. Merged first on purpose so #12411's mechanical --update recomputes against the real post-merge LOC — the cap still only goes down.
Thanks @xiaoyaner0201 — the opt-in contract plus the "were estimates included" flag is the right shape for this.
The contact sheet, the dedup comparator and the drill-down each validated JPEG data-URIs independently. They now share src/lib/guardrails/videoBridgeFrameContract.ts. No behaviour change; the sibling tests that asserted a per-module message were aligned to the shared one. Closes the Standards-4 residue from the 2026-08-18 Video Bridge review.
Verified in a combined worktree with three sibling PRs of this batch: typecheck:core clean, 134/134 focused tests (4 skipped), i18n UI coverage PASS across all 42 locales.
Every job installs through this composite — 36 times per ci.yml run, 8 per
quality.yml run — and each call paid ~80-90 s of npm ci even with setup-node's
npm tarball cache warm (measured 2026-09-01: 3,327 runner-seconds per ci.yml run
just installing). A node_modules cache keyed on runner.os + runner.arch + the
resolved Node version + hashFiles(package-lock.json, .npmrc, postinstall.mjs and
its five helpers) lets an exact hit skip the install entirely.
- No restore-keys, same rule as the ESLint cache (#11600): exact key or a full
npm ci, never a partial tree from another lockfile / Node / postinstall.
- The retry loop is unchanged and remains the miss path; --no-audit --no-fund
because audit:deps is its own gate.
- cache input (default true) lets a caller opt out.
- actions/cache pinned to the v6.1.0 hash already used in nightly-mutation.yml
(zizmor unpinned-uses blanket policy).
- tests/unit/build/npm-ci-retry-composite.test.ts pins the key contents, the
no-restore-keys rule and the miss path.
Refs #8084
serviceKinds now drops .optional() in providerSchema.ts, and check-provider-consistency gains the reverse walk: a canonical provider whose serviceKinds include "llm" must have a REGISTRY entry unless it is in the new KNOWN_CATALOG_ONLY allowlist (providers routed through a connection baseUrl or a specialised executor). That turns "catalog entry outlived its registry entry" — the half-finished provider:remove — into a checkable invariant instead of something a reviewer has to notice.
Reconciled on merge, and worth reading before comparing diffs. The branch's 18 files had landed at the repository ROOT: git diff --name-status showed A gateways.ts, A providerSchema.ts, A check-provider-consistency.test.ts, A backfill-servicekinds.mjs with no directory component. The real provider files, schema, gate and test were never touched, so the +5093/-0 diff was root files AGENTS.md forbids (a test outside tests/, a script outside scripts/) and a no-op for the feature. The content was also 227 commits stale — the root gateways.ts was missing oneminai, among 267 divergent lines.
So each file's actual delta was reapplied onto the current tip rather than copied: the schema one-liner; the gate's KNOWN_CATALOG_ONLY, findCatalogOnlyLlmProviders(), the main() check and the summary line (the branch's copy also repeated the file header and imports at the end — 12 lines of residue from the same accident, dropped); the test's import block and five reverse-walk cases; and backfill-servicekinds.mjs placed at scripts/ad-hoc/, the path its own docstring names, then run against the current catalog: 315 insertions, 352/352 entries declaring serviceKinds, idempotent on a second run.
Two entries the mechanical pass could not get right, both surfaced by doing it against the live tree:
- github in oauth.ts is a single-line object, so the script's id:-per-line regex skipped it — the one failure it reported. Declared ["llm"] by hand, which is what the script's own rule computes.
- magnific came out as ["llm"] but is an image provider (icon: "image", registered in imageRegistry.ts). It is freepik renamed by migration 160, and freepik is in the script's NO_LLM set, so the rename left that set no longer matching. Your reverse walk caught it on its first run — a fair demonstration of why the gate is worth having. Corrected to [], with magnific added to NO_LLM and a note so a re-run cannot reintroduce it.
Verified: check:provider-consistency OK (268 REGISTRY entries, 352 canonical providers, 0 registry-only exceptions, 32 catalog-only), typecheck:core clean, 137/137 across the provider/schema/serviceKinds suites, check-file-size and check:cycles green.
Thanks @Tushar49 — the design is sound and the backfill script did the heavy lifting; only its placement and freshness needed fixing.
On dashboard/memory?tab=engine the Embedding Model quick-select (and the rerank selector) built their lists from a keyword heuristic over the CHAT catalog (AI_MODELS) plus OpenRouter live discovery. Providers whose embedding models are not in that catalog never appeared — mistral, gemini, nvidia nim, groq, vercel-ai-gateway and others that serve embeddings on a standard OpenAI-compatible /embeddings endpoint — and typing such a model by hand failed at runtime with "Unknown embedding provider".
The fix is one generic mechanism rather than a list of per-provider patches: deriveEmbeddingProviderForChatProvider() turns any chat-registry entry with a /chat/completions base into an OpenAI-compatible /embeddings config, with curated EMBEDDING_PROVIDERS entries always winning; the embeddings service resolves a derived config for unknown-but-configured providers instead of rejecting them; deriveRerankProviderForChatProvider() does the same for Cohere-compatible /rerank; and both memory selectors fall back to a free-text provider/model input when no static catalog exists. No provider is special-cased by name, so adding one to the chat registry now makes it embedding- and rerank-capable here automatically.
Verified on the current release tip: merged clean, typecheck:core clean, check:cycles OK across 417 files, and 35/35 across the PR's five new suites (qdrant-quick-select-catalog, memory-provider-listings, rerank-provider-listings, embedding-generic-provider-fallback, rerank-generic-provider-fallback) plus the updated hard-session-lease-bypass-inventory and embeddings-handler.
Note: the base-red disclaimer in the description referenced #9985 against release/v3.8.50 — that window is closed and the current tip carries no open base-red, so nothing was inherited here.
Thanks @rqzbeh — deriving the capability instead of enumerating providers is the version of this that stays correct as the registry grows.
* fix(memory): measure the embedding width instead of waiting for a probe
resolveEmbeddingSource() reports dimensions: null for any source the
hard-coded registry does not describe, and a self-hosted endpoint is by
definition absent from it. Both write paths then deadlocked on that null:
- scheduleVectorUpsert called ensureReady() with the null resolution, which
declines to create vec_memories, and then ignored the {ready:false} answer
and upserted anyway -- straight into the catch, so every memory was stored,
marked needs_reindex, and never vectorized;
- reindexPending refused to embed until the width was known, and the width
could only ever come from an embedding.
Nothing surfaced it: POST /api/memory returned 200 and the health check
stayed green while rowCount stayed at 0.
The comment on EmbeddingResolution.dimensions already calls this a lazy
probe; nobody performed the probe. The upsert path holds a finished vector
when it calls ensureReady, so measure it there, and let reindex spend one
embedding up front to measure -- reusing that vector rather than paying for
it twice. withMeasuredDimensions rebuilds the signature the same way the
resolution did, identity first, so two endpoints serving the same model id
still reindex independently.
scheduleVectorUpsert now also honours a {ready:false} answer instead of
upserting into a table that is not there.
Fixes#12154
* chore(changelog): point the fragment at the real PR number
* fix(memory): extract reindex helpers so the complexity ratchet stays green
runReindexBatch grew past max-lines-per-function and cognitive-complexity
when the lazy-probe path landed. Split measure/ready/item helpers without
changing the #12154 behavior.
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>
* fix(sse): map normalized xhigh to max for GLM-5.x+, DeepSeek-V4+, and provider aliases
* feat(sse): support native max reasoning effort and per-model clamping
* test(sse): add unit tests for Qwen 3.8, Claude 4.7+, GPT-5.6, and 2026 reasoning models
* fix(sse): align tests and file-size split for native max effort
Keep `max` as a first-class canonical tier. Split the new sanitizer
coverage out of base-executor-sanitize-effort.test.ts so the file stays
under testCap, and update discovery/catalog/vscode assertions to expect
native max instead of the old xhigh alias.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(sse): keep combo effort lists and drop unused collectSSE helper
Combo vscode routes still advertise the 5-tier list. Canonical `max` is
preserved in discovery (#9160) and github model metadata. Remove the
unused collectSSE helper that failed the absolute ESLint gate.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Chewji <Chewji9875@users.noreply.github.com>
* feat(ui): enable React Compiler (#67)
Enable reactCompiler: true in next.config.mjs (Next 16 + React 19.2.8).
This automates memoization at build time, removing manual useCallback/useMemo
debt (591 + 283 instances respectively) and preventing stale-closure bugs.
Test results (pre-existing failures unchanged):
vitest UI: 282/295 files pass (13 fail = missing router/ReactFlow mocks)
vitest: 1805/1857 tests pass (52 fail = same pre-existing mock issues)
node:test: api/services/db all pass (except platform-specific
serviceSupervisorSpawnError — Windows spawn("ls") issue)
No new failures introduced by the compiler transform.
Optional cleanup: remove now-redundant useCallback/useMemo in hot components.
* fix(build): add babel-plugin-react-compiler peer dependency (#67)
React Compiler (reactCompiler: true in next.config.mjs) requires
babel-plugin-react-compiler as an explicit peer dependency — Next.js
declares it as optional ("*") and does not auto-install it.
Installed babel-plugin-react-compiler@1.0.0 as a devDependency.
Resolves correctly from both the project root and the next package
context (Turbopack resolution path).
* fix(ci): allowlist babel-plugin-react-compiler for React Compiler
The React Compiler peer is a real npm package (facebook/react, MIT) required
by Next 16 `reactCompiler: true`. Adding it to the anti-slopsquat allowlist
unblocks check:deps and the 6A.8 unit-test gate.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
* fix(ci): drop unused collectSSE helper that trips ESLint
The helper was leftover from #12151 and fails the absolute
lint:json --max-warnings 0 gate on every PR that includes it.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: WebPerson <jonlwheat2-gif@users.noreply.github.com>
* feat(compression): make proactive context-compression threshold a live setting
The proactive compression trigger ratio was a hardcoded COMPRESSION_THRESHOLD =
0.7 in chatCore. Operators could not move compression relative to a client's own
compaction point (e.g. Codex Desktop self-compacts at ~0.85 of its window, so
the 0.7 proxy threshold always preempts the client's compaction with the
proxy's lossier one — see #8932 for what that produced before 3.8.50).
New: key_value namespace 'compression', key 'proactiveConfig',
{"thresholdRatio": 0.7}. Clamped [0.1, 0.99], 30s TTL cache, ipFilter
persistence pattern (#6131), synchronous read stays in the hot path. Default
unchanged; missing/invalid rows fall back to 0.7.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(compression): cover the live proactive-compression threshold (read, validity bounds, fallback, TTL)
Locks in getProactiveCompressionRatio() (src/lib/db/compression.ts), the
key_value-backed replacement for chatCore's hardcoded 0.7:
- shipped default 0.7 when no compression/proactiveConfig row exists
- 30s TTL cache: a fresh DB write stays invisible until the TTL lapses
(clock mocked via node:test mock timers, Date API — the module keeps
its cache private with no reset hook)
- valid override read from key_value, boundary values 0.1/0.99 included
- out-of-range ratios fall back to the DEFAULT (a validity window, not
clamping to the nearest bound — matching the shipped comment)
- broken JSON / non-numeric thresholdRatio: 0.7, without throwing
Guard verified by mutation: switching the window to clamping fails the
out-of-range case.
---------
Co-authored-by: root-cli (Hermes ops) <info@livewellwith.us>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* fix(kiro): do not permanently ban on 'User is not authorized to make this call'
* test(kiro): regression cover the 403 'User is not authorized' non-ban classification
---------
Co-authored-by: Deftera186 <Deftera186@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
* 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>
* feat(combos): add universal handoff feature flag
Add a default-enabled runtime flag that lets operators disable universal context handoffs globally without changing existing combo configuration or requiring a restart.
* fix(i18n): seed the universal-handoff flag description key across locales
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
- test:scoped:full (documented in the script header since #9143 but never wired) rebuilds
config/quality/test-impact-map.json and then selects.
- select-impacted-tests.mjs gains --stdin so --staged selects from the index; the git-diff
path only ever saw commits, so staged-only runs silently fell back to the heuristic.
- Loader parity with npm run test:unit / quality.yml TIA step (#6787): tests/unit/dashboard/**
under --import tsx (CJS transform), tests/unit/serial/** at --test-concurrency=1, the rest
under tsx/esm. The single tsx/esm invocation false-redded every dashboard test the map
selected ("Unexpected token 'export'").
- CONTRIBUTING.md → Running Tests documents the three modes and the fail-safe exit 1.
Refs #8084
@@ -343,6 +343,7 @@ Documentation must describe verified behavior, not plausible behavior.
### Adding a New Provider
0. Check `docs/reference/REMOVED_PROVIDERS.md` first — providers removed at their operator's request must never be reintroduced (guarded by `tests/unit/removed-providers-blocklist.test.ts`)
1. Register in `src/shared/constants/providers.ts` (Zod-validated at load)
2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`)
3. Add translator in `open-sse/translator/` if non-OpenAI format
- **[TS7] fix(types): validate default executor pool config** ([#9849](https://github.com/diegosouzapw/OmniRoute/pull/9849), original [#9795](https://github.com/diegosouzapw/OmniRoute/pull/9795)) — thanks @backryun
- **[TS7] fix(types): preserve The Old LLM proxy contracts** ([#9850](https://github.com/diegosouzapw/OmniRoute/pull/9850), original [#9793](https://github.com/diegosouzapw/OmniRoute/pull/9793)) — thanks @backryun
- **[TS7] fix(types): normalize Gemini Business credentials** ([#9851](https://github.com/diegosouzapw/OmniRoute/pull/9851), original [#9792](https://github.com/diegosouzapw/OmniRoute/pull/9792)) — thanks @backryun
- **[TS7] fix(types): preserve Claude thinking body contracts** ([#9852](https://github.com/diegosouzapw/OmniRoute/pull/9852), original [#9791](https://github.com/diegosouzapw/OmniRoute/pull/9791)) — thanks @backryun
- **fix(response): strip internal reasoning placeholder from all reasoning fields** ([#9853](https://github.com/diegosouzapw/OmniRoute/pull/9853), original [#9790](https://github.com/diegosouzapw/OmniRoute/pull/9790)) — thanks @adevwithpurpose
@@ -888,7 +892,7 @@ _By commits in `ed2db6cb19..v3.8.50`, author identities consolidated via `.mailm
- **fix(combo):** defer the known-context-overflow hard rejection for compressible requests so compression runs before the final context gate, instead of a raw-body estimate 400'ing generic Responses clients targeting a large model before OmniRoute can shrink it ([#10225](https://github.com/diegosouzapw/OmniRoute/issues/10225))
- **fix(api):** deleting a manually-added custom model no longer tombstones a provider-synced model that shares its id. `DELETE /api/provider-models` is addressed by `provider` + `model` alone, so when both a custom row and a synced row existed for one id it removed both and wrote `isDeleted:true`. `replaceSyncedAvailableModelsForConnection` then filtered that id out of every subsequent re-import, so the provider could never resync — model sync kept reporting `added: N` while the catalog stayed empty and `/v1/models` never listed the model again, even though routing to it still worked. The custom row is now removed first and its presence is treated as the operator's intent, leaving the synced sibling importable; a synced-only delete still tombstones as before (#3199, #3782 unaffected) ([#10228](https://github.com/diegosouzapw/OmniRoute/pull/10228)) — thanks @Neuron-Mr-White
- **Audio Bridge:** fix production transcription self-loop uploads so real audio reaches the configured STT provider instead of falling back to an unavailable-provider stub ([#10229](https://github.com/diegosouzapw/OmniRoute/pull/10229)).
- **fix(api):** DeepSeek V4's native `max` reasoning tier is now reachable. DeepSeek accepts `reasoning_effort``low`/`high`/`max` and maps `medium`/`xhigh` down to `high`, while OmniRoute's canonical vocabulary collapses `max` onto `xhigh` — so `{"effort":"max"}` silently resolved to `high` and the catalog never advertised a `max` tier (or its `<model>-max` variant). Following the existing `extendCodexGpt56EffortValues` precedent, the native tier is now preserved for `deepseek`/`ds` V4 models only; the global effort vocabulary is unchanged, routed namespaces (`openrouter/deepseek/…`,`tllm/deepseek_v4`,`oc/deepseek-v4-flash-free`) keep the canonical behavior, and an explicit client `reasoning_effort` still wins ([#10230](https://github.com/diegosouzapw/OmniRoute/pull/10230)) — thanks @Neuron-Mr-White
- **fix(api):** DeepSeek V4's native `max` reasoning tier is now reachable. DeepSeek accepts `reasoning_effort``low`/`high`/`max` and maps `medium`/`xhigh` down to `high`, while OmniRoute's canonical vocabulary collapses `max` onto `xhigh` — so `{"effort":"max"}` silently resolved to `high` and the catalog never advertised a `max` tier (or its `<model>-max` variant). Following the existing `extendCodexGpt56EffortValues` precedent, the native tier is now preserved for `deepseek`/`ds` V4 models only; the global effort vocabulary is unchanged, routed namespaces (`openrouter/deepseek/…`, `oc/deepseek-v4-flash-free`) keep the canonical behavior, and an explicit client `reasoning_effort` still wins ([#10230](https://github.com/diegosouzapw/OmniRoute/pull/10230)) — thanks @Neuron-Mr-White
- **fix(providers):** FreeAIAPIKey now targets `api.freeaiapikey.com`, the host upstream names in its `410 endpoint_moved` response — every request through the provider was failing — and its catalog is resynced to the 10 models the live `/v1/models` actually serves ([#10233](https://github.com/diegosouzapw/OmniRoute/pull/10233))
- **fix(providers):** MonsterAPI's deprecation now actually applies — the flag was written as `isDeprecated`, a key no consumer or schema reads, so the provider kept rendering as healthy in the dashboard, the onboarding wizard and the generated provider reference ([#10234](https://github.com/diegosouzapw/OmniRoute/pull/10234))
- **fix(cliproxy):** read platform/arch at runtime via `os.platform()`/`os.arch()` in `binaryManager` so the embedded installer selects the Windows/ARM assets even when the release bundle is built on a Linux runner (fixes #10244)
@@ -3000,7 +3004,6 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
- Stream model health probes for slow providers ([#7377](https://github.com/diegosouzapw/OmniRoute/pull/7377)) — thanks @JxnLexn
- Add reasoning-based model and effort routing ([#7607](https://github.com/diegosouzapw/OmniRoute/pull/7607)) — thanks @JxnLexn
@@ -3442,7 +3445,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
- **fix(cli):**`omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`.
- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status``key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`.
- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`.
- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`.
- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`.
- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`.
- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`.
- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`.
@@ -3998,7 +4001,6 @@ Thanks to everyone whose work landed in v3.8.45:
- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. ([#6001](https://github.com/diegosouzapw/OmniRoute/pull/6001)) (thanks @KooshaPari)
- **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). ([#6007](https://github.com/diegosouzapw/OmniRoute/pull/6007)) (thanks @KooshaPari)
- **feat(autoCombo):** add a latency/speed-optimized routing mode (shared `rankBySpeed` scoring core) plus the `omniroute_pick_fastest_model` MCP tool. ([#6011](https://github.com/diegosouzapw/OmniRoute/pull/6011)) (thanks @KooshaPari)
- **feat(providers):** refresh The Old LLM (Free) model catalog ([#5181](https://github.com/diegosouzapw/OmniRoute/issues/5181)) — seed the current free `/api/chatgpt` tier (GPT-5/5.1/5.2/5.3/5.4, o3/o4-mini, Gemini 3 Pro / 2.5 Pro / 2.0 Flash / 1.5 Flash, Claude 4.6 Opus/Sonnet & 4.5 Haiku, GPT-4o, Grok 4, DeepSeek V3/R1, Sonar Pro) while keeping the legacy alias IDs for saved-preference compatibility. Also fixes a latent routing bug: `mapModel()` now passes known upstream IDs through unchanged, so Gemini/o-series/Grok/DeepSeek/Sonar models no longer silently collapse onto `GPT_5_4`. Regression guard: `tests/unit/theoldllm-model-refresh-5181.test.ts`. (thanks @WslzGmzs)
- **feat(resilience):** surface Codex **banked reset credits** per connected account ([#5199](https://github.com/diegosouzapw/OmniRoute/issues/5199)) — the Codex quota parsers (`buildCodexUsageQuotas`, `parseCodexUsageResponse`) now additively read `rate_limit_reset_credits.available_count` (+ optional `rate_limit_reached_type`) from the `/wham/usage` payload OmniRoute already fetches, and the provider-limits dashboard renders a **"Banked Reset Credits"** row when a positive count is present. Display-only and **fail-open** — the field is eligibility-gated, so accounts without it are unaffected (parsers never throw on absent/garbage shapes); redemption (an unofficial mutating endpoint) is intentionally out of scope. Regression guard: `tests/unit/codex-banked-reset-credits-5199.test.ts` (8). (thanks @ofekbetzalel)
- **feat(providers):** add sign-up geo-restriction notices for **SenseNova** and **StepFun** ([#5462](https://github.com/diegosouzapw/OmniRoute/issues/5462)) — the provider add-form now warns that SenseNova's console appears to require a Chinese (+86) phone number with no documented international path, and that StepFun's default endpoint is its China platform while a global StepFun Open Platform (`platform.stepfun.ai`, operated by Sparkling AI Pte. Ltd., Singapore) with email/Google/Discord login exists for international users. Informational `notice` only — neither provider is disabled. Regression guard: `tests/unit/regional-provider-cn-notices-5462.test.ts`. (thanks @chirag127)
- **feat(usage):** add on-demand period-scoped usage-data reset (Settings → System Storage) with a purge API and time-window selector. ([#5831](https://github.com/diegosouzapw/OmniRoute/pull/5831))
@@ -5644,7 +5646,6 @@ Thanks to everyone whose work landed in v3.8.43:
- **fix(sse): map `reasoning_effort` to DeepSeek V4's native `{high, max}` vocabulary** — DeepSeek V4 only understands `high`/`max` reasoning levels, so other `reasoning_effort` values are mapped onto its native vocabulary instead of being rejected. ([#4219](https://github.com/diegosouzapw/OmniRoute/pull/4219))
- **fix(glm): default `max_tokens` and an extended timeout for GLM-5.2+ thinking** — GLM-5.2+ thinking responses are slow and need headroom, so OmniRoute now sets a sensible default `max_tokens` and a longer timeout for them. ([#4255](https://github.com/diegosouzapw/OmniRoute/pull/4255) — thanks @dhaern)
- **fix(antigravity): default `includeThoughts` for modern Gemini models** — modern Gemini models on the Antigravity path now default to including thoughts so reasoning isn't silently dropped. ([#4180](https://github.com/diegosouzapw/OmniRoute/pull/4180) — thanks @dhaern)
- **fix(provider-registry): add correct `contextLength` to theoldllm models** — fills in accurate context-window sizes for theoldllm's models. ([#4184](https://github.com/diegosouzapw/OmniRoute/pull/4184) — thanks @herjarsa)
- **fix(models): expose combo model token limits** — `/v1/models` now reports token limits for combo models. ([#4189](https://github.com/diegosouzapw/OmniRoute/pull/4189) — thanks @megamen32)
- **fix(combo): keep the passthrough quota fallback scoped** — prevents the passthrough quota fallback from leaking across unrelated targets. ([#4194](https://github.com/diegosouzapw/OmniRoute/pull/4194) — thanks @Svetznaniy33)
- **fix(combo): opt proactive-fallback compression into the TV1 bail-out (no silent target drop)** — proactive-fallback compression now participates in the TV1 bail-out so a target is never silently dropped. ([#4228](https://github.com/diegosouzapw/OmniRoute/pull/4228))
@@ -6304,7 +6305,6 @@ Thanks to everyone whose work landed in v3.8.43:
- **fix(catalog):** Codex CLI model-catalog refresh no longer errors — `GET /v1/models` now returns a top-level `models: []` array for Codex clients (detected via the `originator` / `user-agent` = `codex_*` headers it sends on `GET /v1/models?client_version=...`), so `codex_models_manager` stops failing to decode the OpenAI-standard response and no longer logs `failed to refresh available models` on every startup. The array is intentionally empty: Codex replaces its built-in per-model agent prompt (`base_instructions`, ~21k chars) with whatever a populated entry carries for the selected model, so emitting our catalog would break Codex's agent behaviour — an empty list keeps Codex on its built-in model info (same inference as before, minus the error). Non-Codex OpenAI clients receive the unchanged `{object,data}` response. ([#3481](https://github.com/diegosouzapw/OmniRoute/pull/3481) — thanks @diegosouzapw)
- **fix(provider):** Cursor's Responses-API-shaped bodies on `/chat/completions` are detected and handled — a body with `input` but no `messages` is now classified as `openai-responses` (instead of forcing `openai` and building from undefined `messages` → upstream 400); standard OpenAI clients are unaffected by the `messages===undefined` guard. ([#3490](https://github.com/diegosouzapw/OmniRoute/pull/3490) — thanks @borodulin)
- **fix(sse):** numeric provider IDs normalized to strings across 4 more surfaces — extends #3427 to the Responses-API SSE passthrough (`response_id`/`item_id`/`call_id`), the buffered/flush path in `stream.ts`, the dedup-key builders, and `sseParser.ts`, preventing `undefined` lookups when IDs arrive as numbers. ([#3451](https://github.com/diegosouzapw/OmniRoute/pull/3451) — thanks @disafronov)
- **fix(theoldllm):**`X-Request-Token` generated server-side, dropping the Playwright dependency — replicates the site's client `rie()` token (djb2 hash + `oldllm-client-2026` seed + UA prefix + 8-hex `crypto.randomUUID` suffix) directly, so The Old LLM no longer needs a headless browser to mint tokens. ([#3491](https://github.com/diegosouzapw/OmniRoute/pull/3491) — thanks @borodulin / @diegosouzapw)
- **fix(combo):** parallel pre-screen + circuit-breaker fast-exit for priority combos — provider profiles and model availability for all targets are pre-screened concurrently (max 5), and targets whose circuit breaker is OPEN are skipped immediately, reducing first-token latency on multi-target priority combos. ([#3169](https://github.com/diegosouzapw/OmniRoute/pull/3169) — thanks @pizzav-xyz)
- **fix(authz):** URL-tokenized client endpoints (`/api/v1/vscode/<key>/...`) authenticate again when the caller sends its own non-OmniRoute `Authorization` header — a non-`Bearer <token>` header (e.g. VS Code Copilot's own, or an empty `Bearer `) no longer short-circuits auth; it falls through to the path-scoped URL token (still validated downstream), instead of 401'ing under `REQUIRE_API_KEY=true`. ([#3504](https://github.com/diegosouzapw/OmniRoute/pull/3504) — thanks @zhiru / @diegosouzapw)
- **fix(playground):** the dashboard provider Test playground works under `REQUIRE_API_KEY=true` — it previously sent the **masked** key (`sk-xxxx****yyyy`) as a bearer (always invalid → 401). It now authenticates via the dashboard session and sends only the key **id** (`x-omniroute-playground-key-id`); the gateway resolves the secret server-side, honored **only** for an authenticated session and never putting the key secret on the wire. ([#3503](https://github.com/diegosouzapw/OmniRoute/pull/3503) — thanks @zhiru / @diegosouzapw)
@@ -6337,7 +6337,7 @@ Thanks to everyone whose work landed in v3.8.43:
- **fix(translator):** Vertex AI tool calls no longer fail with `400 Unknown name "id"` — the OpenAI-style `id` field is stripped from `functionCall`/`functionResponse` parts for `vertex`/`vertex-partner`; the public Gemini API still receives `id` as required for Gemini 3+ signature matching. ([#3457](https://github.com/diegosouzapw/OmniRoute/pull/3457) — thanks @nullbytef0x / @diegosouzapw)
- **fix(claude):** Claude Code `claude-opus-4-8` tool calls no longer break with `tool call could not be parsed` — OmniRoute no longer force-injects `interleaved-thinking` / `advanced-tool-use` / `effort` beta flags the client never negotiated; clients sending their own `anthropic-beta` header control those betas themselves. ([#3458](https://github.com/diegosouzapw/OmniRoute/pull/3458) — thanks @Forcerecon / @diegosouzapw)
- **fix(catalog):** imported/custom models on no-auth providers (e.g. The Old LLM) now appear in `GET /api/v1/models` and the Playground model selector — the eligibility gate required a DB connection row which no-auth providers never have, silently dropping every imported model for them. ([#3463](https://github.com/diegosouzapw/OmniRoute/pull/3463) — thanks @tjengbudi / @diegosouzapw)
- **fix(catalog):** imported/custom models on no-auth providers now appear in `GET /api/v1/models` and the Playground model selector — the eligibility gate required a DB connection row which no-auth providers never have, silently dropping every imported model for them. ([#3463](https://github.com/diegosouzapw/OmniRoute/pull/3463) — thanks @tjengbudi / @diegosouzapw)
- **fix(browser):** optional `cloakbrowser` import no longer causes bundle errors when the package is absent — the import is now wrapped in a dynamic require so the build succeeds on environments that don't install the optional dep. ([#3460](https://github.com/diegosouzapw/OmniRoute/pull/3460) — thanks @rdself)
- **fix(claude-web):** claude-web session handling cleanup — corrects an edge case where session cookies were not properly refreshed after a Turnstile challenge, and removes stale wrapper code left over from the provider split. ([#3449](https://github.com/diegosouzapw/OmniRoute/pull/3449) — thanks @androw)
- **fix(analytics):** SQL named params are now scoped per query context — a shared params object was being mutated across concurrent analytics queries, causing `SQLITE_MISUSE: named parameter not found` errors under load. ([#3447](https://github.com/diegosouzapw/OmniRoute/pull/3447) — thanks @ReqX)
@@ -6513,8 +6513,7 @@ Thanks to everyone whose work landed in v3.8.14:
- **fix(dashboard):** Agent Bridge page (`/dashboard/tools/agent-bridge`) no longer crashes with "Internal Server Error" — the page replaced its well-shaped state with the raw `/api/tools/agent-bridge/state` response (`{ server, agents }`), leaving `serverState` undefined and throwing `Cannot read properties of undefined (reading 'running')`. A shared `normalizeAgentBridgeState()` now maps the route shape into the page contract (incl. `server.certExists → certTrusted`) and always returns safe defaults, used by both the SSR loader and the polling hook. (#3318 — thanks @tycronk20)
- **fix(codex):** strip client-only params (`prompt_cache_retention`, `safety_identifier`, `user`) on the native `codex/``/v1/responses` passthrough — Codex upstream rejects them with `400 Unsupported parameter`, which broke Factory Droid and any client injecting those fields. The chat-completions path already stripped them; the responses→responses passthrough now does too. (#3317 — thanks @tycronk20)
- **fix(theoldllm):** stop the `[502]: Body is unusable: Body has already been read` error on the cached-token path — the executor read the same upstream `Response` body with `.text()` twice; it now reads it once and only re-reads after a token-rejection refetch. (#3296 — thanks @onizukashonan14-png)
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
- **fix(dashboard):** refresh the connection list after a Codex/Claude/Gemini auth import — the import modals called `fetchData()` (which only reloads provider metadata), so a freshly-imported connection stayed invisible until a manual reload; they now call `fetchConnections()`. ([#3320](https://github.com/diegosouzapw/OmniRoute/pull/3320) — thanks @zhiru)
- **fix(cli):**`omniroute update` no longer always fails on a global install — `getCurrentVersion()` and `createBackup()` now resolve `package.json`/`bin` relative to the script (`import.meta.url`) instead of `process.cwd()` (the user's working dir on a global npm/brew install → _"Could not determine current version"_), and the backup copies the `cli` directory with `cpSync({recursive:true})` instead of `copyFileSync`, which threw a swallowed `EISDIR` → _"Failed to create backup. Aborting"_. (#3295 — thanks @uniQta)
- **fix(sse):** harden the passthrough stream against empty upstream responses — emit a synthetic retry chunk on an empty `choices: []` (fixes a Copilot Chat crash) and log empty post-`tool_calls` completions; also registers **MiniMax M3** (1M context) across 8 provider tiers. ([#3297](https://github.com/diegosouzapw/OmniRoute/pull/3297), #3110 — thanks @wilsonicdev)
@@ -6606,7 +6605,6 @@ Thanks to everyone whose work landed in v3.8.12:
### ✨ New Features
- **theoldllm:** add The Old LLM — a free, Playwright-backed provider with dual-mode operation (cached browser token + direct fetch) bridged through a Vercel relay (#3217 — thanks @oyi77)
- **codex:** add Codex login via OpenAI's browser-driven device authorization flow, exposed as a shareable "Adicionar Externo" public link (`/connect/codex/{token}`) so a third party can complete the OpenAI device login without dashboard access (#3195 — thanks @zhiru)
- **proxy:** per-connection proxy distribution — `proxy_enabled` DB schema + Zod-validated resolution backend, automatic proxy-fallback selection when provider validation hits a network error, and a dashboard UI with per-connection toggles and a tag-filtered "Distribute Proxies" button (#3170, #3171, #3172 — thanks @pizzav-xyz)
- **api:**`/v1/images/generations` and `/v1/images/edits` now resolve a bare combo/alias model name (e.g. `image`) to its single image target, and `/v1/images/edits` forwards multipart edits to custom OpenAI-compatible providers' `{base_url}/images/edits` (also accepting JSON/data-URL edit input) instead of rejecting everything but chatgpt-web (#3214, #3215 — thanks @ngocquynh85)
# Smoke check native database driver used by Bun (bun:sqlite)
RUN bun -e "import { Database } from 'bun:sqlite'; const db = new Database(':memory:'); db.query('SELECT 1 AS ok').get(); db.close(); console.log('bun:sqlite smoke: OK');"
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 352 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 352 AI providers · 150+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 356 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 356 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
<div align="center">
## 💰 ~1.51B Free Tokens / Month
## 💰 ~1.47B Free Tokens / Month
</div>
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **446 free-tier entries across 38 recurring pool keys** and computes the token headline from the **20 pools with a published positive monthly budget**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **444 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.51B free tokens per month steady, up to ~2.13B in the first month with signup credits, from 38 documented recurring pool keys covering 446 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 20 recurring pools with a published positive monthly token budget; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, LLM7 150M, Nara 150M, Gemini 60M and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.10B in the first month with signup credits, from 34 documented recurring pool keys covering 444 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 352 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 352 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 356 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 356 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 52 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
<br/>
<br/>
@@ -463,7 +462,7 @@ All **19** strategies — mix & match per combo step:
</div>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 352 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 356 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -519,9 +518,9 @@ Pix copia-e-cola:
## 📡 OmniRoute Radar
The main free-tier headline remains **~1.51B tokens/month** from the documented,
The main free-tier headline remains **~1.47B tokens/month** from the documented,
pool-deduplicated catalog above. Temporary provider signup credits can separately lift the first
month to **~2.13B**. Radar is an optional, signed catalog overlay for people who want fresher
month to **~2.10B**. Radar is an optional, signed catalog overlay for people who want fresher
free-model availability between OmniRoute releases; the community catalog and every existing free
feature remain free.
@@ -649,7 +648,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
</div>
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **446 per-model rows**, **38 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **444 per-model rows**, **34 recurring pools** and **52 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
<div align="center">
@@ -725,6 +724,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
<tr><td align="left" nowrap>🖥️ <b>Desktop (Electron)</b></td><td align="left" nowrap><code>npm run electron:build</code></td><td align="left">Native window + system tray — <b>Windows / macOS / Linux</b></td></tr>
<tr><td align="left" nowrap>🎩 <b>Menu-bar (OmniRouteTray)</b></td><td align="left" nowrap><code>brew install --cask zoispag/tap/omniroute-tray</code></td><td align="left">Supervises & auto-updates the server — <b>macOS</b></td></tr>
<tr><td align="left" nowrap>💪 <b>ARM</b></td><td align="left" nowrap>native <code>arm64</code></td><td align="left">Raspberry Pi, ARM servers, Apple Silicon</td></tr>
<tr><td align="left" nowrap>📱 <b>Android (Termux)</b></td><td align="left" nowrap><code>pkg install nodejs && npx -y omniroute</code></td><td align="left">Runs <b>on your phone</b>, 24/7, no root</td></tr>
<tr><td align="left" nowrap>📲 <b>PWA</b></td><td align="left" nowrap>"Add to Home Screen"</td><td align="left">Fullscreen, offline, installable from browser</td></tr>
@@ -733,7 +733,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
<tr><td align="left" nowrap>🛠️ <b>From source</b></td><td align="left" nowrap><code>npm install && npm run dev</code></td><td align="left">Hack on it, contribute</td></tr>
@@ -768,6 +768,42 @@ From inside the editor: open the **Extensions** view, search **"OmniRoute"**, cl
<div align="center">
### 🎩 New: OmniRouteTray — your gateway, living in the menu bar
</div>
> `omniroute serve` is happiest when it's always on. **[OmniRouteTray](https://github.com/zoispag/omniroute-tray)**
> turns that into a set-and-forget menu-bar app for macOS: it starts the server, keeps it alive
> across reboots, updates it in place, and puts your live token budget one click away — **no
> terminal window left open, no `npm install -g omniroute` to babysit.**
Built with [Tauri v2](https://v2.tauri.app/) (a Rust core the size of a rounding error), it ships
its own signed Node 24 runtime and manages an app-owned OmniRoute install, so it never fights your
global `node`/`bun`. It **shares your existing `~/.omniroute/` config and database** — so it's the
same OmniRoute you already run, just with a hat on. 🎩
<table>
<tr><th align="left">What it does</th><th align="left">How</th></tr>
<tr><td align="left" nowrap>🟢 <b>Supervises the server</b></td><td align="left">Spawns <code>omniroute serve</code>, adopts an already-running instance instead of duplicating it</td></tr>
<tr><td align="left" nowrap>📊 <b>Live usage at a glance</b></td><td align="left">Provider quota bars, Claude session/weekly limits with reset countdowns, 30-day cost breakdown</td></tr>
<tr><td align="left" nowrap>🔄 <b>Auto-updates in place</b></td><td align="left">Staged install, atomic swap, rollback on failure — always on the newest release</td></tr>
<tr><td align="left" nowrap>🚀 <b>Start on login</b></td><td align="left">Optional launch at login; tray-only, no dock icon</td></tr>
<tr><td align="left" nowrap>🩺 <b>Doctor & logs</b></td><td align="left">One-click diagnostics and server log access</td></tr>
</table>
```sh
brew install --cask zoispag/tap/omniroute-tray
```
<sub>Prefer a download? Grab the latest <code>.dmg</code> from
<a href="https://github.com/zoispag/omniroute-tray/releases">Releases</a>. Source, issues and build
docs live at <a href="https://github.com/zoispag/omniroute-tray">zoispag/omniroute-tray</a>.
<br/>💛 A community project by <a href="https://github.com/zoispag">@zoispag</a> — not an official OmniRoute release.</sub>
Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
- **Automatic Webpack bundler selection in dev**: Development (`bun run dev`) automatically detects Bun and disables Turbopack in favor of Webpack to prevent native V8 binding incompatibilities. Production builds (`bun run build`) follow `OMNIROUTE_USE_TURBOPACK` exactly as on Node: Turbopack by default, `OMNIROUTE_USE_TURBOPACK=0` to build with Webpack (`Dockerfile.bun` exposes it as a `--build-arg`).
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).
```bash
@@ -1208,7 +1244,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
<tr><td nowrap><b><a href="docs/ops/COVERAGE_PLAN.md">Coverage Plan</a></b></td><td>Test coverage strategy for 39,000+ static test declarations across 5,100+ tracked test files</td></tr>
- **feat(providers):** add SeekAi (`seekai.cc`) as an OpenAI-compatible New-API gateway — catalog id `seekai` (alias `ska`), `https://seekai.cc/v1`, live `/v1/models` via `passthroughModels`, aggregator-list membership so New-API balance detection can opt in. No referral/aff codes. ([#11786](https://github.com/diegosouzapw/OmniRoute/issues/11786))
- **feat(sse):** treat `max` as a first-class reasoning-effort tier and clamp per model family (GLM 5.1+/DeepSeek V4+/Kimi K3+ keep native `max`; o1/MiniMax/Grok/Muse Spark clamp to their upstream ceiling) ([#11875](https://github.com/diegosouzapw/OmniRoute/pull/11875)) — thanks @Chewji9875
- **feat(providers):** import-from-file modal shows per-row API errors and ships a downloadable CSV template ([#12071](https://github.com/diegosouzapw/OmniRoute/issues/12071))
- **feat(providers):** dashboard search matches connection name and `baseUrl` so imported OpenAI-compat nodes surface on the provider card ([#12108](https://github.com/diegosouzapw/OmniRoute/issues/12108))
- **feat(settings):** persist `headroomUrl` through Settings so status/start use the operator URL instead of only `HEADROOM_URL` ([#12306](https://github.com/diegosouzapw/OmniRoute/issues/12306))
- **feat(radar):** explain Community, single-use, contributor, supporter, recovery, abuse, offers, and privacy rules before either Radar activation action, and remove the superseded fixed-PR grant promise from every UI locale ([#12342](https://github.com/diegosouzapw/OmniRoute/pull/12342))
- **feat(gamification):** the dashboard Profile page now shows the real daily streak — `/api/gamification/level` returns `streak: { current, longest }` (per key with `apiKeyId`, operator-wide maximum otherwise) and the streak card reads it instead of a hard-coded 0 (#2403)
- **feat(gamification):** the dashboard leaderboard now shows each API key's display name under the Name column instead of a truncated key id; `GET /api/gamification/leaderboard` attaches `name` per entry (name only — no key material), while the shared ranking helper and the federation leaderboard stay id-only — thanks @pacocartones
- **feat(gamification):** enforce the documented 1000 XP/min per-API-key anti-cheat rate limit on the XP award path; over-limit awards are logged and skipped instead of persisted, and the sliding window now matches the timestamp format stored in `xp_audit_log` ([#2403](https://github.com/diegosouzapw/OmniRoute/issues/2403))
- **feat(admin):** localize the gamification anomalies page — the loading state, the Status column and the Suspicious badge now come from the `common` catalog (new `common.suspicious` key propagated to every locale) — add it to the Gamification sidebar group as `gamification-admin` (`/dashboard/gamification/admin`), and expose the loading and empty states as polite `role="status"` live regions (#12401 — thanks @pacocartones)
- **feat(providers):** skip GitHub combo members missing from the live synced catalog, and drop Copilot models that are policy-disabled or hidden from the model picker ([#12473](https://github.com/diegosouzapw/OmniRoute/pull/12473)) — thanks @RaviTharuma
- **feat(i18n):** locale-expansion tooling — `npm run i18n:add-locale` adds a language to config, dashboard, docs mirrors, CLI, README/indexes and site in one command; browser, CLI and cookie detection resolve `uk`, `fil`/`tl`, `zh-Hant` (and the retired legacy `in` → `id`) via config aliases; `config/i18n.json` now ships in the npm package so the published CLI can read it; new real-translation ratio gate (`npm run i18n:check-ratio`, advisory) with a per-locale ratchet baseline; `run-translation --adopt` restores `.i18n-state.json`; `sync-language-bars` generates 🌐 bars from config; `validate_translation.py` loads its allowlist again. Retires the duplicate `in` locale (Indonesian mislabelled as Hindi) — 42 honest locales; saved `NEXT_LOCALE=in` / `OMNIROUTE_LANG=in` keep working. (#12496)
- **feat(api):** Emit gateway-measured `tokens_per_second` (TTFT excluded) on streaming usage and `X-OmniRoute-Tokens-Per-Second` when first-token latency is known ([#12616](https://github.com/diegosouzapw/OmniRoute/issues/12616))
- **feat(providers):** add MaxAI as a signed, OpenAI-compatible provider serving its 13 paid chat models (GPT-5.6 / Luna / Thinking, Claude 5 Sonnet, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite, Grok 4.1-fast / 4.5, DeepSeek V3.2 / R1, Llama 3.3 70B) through OmniRoute's `/v1` endpoint, with per-request HMAC-SHA1→SM3→AES request signing, live model + context-window discovery from `/models/get_config`, and prompted tool-calling translated to OpenAI `tool_calls`
- **feat(providers):** MaxAI vision input — image_url content parts are forwarded inline in `message_content` to the 6 vision-capable models (GPT-5.6 / Luna / Thinking, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite)
- **feat(providers):** MaxAI document RAG — inline base64 file/document attachments are uploaded to MaxAI (content-addressed `doc_id`) and attached to the chat via `doc_list`
- **feat(providers):** browserless MaxAI onboarding — email device-pair login (`/api/providers/[id]/login`) and signed access-token refresh, so a connection can be created and kept fresh without a real browser or Google OAuth
- **feat(providers):** per-provider TLS impersonation profile (MaxAI presents a Windows Firefox-150 client fingerprint) so its bot-sensitive endpoints accept OmniRoute traffic
- **feat(providers): add UC Direct (uncensored.com Developer API), the metered OpenAI-compatible surface.** A standard OpenAI-compatible passthrough (default executor) for uncensored.com's official REST API at `https://api.uncensored.com/api/v1`: `X-api-key` auth (never-expiring `uai_sk_live_` key), `POST /chat/completions` with streaming SSE and native tool-calling, and the full live metered catalog (82 models across 15 providers, discovered from the public `GET /v1/models`). Registered as provider `uc-direct` (alias `ucd`). Complements the un-metered `uc` persona provider — same models, metered credits and a plain API key instead of a subscription session.
- **feat(providers): add UC (uncensored.com), the un-metered subscription "persona" chat as an OpenAI-compatible provider.** A WebSocket web-app port: a durable Clerk credential mints a short-lived session token per connect (browserless — no API key), driving UC's persona socket. Ships the browserless email-code login (request → verify → harvest), the 19 verified persona models (Claude Opus, Gemini, Grok, GLM, Kimi, DeepSeek, MiniMax, incl. the uncensored variants), prompted `<tool>` tool-calling with a per-model code-style dialect + auto-cure retry for guardrailed models, live `<think>`/reasoning split, streaming + non-streaming OpenAI responses, and full quota/auth error surfacing (paywall / message-limit / rate-limit → 429, invalid session → 401 re-login). Full multimodal parity via the persona blob-upload layer: **vision** (image input, 15 vision-capable models), **document RAG** (PDF/doc upload, server-side extraction), **image generation** (22 models), **video generation** (14 models, async signed-url → poll), and **TTS** (streaming MP3). Registered as provider `uc` (alias `ucn`). The metered OpenAI-compatible Developer API is a separate `uc-direct` provider.
- **fix(security):** Sanitize provider and runtime failures before public API, SSE and MCP responses and before persistent request, proxy and usage logs, preventing credentials, stack traces and host filesystem paths from crossing those boundaries while preserving stable error codes and useful diagnostics.
- **fix(cli):** `omniroute tunnel create` no longer crashes with `Cannot read properties of undefined (reading optsWithGlobals)` — removed the duplicate positional argument that caused Commander.js to misalign the action callback parameters ([#12295](https://github.com/diegosouzapw/OmniRoute/issues/12295))
- Fixed the v3.8.50 Costs and Analytics dashboards so flat-rate Claude Code usage can be shown as an explicitly requested token-price estimate without changing default billed-cost semantics.
- Fixed archived usage retention so each request is priced individually instead of pricing a day's summed tokens once, which understated archived cost whenever a day mixed cache-heavy and ordinary requests.
- Fixed the Costs dashboard so it discloses when displayed figures include flat-rate token-price estimates instead of labelling them as billed spend, using the flag the analytics API already returns; the month-end projection and the CSV/JSON exports carry the same marker, and billed-cost mode is unchanged.
- **fix(settings):** `PUT /api/settings/cache-config` now persists `alwaysPreserveClientCache` to the flat general settings the runtime cache-control policy actually reads; previously the value landed in the databaseSettings "cache" section and was silently ignored, so the endpoint had no effect on `cache_control` passthrough ([#12304](https://github.com/diegosouzapw/OmniRoute/pull/12304)) — thanks @davidebaraldo
- **fix(grok-cli):** treat omitted SuperGrokPro `creditUsagePercent` as 0% used so Provider Limits still renders a weekly bar (proto3 zero-elision) ([#12312](https://github.com/diegosouzapw/OmniRoute/pull/12312)) — thanks @HouMinXi
- **fix(quota):** drop the generic quota cache (agy / Antigravity / Claude OAuth) on an upstream 429 so reset-aware scoring does not keep a 60s stale snapshot, and force-refresh the next usage fetch so inner provider caches cannot recache the same window ([#12325](https://github.com/diegosouzapw/OmniRoute/pull/12325)) — thanks @HouMinXi
- **fix(combos):** deleting a combo now clears its persisted LKGP pins instead of leaving unreachable `key_value` rows behind ([#12326](https://github.com/diegosouzapw/OmniRoute/issues/12326))
- **fix(sse):** Keep ZWNJ (U+200C) and ZWJ (U+200D) in assistant text, reasoning and tool-call arguments — Persian/Kurdish half-space (`ارائهدهنده`), Arabic/Indic shaping and emoji sequences no longer lose them; the response de-obfuscation now removes joiners only between ASCII word characters, where the request side inserts them ([#12186](https://github.com/diegosouzapw/OmniRoute/issues/12186)) — thanks @rezjalibd
- **fix(resilience):** count resolved upstream 5xx results against the provider circuit breaker on the chat path — `CircuitBreaker.execute()` no longer reads a resolved `{ success: false, status: 5xx }` as a success that cancels the call-site failure, so a provider answering 503s now trips its breaker instead of staying `CLOSED` at `failureCount: 1`; single-model and combo dispatches are each accounted exactly once ([#12254](https://github.com/diegosouzapw/OmniRoute/issues/12254))
- **fix(providers):** resolve the Codex quota auto-ping model from the live provider catalog and lifecycle registry instead of the retired `gpt-5.1-codex-mini`, and pause the ping with one actionable warning when no selectable Codex model exists rather than retrying a shut-down id every cooldown window ([#11905](https://github.com/diegosouzapw/OmniRoute/issues/11905))
- **fix(api):** keep the `{created, data}` wrapper on combo-routed `/v1/images/generations` responses and default Codex image results to `b64_json` on both `/v1/images/generations` and `/v1/images/edits` so Codex CLI's built-in `image_gen` can decode them ([#12268](https://github.com/diegosouzapw/OmniRoute/issues/12268))
- **fix(sse):** Name the shadowed custom provider node when a built-in provider id/alias (e.g. `openference` → `of`) reserves the prefix of an existing OpenAI/Anthropic-compatible node, so the runtime `No active credentials for provider: <built-in>` error explains that the prefix routed to the built-in and never reached the node's healthy connections, instead of contradicting the dashboard ([#11943](https://github.com/diegosouzapw/OmniRoute/issues/11943)) — thanks @morpheus9393
- **fix(cli):** `omniroute tunnel create` no longer crashes with `Cannot read properties of undefined (reading optsWithGlobals)` — removed the duplicate positional argument that caused Commander.js to misalign the action callback parameters ([#12295](https://github.com/diegosouzapw/OmniRoute/issues/12295), [#12368](https://github.com/diegosouzapw/OmniRoute/pull/12368))
- **fix(i18n):** wrap `ccOnboardingKeyPlaceholder` in ICU single quotes across all 43 locales so angle brackets render literally instead of being parsed as rich-text tags, which crashed the Claude Code onboarding block with `INVALID_MESSAGE: INVALID_TAG` ([#12302](https://github.com/diegosouzapw/OmniRoute/issues/12302))
- **fix(models):** publish `effort_tiers` on Kimi K3's synced base-model entries (`kmca/k3`, `kmca/k3-256k`) so catalog-only clients (OpenCode, plain SDK pickers) can see and select the reasoning tiers (`low`/`high`/`max`) the synced metadata already carried — the `isSkippedEffortProvider` gate no longer suppresses tier visibility on those base entries, while synthetic `<id>-<tier>` variant generation stays prevented and Codex/GLM base models remain excluded unchanged ([#12299](https://github.com/diegosouzapw/OmniRoute/issues/12299))
- **fix(guardrails):** keep `auto`/`auto/*` virtual combos exempt from the Vision Bridge `fixedModel` credential guard so a combo target is passed through instead of silently falling back to global auto-selection ([#12237](https://github.com/diegosouzapw/OmniRoute/issues/12237))
- **fix(combo):** capability-filter exhaustion caused by `max_tokens` above every target's known output limit now reports that reason (requested `max_tokens` vs the pool's highest known ceiling) instead of the unrelated "supports structured output" message ([#12229](https://github.com/diegosouzapw/OmniRoute/issues/12229)) — thanks @DW-MediaLab
- **fix(auth):** the `least-used` account strategy now prefers accounts without backoff before falling back to oldest `lastUsedAt`, the same tie-break `round-robin` already applies, so a failover no longer lands on a just-rate-limited account for a single request ([#12279](https://github.com/diegosouzapw/OmniRoute/issues/12279)) — thanks @tenshiak
- **fix(docker):** the `chatgpt-web-codex-browser` image now finds the Chrome binary under `chrome-linux64/` (Chrome for Testing layout in `playwright:v1.62.0-noble`) as well as the legacy `chrome-linux/`, so the container no longer crash-loops with `exec: --headless=new: not found` ([#12024](https://github.com/diegosouzapw/OmniRoute/issues/12024))
- **fix(providers):** declare `groq/compound` and `allam-2-7b` as non-reasoning models in the curated Groq registry so `reasoning_effort` / `output_config.effort` / `thinking` from Claude Code are stripped instead of forwarded, which Groq rejected with HTTP 400 ([#12134](https://github.com/diegosouzapw/OmniRoute/issues/12134))
- **fix(executors):** `OpencodeExecutor` no longer forces a direct connection when the connection has a proxy assigned in Proxy Management but no per-account proxies: the single-account fast path used to wrap the upstream dispatch in the direct-egress sentinel, discarding the ambient proxy context the chat handler had pinned from `proxy_assignments`, so API-key `opencode`/`opencode-go` connections egressed from the host IP (and hit geoblocks) despite the assignment. The direct pin is now applied only when no ambient proxy context exists ([#11894](https://github.com/diegosouzapw/OmniRoute/issues/11894) — thanks @hizzt)
- **fix(api):** `GET /v1/models` with `MODELS_CATALOG_PREFIX_MODE=canonical` (or `?prefix=canonical`) now lists providers whose registry alias is undefined or equal to their own id (Antigravity, Antigravity CLI and other self-aliased built-ins) — their single `provider/model` id was dropped by the alias/canonical duplicate guard in the static, synced, custom and alias-backed catalog loops ([#12058](https://github.com/diegosouzapw/OmniRoute/issues/12058)) — thanks @cheynetom
- **fix(translator):** Drop replayed `thinking` blocks that carry no signature (the shape produced from cross-provider `reasoning_content`) instead of stamping the default Claude signature on them, which Anthropic rejected with `400 Invalid signature in thinking block` on the next turn served by an Anthropic rung ([#12105](https://github.com/diegosouzapw/OmniRoute/issues/12105)) — thanks @atescivitci-cmd
- **fix(cli):** Resolve Bun's `--preload` polyfill path against the package root instead of `dist/`, so `omniroute` installed with `bun install -g` no longer crashes at startup with `error: preload not found …/dist/open-sse/utils/setupPolyfill.ts` ([#11980](https://github.com/diegosouzapw/OmniRoute/issues/11980)) — thanks @joglomedia
- **fix(providers):** `gemini-business` now publishes its model catalog — `/v1/models` and `/v1/providers/gemini-business/models` list the 12 enterprise Gemini ids the executor understands instead of returning an empty list (#12107)
- **fix(db):** install `busy_timeout` before the SQLite connection's first statement so a process opening the database while another one closes its WAL connection waits out the transient EXCLUSIVE lock instead of dying with `database is locked`, and recognise the drivers' real BUSY/PROTOCOL/IOERR errors as transient in the corruption probe so the same lock no longer renames the database away as corrupt; deflakes `cross-process contenders never both acquire the same connection` (#12394 — thanks @pacocartones)
- **fix(chat-admission):** derive the `chat_admission_busy` 503 `Retry-After` from observed heavyweight-lease occupancy — the larger of the exhausted `OMNIROUTE_CHAT_ADMISSION_QUEUE_MS` window and the time since capacity last turned over, capped at 60 s — instead of a fixed 1 s (structural) / 2 s (byte-stage) hint that invited Codex/agent fan-out clients to re-send ~1 MiB `/v1/responses` bodies every second into a gate held for the whole SSE lifetime; an idle gate keeps the historical floors ([#12135](https://github.com/diegosouzapw/OmniRoute/issues/12135)) (#12395 — thanks @pacocartones)
- **fix(api-manager):** the API key permissions modal no longer silently drops `allowedCombos` entries its Combo picker cannot render — routing-rule names such as `rt-*`, which the backend already honours — when "All" is clicked and the key is switched back to "Restrict"; those entries now survive the toggle, are listed read-only under the combo list so the count and the list agree, and are saved back verbatim instead of persisting `[]` (deny-all) (#12397 — thanks @pacocartones)
- **fix(catalog):** write the NUL separator of the catalog connection memo key, the provider serviceKind memo key, the Video Bridge promotion group key and a JSON-exactness test fixture as the `\u0000` escape instead of a raw byte — same runtime value, but the raw byte made git, GitHub and ripgrep treat those files as binary (hidden PR diffs, silently skipped searches); a guard test now keeps raw NUL bytes out of `src/`, `open-sse/` and `tests/` (#12403 — thanks @pacocartones)
- **fix(providers):** add `CLAUDE_CODE_CLIENT_VERSION` and `GITHUB_COPILOT_CLI_VERSION` env overrides so Anthropic/Copilot client-version gates can be unblocked without a rebuild ([#12417](https://github.com/diegosouzapw/OmniRoute/issues/12417))
- **fix(db):** back-fill `last_ping_at` and `last_pinged_reset_key` on `provider_connections` during schema reconciliation so divergent lineages that skipped `123_quota_auto_ping` still accept quota auto-ping writes ([#12470](https://github.com/diegosouzapw/OmniRoute/pull/12470) — thanks @KooshaPari)
- **fix(ci):** document MIT exceptions for `@eloqnt/{config,format-json,format-po}` (next-intl transitive; locked tarballs omit `license`) and keep the A2A lifecycle vitest off the real SQLite persistence seam ([#12581](https://github.com/diegosouzapw/OmniRoute/issues/12581))
- OpenCode plugin `/v1/models` catalog fetch now waits 30s by default and attaches HTTP `statusCode` on 401/5xx so host fallback plugins can hop instead of seeing an untyped AbortError/UnknownError.
- **CI:** the OpenAPI security-tier gate now mirrors `isAlwaysProtectedPath()` in full — it also reads `ALWAYS_PROTECTED_API_PATTERNS`, so the pattern-gated credential routes (`/api/providers/{id}/{claude,codex}-auth/{export,apply-local}`, GHSA-5926-2w35-7h4q) no longer report as unannotated. (#12605)
- **fix(api):** GET /v1/models no longer waits forever on a hung coalesced catalog rebuild; cold-path waits are bounded (`CATALOG_BUILD_TIMEOUT_MS`, default 8s) and a last-good 200 is served when the rebuild times out ([#12627](https://github.com/diegosouzapw/OmniRoute/issues/12627)).
- **fix(providers):** Perplexity Web no longer turns upstream stream failures into successful assistant text; pre-content failures remain eligible for fallback, partial output ends with a structured sanitized error, and failed sessions are not persisted
- **Z.ai Web:** HTTP 200 streams carrying an upstream error now terminate with a structured failure instead of assistant text plus a normal stop, preserving partial output while allowing pre-content combo fallback.
- **fix(security):** sanitize `request.failed` diagnostics before publishing them to live dashboard listeners and replay history, while keeping status, model, provider, latency, and internal call-log diagnostics intact.
- **fix(memory):** Embedding Model Quick select, Embedding Source remote dropdown, and Rerank selector now list every configured provider with embedding/rerank support instead of only chat-catalog text matches plus OpenRouter live discovery; a generic OpenAI-compatible `/embeddings` + Cohere-compatible `/rerank` runtime fallback resolves any configured chat provider's embedding/rerank endpoint, so unlisted providers no longer fail with "Unknown embedding provider"; both memory selectors gained a free-text model override
- Harden SQLite upgrades around the historical migration-074 version collision: missing discovery and inspector tables are replayed atomically, pre-existing databases (including setup-created skeletons) receive reusable content-addressed safety snapshots, and Node test/eval probes without `DATA_DIR` are isolated from the operator database.
- **fix(grok-web):** treat upstream streaming failures as failures instead of successful
assistant text: error-only streams now fail readiness with HTTP 502, while failures after
legitimate content preserve that partial output and terminate through the sanitized stream
failure path without a normal `stop` completion.
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.