Compare commits

...

345 Commits

Author SHA1 Message Date
diegosouzapw
262c4e9046 fix(security): container/Fly REQUIRE_API_KEY posture + free-tier usage leak (#13679)
PR E of the #13679 insecure-defaults umbrella (items #6, #7; item #8 analyzed
as by-design, no change). The published Docker image and fly.toml shipped
without REQUIRE_API_KEY set, so a bare `docker run` (README/QUICK-START
one-liners, no --env-file) or a `fly deploy` combined "keyless" with
"world-reachable" for the anonymous /v1 LLM proxy. docker-compose.yml already
mitigates this via loopback-only binding (#12568) and correctly keeps
following the operator's own .env, so it is untouched. The npm/CLI
local-first REQUIRE_API_KEY=false default in featureFlagDefinitions.ts is
also untouched per the owner's decision.

/api/free-tier/summary ships an unconditional Access-Control-Allow-Origin: "*"
and always included the operator's own local usedThisMonth/remaining usage
regardless of auth — a low-severity info leak to any reachable origin. Both
fields are now withheld from unauthenticated callers while the intentionally
public catalog data stays served to everyone.

The gemini-SSE (openai-to-gemini-sse.ts) sub-finding needed no code change:
/v1beta/models/*:streamGenerateContent is already classified CLIENT_API and
fronted by clientApiPolicy through src/proxy.ts before the translator ever
runs, and its CORS-header echo was already hardened fail-closed by #12573.
REQUIRE_API_KEY=true (this PR's container/Fly default) closes the dependency
that finding cited. Added a locking regression test confirming this chain.

Regression tests:
- tests/unit/issue-13679-container-posture-require-api-key.test.ts
- tests/unit/issue-13679-free-tier-summary-usage-leak.test.ts
- tests/unit/issue-13679-gemini-sse-requires-api-key.test.ts (confirmation)

Refs #13679
2026-09-16 14:08:52 -03:00
Diego Rodrigues de Sa e Souza
80828fc88a fix(sse): say when an API key's allowlist is what hid every connection (#13879)
#13832. A user reported that `nvidia` and `openrouter` — added after the
initial setup — always failed chat with `No active credentials for provider: X`,
while on the same instance and the same minute `/api/providers/{id}/test`
returned valid and `/sync-models` pulled 82 models.

Reproducing the resolution chain on the tip shows no defect in it: a connection
created exactly as `POST /api/providers` creates one resolves for every model
tried, and creation order is irrelevant — the query is `provider = ? AND
is_active = 1`, there is no boot-time registry and no migration that backfills
only older rows.

The three-line AUTH log the reporter pasted is reachable from exactly one place:
the pool arriving EMPTY at the key-policy filter. Every post-query skip produces
a different message ("all N accounts unavailable"). So the connections exist and
are active; the calling key's `allowed_connections` / quota scope removed them —
the shape you get from a key minted before those providers existed, which is
also why the older providers on that key keep working.

The real defect is that nothing ever said so. `/test` and `/sync-models` address
a connection by id and never consult the key's scope, so they cannot contradict
it, and the one log line that hinted at the filter became `debug` in #11937.

`getProviderCredentials` now counts the connections it had before applying the
key policy and, when that filter is what emptied the pool, returns
`{ blockedByKeyPolicy, blockedCount }` instead of a bare null. `handleNoCredentials`
turns it into a 403 naming the allowlist and the fix, alongside the existing
allRateLimited/allExpired branches. 403, not 401: the credential is valid, this
principal just may not use it.

Test is red-first in tests/unit/chat-helpers.test.ts (it asserts the status, the
count and that the message names the gate).

This does not close the report on its own — it makes the next occurrence
self-explanatory. The reporter still needs to confirm their key's
allowed_connections/allowed_quotas.
2026-09-16 13:35:49 -03:00
Diego Rodrigues de Sa e Souza
ba274b616a fix(providers): validate Zylo keys against the chat route, not its open catalog (#13877)
#13828. `zylo-api` is registered as OpenAI-compatible, so the generic probe
validated a key with `GET /v1/models` and returned on the first 2xx. Zylo serves
that route WITHOUT authentication — it answers 200 with no Authorization header
at all, and 200 for a bogus key — so the account-setup dialog greened any
string. The first request Zylo actually authenticates is the user's own model
test, which comes back `401 {"error":"Key not found: zk-…"}`.

Running the production validator against a fake key returned `{valid:true}`
before this change.

Two corrections to the report: nothing passes a key value where a key name is
expected — there is no such lookup — and that 401 text is Zylo's own, not
OmniRoute's. The defect is a false-green validation, which is worse: an invalid
key is stored as working and only fails later, at the model level.

`POST /v1/chat/completions` is authenticated, so a single probe there is the
correct auth check — the remedy already applied to dify (#11002) and bytez
(#5422). Registered under both `zylo-api` and the `zylo` alias, matching the
adobe-firefly/firefly pair, so a connection stored under the alias does not fall
back to the open-catalog probe.

Tests are red-first: a key the chat route rejects must not validate, the catalog
route must not be consulted at all, a key it accepts still validates, and the
alias takes the same path. The first and third failed before the fix.

Not in scope, reported separately: Zylo's catalog is not OpenAI-shaped
(`{text:[…],image:[…]}`), so model sync yields 0 models.
2026-09-16 13:35:33 -03:00
lorenzozane
309b635740 fix(providers): bypass web-search fallback for Antigravity target (#13447) (#13765)
Merged. The repro in the description is exactly right — rewriting a native `web_search` tool into `omniroute_web_search` for a client that never declared it makes streaming Responses clients disconnect. Mirroring the existing Gemini bypass keeps the change to the one target that needs it, and the suite goes from 20 pass / 3 fail on base to 23 / 0 with the fix.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
2026-09-16 13:29:05 -03:00
lorenzozane
515bf8ce3b test(embeddings): cover custom embeddings authorization (#13763)
Merged. This is not covered by the existing `embeddings-auth.test.ts`, which asserts the *inbound* auth; yours records the *outbound* contract — the key configured on a custom OpenAI-compatible connection must leave as `Authorization: Bearer <key>`, with the mock returning 401 when it does not. That contract had no guard until now.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
2026-09-16 13:21:48 -03:00
lorenzozane
1510fb0d62 test(combo): gate-level regression for stale persisted-cooldown skip (#13694) (#13767)
Merged. Test-only and worth it: the production mechanism is already fixed on this base (#12168 grace-bounded the persisted-`unavailable` skip, #12899 fixed combo-name allow-listing), but nothing stopped it from regressing. Your cases pin both directions — a stale/orphan persisted cooldown proceeds through the gates, a genuinely fresh one still skips.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
2026-09-16 13:14:37 -03:00
lorenzozane
ed2e22197a fix(providers): add missing modelsUrl for qwen-cloud-token-plan (#13764)
Merged. One missing field, one very concrete symptom: without `modelsUrl` the import silently fell back to the local catalog with "API unavailable". The test asserts both the registry entry and the derivation, so a future registry edit cannot drop it again.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you.
2026-09-16 13:07:39 -03:00
Bob.Hou
e7999c477b fix(db): bound health scans and isolate native diagnostics (#13717)
Merged after a maintainer rework that kept every one of @HouMinXi's commits intact.

**What the rework added on top of the contribution:** the new DB health-check behaviour is gated behind a default-off feature flag (`src/shared/constants/featureFlagDefinitions.ts`, `defaultValue: "false"`), documented in `docs/reference/FEATURE_FLAGS.md` with the description key carried into all 66 locales, so the release default is unchanged and the new bounds only apply when an operator opts in. The optional-FTS5 migration set was reconciled by hand with the "180" entry that landed meanwhile (`src/lib/db/migrationRunner/constants.ts`).

**Carried from your rebased head:** the `/api/db/health` local-only classification in `src/server/authz/routeGuard.ts` plus its `routeGuard` assertion — `runManagedDbHealthCheck()` forks native diagnostics into a child process, so Hard Rules #15/#17 apply. Re-verified here: 37 pass / 0 fail.

Validated as a combined board first (this PR merged with the 21 siblings of the same wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-counts, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 176 passing / 0 failing focused node:test cases across the 25 test files the wave touches and the dashboard test under Vitest (2/0). Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thank you for the depth of this one — the resource-bounds suite and the sql.js startup/backup coverage are the kind of tests that keep a database layer honest.
2026-09-16 12:59:45 -03:00
Diego Rodrigues de Sa e Souza
9d9284417c fix(cli): translate the CLI for every locale (38 catalogs were nearly empty) (#13892)
sync-ui-keys --catalog=cli + blocking i18n:check-keys:cli gate; 65 CLI catalogs synced (52,000 strings, 0 __MISSING__, all 830 keys, placeholders verified). ⚠️ base-red inherited: #12732
2026-09-16 12:27:59 -03:00
Diego Rodrigues de Sa e Souza
9e36dde2f7 feat(i18n): retranslate-site rewrites the verbatim-English leaves of the site catalogs (#13886)
scripts/i18n/retranslate-site.mjs + untranslatable-site-keys.json (22 keys) + tests; the run landed on the site as OmniRouteSite#8 (2,059 leaves, mean English residue 10.3 % → 6.3 %). ⚠️ base-red inherited: #12732
2026-09-16 12:19:50 -03:00
Diego Rodrigues de Sa e Souza
060f331264 fix(i18n): pt-BR review pass over the retranslated leaves (172 corrections) + review-locale script (#13885)
Reviewer pass (native-speaker prompt) over the 1,865 pt-BR leaves retranslated in #13782: 172 corrections applied; new scripts/i18n/review-locale.mjs with tests. ⚠️ base-red inherited: #12732
2026-09-16 12:19:40 -03:00
Diego Rodrigues de Sa e Souza
7f496c79d0 fix(ci): drain three base reds blocking every PR — generated SKILL.md, stryker registry, env/docs contract (#13834)
* docs(skills): regenerate omni-settings SKILL.md for the egress-observation endpoint

* fix(ci): register provider-401-ambiguous-runtime test in stryker tap.testFiles

* fix(ci): drain current base drift — regenerate cli-mcp SKILL.md, register 4 stryker tests

* fix(ci): document OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE and CDP_PROXY_TOKEN (env/docs contract)
2026-09-16 12:00:53 -03:00
Diego Rodrigues de Sa e Souza
1cb4c82071 fix(ci): bound the forgotten-sibling report so an advisory step stops failing the job (#13889)
"Fast Quality Gates" is red on every open PR against release/v3.8.51. The failing
step is `forgotten-sibling-tests`, which is explicitly advisory — its own output
says "Report-only calibration: these findings do not fail the job" — yet it exits 1.

When a PR diff touches a hub module (`open-sse/config/providerRegistry.ts` in the
current reds), the analysis walks every import edge in the repo and multiplies each
consumer by its candidate tests. The result reaches millions of rows, and
`lines.join("\n")` then exceeds V8's maximum string length. The throw lands in
main()'s catch, which exits 1 — so an advisory report takes the whole job down.

Measured with a synthetic hub cross-product, before the change:

  3,000,000 findings -> a 435 MB report string (no throw, but absurd)
  4,500,000 findings -> Invalid string length   (the CI failure, verbatim)

After: the same 4,500,000 findings render as 27 KB.

The fix bounds only the ENUMERATION. The header keeps the exact totals, so the
signal ("this diff has N unreviewed sibling tests") is unchanged; at most 200 rows
per section are listed, followed by a line naming how many were withheld. The JSON
artifact gets the same treatment (5,000 items per array) plus an explicit `totals`
object, since `JSON.stringify` would throw on the same input for the same reason.

`markdown()` is exported so the bound is testable without a CI-sized diff.
2026-09-16 11:45:40 -03:00
Diego Rodrigues de Sa e Souza
5faf44f975 fix(docs): restore the env/docs contract broken by the #13679 vars (#13875)
`check:env-doc-sync` is failing on the release tip, which fails "Docs Gates
(fast-path)" on every open PR against release/v3.8.51 (base-red #13866).

Both gaps come from #13679:

- `OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE` is read in src/lib/cloudSync.ts but
  was in neither .env.example nor ENVIRONMENT.md. Documented with the behaviour
  the code actually implements: opt-in rejection of an UNSIGNED response when no
  local secret is configured, default off for v3.8.x back-compat, and a present
  signature always verified — and always rejected when
  OMNIROUTE_CLOUD_SYNC_SECRET is unset — regardless of the flag.
- `CDP_PROXY_TOKEN` was in .env.example but missing from ENVIRONMENT.md. Added to
  the ChatGPT Web (Codex) table next to CHATGPT_WEB_CODEX_CDP_URL, in that
  section's language, describing the X-Omni-Cdp-Token header the sidecar expects
  and the compose-network isolation that applies when it is unset.

Docs only, no code change.

Verified on this branch: check:env-doc-sync reports all three directions in sync
(817 vars in .env.example, 834 in ENVIRONMENT.md); check:docs-sync passes;
check:docs-counts reports only pre-existing soft drift.
2026-09-16 10:40:27 -03:00
Diego Rodrigues de Sa e Souza
ec98525326 fix(tests): list dist/httpClientAbortGuard.mjs in the pack-artifact policy assertion (#13872)
Merged on local evidence: the assertion is red on a pristine release/v3.8.51 (19 pass / 1 fail) and green here (20/0). One-line test fix; the packaging policy itself is untouched.
2026-09-16 10:10:32 -03:00
Bob.Hou
0cc0169360 fix(db): give conversation_turn_nodes its own 1-day retention (#13344) 2026-09-16 08:23:17 -03:00
Bob.Hou
b4f51b2e9e fix(providers): restore grok-4.6/4.5 default reasoning effort (#13628) 2026-09-16 08:15:21 -03:00
Bob.Hou
54f19c7742 feat(providers): fetch live xAI catalog for xai-oauth (#13518) 2026-09-16 08:08:49 -03:00
Bob.Hou
cdcde97c70 feat(providers): add Agnes AI (China) as agnes-cn on api.agnes-ai.cn (#13399) 2026-09-16 08:01:39 -03:00
Diego Rodrigues de Sa e Souza
19b9d05e08 fix(providers): xAI translators drop legacy function_call and zero total_tokens (#12692, #12700) (#13753)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:18:37 -03:00
Diego Rodrigues de Sa e Souza
47032e7769 fix(providers): correct Magnific key validation probe path (#12927) (#13754)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:18:22 -03:00
Diego Rodrigues de Sa e Souza
8939ccbae3 fix(sse): dynamic-specifier require for wreq-js in Codex WS transport (#12491) (#13756)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:17:57 -03:00
Diego Rodrigues de Sa e Souza
5ff85c6db6 fix(oauth): fall back to public Code Suggestions on any GitLab Duo direct_access 403 (#12958) (#13758)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:17:42 -03:00
Diego Rodrigues de Sa e Souza
8f6205e36a fix(providers): backfill combo context limit from snapshot after cold start (#13000) (#13759)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:17:27 -03:00
Diego Rodrigues de Sa e Souza
694c1b74eb fix(cli): add POST /api/mcp/restart and mcp enable/disable subcommands (#13012) (#13770)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:17:04 -03:00
Diego Rodrigues de Sa e Souza
9578eeb380 fix(sse): widen max_tokens/tool_use exemption to lone empty text blocks (#12968) (#13771)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:16:49 -03:00
Diego Rodrigues de Sa e Souza
da58ce590f fix(skills): repair nested malformed schemas in injected skill tools (#13022) (#13772)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:16:34 -03:00
Diego Rodrigues de Sa e Souza
9342e4bf68 fix(sse): accept Responses API custom tool_choice (#13122) (#13775)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:16:18 -03:00
Diego Rodrigues de Sa e Souza
aeba6b1a04 fix(routing): publish dashboard events from the round-robin combo loop (#13089) (#13776)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:16:02 -03:00
Diego Rodrigues de Sa e Souza
0dbd7f47d2 fix(sse): classify missing Chromium as a Z.ai host/config cooldown (#13232) (#13777)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:15:47 -03:00
Diego Rodrigues de Sa e Souza
74e44d7630 fix(db): defer process.exit(0) by a macrotask on graceful shutdown (#13306) (#13778)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:15:31 -03:00
Diego Rodrigues de Sa e Souza
0459c6dc0a fix(cli): surface fatal [STARTUP] boot diagnostics without --log (#13314) (#13779)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:15:15 -03:00
Diego Rodrigues de Sa e Souza
94220af289 fix(providers): stop zed-hosted claude-haiku-4-5 thinking from inflating max_tokens (#13364) (#13780)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:15:00 -03:00
Diego Rodrigues de Sa e Souza
05b44fa48e fix(db): stop backoff-reset from busting the model catalog cache (#13389) (#13783)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:14:46 -03:00
Diego Rodrigues de Sa e Souza
f5501cf9a3 fix(providers): gemini-web no longer drops system instructions or the tool contract (#13380) (#13784)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:14:31 -03:00
Diego Rodrigues de Sa e Souza
57f41f5dd1 fix(sse): frame post-keepalive /v1/responses stream errors with a type field (#13431) (#13785)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:14:14 -03:00
Diego Rodrigues de Sa e Souza
d2fadb01bc fix(db): reconcile INCREMENTAL auto_vacuum drift via the vacuum scheduler (#13432) (#13786)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:13:59 -03:00
Diego Rodrigues de Sa e Souza
842c32f6f0 fix(compression): keep tool messages through lite redundant-remove (#13429) (#13787)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:13:44 -03:00
Diego Rodrigues de Sa e Souza
ad4d286c85 fix(sse): forward cache-creation tokens through the responses usage hop (#13472) (#13790)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:13:29 -03:00
Diego Rodrigues de Sa e Souza
edeb76b96f fix(sse): stop PII sanitizer splicing OpenRouter metadata into content (#13488) (#13792)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:13:14 -03:00
Diego Rodrigues de Sa e Souza
5d1f4687eb fix(sse): fail closed on background token-refresh for dead proxy pools (#13470) (#13793)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:12:59 -03:00
Diego Rodrigues de Sa e Souza
2e2dff79f2 fix(cli): redraw Windows tray icon with dark outline + ship icon.ico (#13535) (#13797)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:12:43 -03:00
Diego Rodrigues de Sa e Souza
2bbe6e575b fix(sse): stop unhydrated compatible connections routing to the real OpenAI/Anthropic API (#13452) (#13798)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:12:28 -03:00
Diego Rodrigues de Sa e Souza
68927fc711 fix(providers): MiniMax-M3 reasoning leaks into delta.content instead of reasoning_content (#13558) (#13799)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:12:12 -03:00
Diego Rodrigues de Sa e Souza
675ab1875e fix(api): persist hideAutoCombos/hideNoThinkVariants in settings schema (#13562) (#13800)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:11:56 -03:00
Diego Rodrigues de Sa e Souza
0ba661ed97 fix(providers): surface real Antigravity upstream error detail (#13591) (#13801)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:11:41 -03:00
Diego Rodrigues de Sa e Souza
61d6152699 fix(usage): thread real error/exit-code through callLogs worker failOpen (#13597) (#13802)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:11:27 -03:00
Diego Rodrigues de Sa e Souza
d7d518a873 fix(api): record audio transcription/translation/speech requests in call_logs (#13544) (#13803)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:11:13 -03:00
Diego Rodrigues de Sa e Souza
79c4197f07 fix(security): reject unverifiable X-Cloud-Sig in cloud-sync HMAC check (#13679 PR A) (#13804)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:10:58 -03:00
Diego Rodrigues de Sa e Souza
b33c00b0b6 fix(db): cap per-request batch sweep and guard shared files (#13680, #13681) (#13805)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:10:43 -03:00
Diego Rodrigues de Sa e Souza
9e53fadc47 fix(sse): recognize bare delta.reasoning in combo streaming quality-gate peek (#13620) (#13806)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:10:28 -03:00
Diego Rodrigues de Sa e Souza
771c4ce513 fix(providers): echo reasoning_content for bai DeepSeek thinking-mode follow-ups (#13599) (#13807)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:10:13 -03:00
Diego Rodrigues de Sa e Souza
3fd2440f8a fix(sse): stop re-prepending Kiro tool docs onto every subsequent turn (#13652) (#13808)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:09:58 -03:00
Diego Rodrigues de Sa e Souza
8e3d06bd9f fix(security): isolate CDP proxy network + auth gate (#13679 PR F) (#13811)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:09:44 -03:00
Diego Rodrigues de Sa e Souza
c06ac9aafa fix(security): remove literal secrets from podman manifest, block CHANGEME remote login (#13679) (#13812)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:09:29 -03:00
Diego Rodrigues de Sa e Souza
4de71978b3 fix(security): random per-process self-loop admission bearer (#13679) (#13813)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:09:15 -03:00
Diego Rodrigues de Sa e Souza
5574eab259 fix(api): restore function_call name on non-streaming /v1/responses (#12370) (#13824)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:08:58 -03:00
Diego Rodrigues de Sa e Souza
7ca64796ed chore(ci): clear the last two release/v3.8.51 base-reds (agent-skills sync + stryker tap.testFiles) (#13826)
Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging).
2026-09-16 06:08:44 -03:00
Bob.Hou
e7214c72fc ci(acceptance): emit a shadow release-acceptance report next to release-green (#13701)
Adds a shadow release-acceptance report alongside release-green: an inventory/reduce/oracle pipeline under `scripts/quality/release-acceptance/` with a JSON schema, fixtures and a workflow that uploads the report as an artifact.

Contained by design, which is why it merges as-is: it runs only on push to `release/v*` and on manual dispatch (never on pull requests), the step is `continue-on-error`, `permissions: contents: read`, `persist-credentials: false`, and it consumes no secrets. Nothing in the product changes; the report is advisory until we decide to promote it.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 03:17:52 -03:00
Bob.Hou
f36fcd8c31 fix(resilience): extend process crash guard to combo hedge cancels and upstream fetch failures (#13636)
Closes a real process-killer: the direct-response start timeout could fire after the fetch promise had already settled, and aborting at that point delivered the abort reason to a promise nobody was awaiting — Node promotes that to an `unhandledRejection` → `uncaughtException` and the process dies (#12861). The timer is now a no-op once the attempt has settled, and the same guard is extended to combo hedge cancels and upstream fetch failures.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 03:03:07 -03:00
Diego Rodrigues de Sa e Souza
d7d010b102 feat(i18n): blocking key-completeness gate — every locale carries every en.json key (#13827)
New blocking gate `i18n:check-keys` (`scripts/i18n/check-key-completeness.mjs`): every locale catalog must carry exactly the key set of `en.json`, whatever the age of the key. The percentage and new-key gates let batch 1 (#13044) ship 43 keys short and batch 2 (#13660) 10 keys short. Wired into the i18n-ui-coverage job; documented in QUALITY_GATES.md and the i18n guide (post-merge re-sync, retranslation with pinned names).

⚠️ base-red inherited: #12732
2026-09-16 02:52:31 -03:00
Bob.Hou
6ef0f1c06c fix(memory): skip FTS rewrite on access-count updates (#13331)
Restores `memory_id` to the `memory_fts_au` trigger's `WHEN` clause and adds an FTS5 rebuild to the memory cleanup pass.

This repairs a regression that landed yesterday: migration 178 guarded the trigger with `old.content IS DISTINCT FROM new.content OR old.key IS DISTINCT FROM new.key`, which drops the `memory_id` term the insert path depends on. `createMemory` inserts the row, the AFTER INSERT trigger stores an auto-assigned FTS5 rowid, and the follow-up `UPDATE memories SET memory_id = rowid` is what re-syncs FTS — an update that touches neither `content` nor `key`. With 178 alone that update stopped firing, so newly created memories drifted out of the FTS index and keyword/hybrid search silently returned nothing for them. Migration 180 adds the third term back.

Maintainer note before merge: the cleanup half now runs `rebuild` on every pass rather than `optimize` only when rows were deleted — accepted as-is; it is bounded by the memory table size and the trigger fix removes the bloat source that motivated it.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:49:32 -03:00
Bob.Hou
c68b7f58c2 fix(gemini): send thinkingLevel for 3.8 Flash so thoughts stop eating the output cap (#13463)
Gemini 3.8 requests now carry `thinkingLevel` instead of a numeric `thinkingBudget` in both request translators, and `includeThoughts` is no longer injected unless the client asked for it — the numeric budget was being spent on thoughts and truncating the visible answer (`finish_reason=length` with the reply cut short).

Scoped to model ids matching `gemini-3.8` (including the `agy/` and `antigravity/` surfaces); 2.5, 3.1 and 3.7 are untouched, verified at each of the four write sites.

Maintainer note: on the Antigravity envelope path a request without an explicit `max_tokens` no longer keeps `maxOutputTokens` (the guard there reads `thinkingBudget`, which is now absent). The upstream picks the limit in that case; accepted.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:37:32 -03:00
Bob.Hou
d6a0460bd6 fix(providers): declare Agnes official thinking effort tiers (#13655)
Declares the accepted thinking-effort tiers per Agnes chat model (2.0/2.5: none/low/medium/high/max; 3.0 adds minimal/xhigh), so the generic declared-tier clamp maps `xhigh`/`off` onto values the upstream accepts instead of forwarding them verbatim and collecting a 400.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:23:53 -03:00
Bob.Hou
d589e6b76e fix(providers): clamp SenseNova DeepSeek V4 Flash effort to high (#13626)
SenseNova DeepSeek V4 Flash now clamps `xhigh`/`max` down to `high` before the generic max-tier rewrite, and the model stops advertising `xhigh` — both values were rejected upstream. The clamp covers the first-party `sensenova` provider and openai-compatible connections that address the model as `snova/deepseek-v4-flash`.

Explicitly preserved: `sensenova/glm-5.2` and `deepseek-v4-flash` on `cmd`/`opencode-go`/`ollama-cloud` keep `max`.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:20:58 -03:00
Bob.Hou
2b3847e775 fix(sse): strip trailing assistant prefill on official Claude OAuth (#13572)
Adds `claude` to the providers whose trailing text-only assistant turn is stripped before dispatch — official Claude rejects assistant prefill with `400 This model does not support assistant message prefill`.

Maintainer note: the strip applies to the whole `claude` provider family (API key as well as OAuth), matching what the Vertex-hosted Claude path (`open-sse/executors/antigravity.ts`), the Copilot path (`open-sse/executors/github.ts`) and the MITM handler already do unconditionally.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:18:20 -03:00
Bob.Hou
67f6c039f3 fix(embeddings): send stored API key on private-host embeddings nodes (#13398)
When an embeddings provider is classified `authType: "none"` and no credentials resolved, the service now looks up the stored connection and promotes to bearer if it holds a key — so private-host/CGNAT embeddings nodes that do require a key stop being called anonymously (#13234).

Security posture holds: `isNoAuthLocalEmbeddingHost` is `isPrivateHost(hostname) && !isCloudMetadataHost(hostname)`, so cloud metadata addresses never reach `authType: "none"` and therefore never reach the new branch; the key only ever goes to the host the operator configured on that connection.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 02:00:54 -03:00
Bob.Hou
4902ac8128 fix(network): skip Chrome TLS impersonation for Groq (#13445)
Groq is now excluded from Chrome TLS impersonation in both the direct and the proxied dispatch branch, even when `TLS_FINGERPRINT_PROVIDERS` is unset or explicitly lists it — Cloudflare answers the spoofed fingerprint with 1010 `browser_signature_banned` (#13225).

Scope is contained: the whole path is behind `ENABLE_TLS_FINGERPRINT`, which defaults to off, so nothing changes for operators who never opted in.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 01:54:00 -03:00
Bob.Hou
ce98c30cfb fix(combos): stop Gemini thinking from failing dashboard combo tests (#13560)
Two related fixes: the combo health probe sends `reasoning_effort: "none"` for Gemini-family models so the probe budget is not spent on thinking, and `detectMalformedNonStream` stops classifying a response with `finish_reason` `length`/`tool_calls`/`content_filter` and empty content as `empty_choices`.

The second half is the important one: it brings the post-translation check in line with `isEmptyContentResponse` (`open-sse/services/errorClassifier.ts`, `LEGIT_EMPTY_OPENAI_FINISH`), which already treated those finish reasons as legitimate. Until now a response could pass the pre-translation check and still be rewritten into a synthetic 502 afterwards — for every non-streaming completion, not just combo probes.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 01:47:32 -03:00
Bob.Hou
79ebbb525f fix(codex): whitelist reasoning object keys before the wire (#13643)
The Codex executor now whitelists the wire `reasoning` object to `effort`/`summary` before dispatch instead of spreading whatever the client sent, and maps `reasoning.enabled === false` to `effort: "none"` when no more specific effort was requested. OpenRouter-style keys (`enabled`, `max_tokens`, `exclude`) were reaching the Responses API and 400-ing the whole combo target with `Unknown parameter: 'reasoning.<key>'`.

The precedence chain keeps an explicit per-request effort ahead of `enabled: false`, and the strip matches the siblings already removed in the same function (`truncation`, `user`, `prompt_cache_retention`).

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @HouMinXi!
2026-09-16 01:38:55 -03:00
Dizzle
d4835c512c fix(sse): skip already-refused route per request on 429 (#13795)
On a 429 the opencode executor now records the refused proxy's key in the request-local tried-set, exactly like the 403/451, 5xx, stall and network arms already did — so a second account sharing that same proxy is not dialed and refused again before the loop reaches a genuinely different route (direct, or another proxy).

Reviewed against the tip that already carries your 38 merges from this evening: this is additive to the flags that landed today (`OPENCODE_RATE_LIMITED_429_EARLY_STOP`, `PROXY_SKIP_RECENTLY_FAILED`, `OPENCODE_USER_BLOCKED_ROTATION`, `OPENCODE_TRANSIENT_FAILOVER_BACKOFF`) and does not double-skip when combined with them; direct accounts have a null proxy key and correctly record nothing.

Validated as a combined board first (this PR merged with the 11 siblings of the same batch on the release tip): eslint on every changed file with the suppressions file, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, i18n new-key coverage, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 275 passing / 0 failing focused node:test cases across the 28 test files the batch touches. Then re-validated alone on the fresh tip before this merge: conflicts re-resolved, file sizes rebaselined for this PR's own growth, eslint and this PR's focused tests re-run.

Thanks @maxmad64bis!
2026-09-16 01:17:56 -03:00
Diego Rodrigues de Sa e Souza
cde49c9372 fix(i18n): retranslate the verbatim-English leaves in all 65 catalogs; ratio gate now blocking (#13782)
PR-4 of the locale-expansion plan. 215,363 strings retranslated across the 65 catalogs with the new `sync-ui-keys --retranslate-identical`; the share of leaves still identical to English drops from a mean of 18.3 % to 1.8 % (Spanish 56 → 2.1). No `__MISSING__` marker or missing key is left; zh glossary normalised; pinned product/flag names kept English and allowlisted. Baseline tightened and the CI step `i18n real-translation ratio` is blocking from here on.

⚠️ base-red inherited: #12732
2026-09-15 23:19:43 -03:00
Diego Rodrigues de Sa e Souza
8f55d85d22 fix(ci): clear the release/v3.8.51 base-reds left by the 09-15 batch (stryker, CLI i18n, paid-target fixture, call-log traceId, Jina prefix, callLogStats import, gitleaks) (#13747)
* fix(ci): clear the release/v3.8.51 base-reds left by the 09-15 batch — stryker coverage, CLI ready_timeout key, paid-target fixture, call-log traceId, Jina custom prefix

Every PR into release/v3.8.51 pushed after #13635/#13678 still failed Fast
Quality Gates and all four Unit fast-path shards on the same 16 tests. Each one
reproduces on the pure tip; none is a product defect:

- mutation-test-coverage: noauth-model-lockout and
  local-token-budget-429-skips-cooldown (#13606) were missing from
  stryker.conf.json tap.testFiles.
- cli-i18n-catalog: --ready-timeout calls t("serve.ready_timeout") with no
  catalog entry; added to en, zh-CN and zh-TW (the parity-checked locales).
- paid-model-target(-routes)-6540: #13407 removed Together's one-time credit
  from the free catalog, so "together/..." classifies as unknown and the
  save-time guard correctly lets it through. Fixture is now
  gemini/gemini-3.1-pro-preview, plus a precondition test on the fixtures.
- attempt-logging-early-keepalive-merge / video-bridge-log-redaction: #13546
  keys the call-log row on traceId; baseCtx now defaults traceId to
  pendingRequestId (same pattern as chatcore-attempt-logging). The keepalive
  test also moves to the 30s wall-clock poll deadline video-bridge uses.
- models-catalog-route: custom Jina rows keep the jina-ai/ prefix; #13403
  changed the custom assertion to jina/ (only synced rows use the alias).

Refs #12732

* fix(ci): clear the four reds the first r4 CI run surfaced — callLogStats duplicate import, Uzbek gitleaks false positive, redaction probe traceId, file-size

- src/lib/db/callLogStats.ts: the #13641 merge left ERROR_TYPE_CONTRACT
  imported twice (TS2300), failing API Route Typecheck and
  check:dashboard-typecheck on every PR.
- .gitleaks.toml: the Uzbek catalog from #13727 translates outputTokenDesc as
  "Yakunlash/javob tokenlari"; generic-api-key reads it as a token value.
- dashboard-request-failed-redaction-probe: reads the persisted row by
  traceId (#13546); with pendingRequestId it asserts null.
- models-catalog-route: drop the explanatory comment, which pushed the frozen
  file over its size cap; the rationale lives in the changelog fragment.

Refs #12732

* fix(ci): re-freeze the two test files #13748/#13749 grew past their file-size caps

PR-mode check:file-size relaxes source files against the base but not
testFrozen, so image-generation-handler.test.ts (2133->2235, #13748) and
batch_api.test.ts (1345->1348, #13749) failed Fast Quality Gates on every PR,
this one included. Caps set to the merged LOC, with the justification entry.

Refs #12732

* fix(ci): register free-badge-provider-gate (#13645) in stryker tap.testFiles

#13645 landed a covering test for src/sse/services/auth.ts without the
stryker entry, so the strict mutation-test-coverage gate went red again.

Refs #12732

* fix(ci): clear two more base-reds the #13440/#13439 merges added

- stryker.conf.json: register daily-reset-tz-threading (#13440), which covers
  accountFallback.ts and rrState.ts.
- .gitleaks.toml: allowlist the PROTECTED_PRIORITY_INFRA_502_ENABLED flag id
  (#13439); generic-api-key reads its key: as a token (secrets ratchet 0 -> 1).

Refs #12732

* docs(changelog): tidy the stryker base-red fragment wording

Refs #12732
2026-09-15 22:48:19 -03:00
Dizzle
997cd4d509 fix(sse): stop retry wave on rate-limited 429 and drain 429 once (#13657)
The opencode executor classifies rate-limited 429 bodies (`classify429`, with real tests) and, when a whole account wave is exhausted, returns the last real upstream 429 — status, body, `Retry-After` and quota headers intact — so the provider error rules (monthly-quota cooldown) keep working.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original stopped the cross-account wave at the first classified 429 and replaced the response with a synthetic one that dropped the body and headers; stopping early is now opt-in behind `OPENCODE_RATE_LIMITED_429_EARLY_STOP` (default off), the rate-limited account is still cooled down, the body is read as a bounded 8 KiB prefix from a clone and the original is never consumed, and the unused `status` input is gone.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 22:01:56 -03:00
Diego Rodrigues de Sa e Souza
c44f5da388 fix(i18n): drop the duplicated flag description key and the duplicated ERROR_TYPE_CONTRACT import left by the batch merges (#13816)
Merged with admin on local + CI evidence: `tests/unit/i18n-catalogs-no-duplicate-keys.test.ts` red on the tip (59 catalogs) → `pass 3 / fail 0` here; **API Route Typecheck passes on this PR** (it fails on every PR based on the current tip because of the duplicated `ERROR_TYPE_CONTRACT` import this removes); CodeQL, semgrep, Vitest fast-path, Docs gates, Change Classification pass. The remaining red checks (Fast Quality Gates, Merge integrity, Unit Tests fast-path 1/2/4) are the same inherited tip reds every PR on release/v3.8.51 shows right now — #13747 sweeps them. Both removed lines were byte-identical duplicates; nothing parsed or typed changes.
2026-09-15 21:35:03 -03:00
Dizzle
94d27e44fe fix(providers): stop parking Mistral connections on a bare 401 with no clear auth failure (#13609)
Behind the new `MISTRAL_AMBIGUOUS_401_SOFT_LOCKOUT` flag (default off), a bare Mistral 401 (`{"detail":"Unauthorized"}`, identical for a revoked key and an exhausted quota) gets a retryable cooldown instead of parking the connection as `expired`; after three soft strikes within an hour the next bare 401 parks it, so revocation still converges.

Maintainer rework before merge (kept the idea, no default behavior change):
- The predicate is shared with the connection-test module instead of duplicated; the squeezed 139-char line that dodged the file-size gate is formatted normally and the growth is rebaselined honestly with an annotation.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 21:19:47 -03:00
Dizzle
13f44af6e5 fix(sse): pause failover dispatch after repeated transient upstream failures (#13615)
Behind the new `OPENCODE_TRANSIENT_FAILOVER_BACKOFF` flag (default off), after two consecutive transient upstream failures the opencode rotation pauses before each later account (1.5s, 3s, 6s, capped at 10s per request) instead of hammering the upstream.

Maintainer rework before merge (kept the idea, no default behavior change):
- The pause honors the client abort signal (no dispatch after a disconnect), the failed attempt's body is cancelled before sleeping, `transientRetryDelayMs` now uses its arguments, and the sleep is injectable so the tests run without real timers.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 20:58:12 -03:00
Dizzle
e325888d78 fix(sse): fail over opencode request on refused-route 403 (#13498)
Behind the new `OPENCODE_USER_BLOCKED_ROTATION` flag (default off), a 403 or 451 carrying `user_blocked` on a proxied opencode account rotates at most once to the next account instead of being returned as-is.

Maintainer rework before merge (kept the idea, no default behavior change):
- 403 and 451 are handled by one predicate (the original returned 451 without rotation), the refused account gets a cooldown and joins the tried-set, and the response body of the attempt rotated away from is cancelled.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 20:39:39 -03:00
Dizzle
d8c0448293 fix(opencode): rotate once when a Responses stream stalls before its first byte (#13484)
Behind the new `OPENCODE_RESPONSES_STALL_ROTATION` flag (default off): a streamed Responses reply with no first body byte within `RESPONSES_FIRST_BYTE_TIMEOUT_MS` (15s) cools the account and rotates once; a second stall fails fast instead of waiting the 80s readiness timeout.

Maintainer rework before merge (kept the idea, no default behavior change):
- The TLS first-byte watchdog from #12656 is restored byte for byte (the PR had changed its pump, timer and cancel); the stall guard lives in its own module.
- Proxy-less multi-account setups now rotate the same way as proxied ones (the original threw for them), a client abort during the wait rethrows instead of rotating, and the env var is documented as flag-only.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 20:10:57 -03:00
Dizzle
5cf4316b67 fix(proxy-health): refused probe responses reset the consecutive-failure streak (#13608)
Behind the new `PROXY_HEALTH_BLOCKED_RESETS_STREAK` flag (default off), a probe the target refuses (401/403/429) resets the proxy's consecutive-failure streak, so a proxy that clearly relays is not marked dead by spaced-out real failures.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original reversed the deliberate #10654 policy for everyone; with the flag off a refusal stays neutral, and the existing assertions are restored. The stale JSDoc and the wrong "any relayed response resets" comment are fixed (5xx stays inconclusive).
- The source-grep test became a real sweep test: a local relay answering 403 drives fail → blocked → fail with auto-disable, in both flag modes.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 20:01:25 -03:00
Dizzle
ca312d57aa feat(proxies): show how many egress IPs actually served a proxy pool (#13581)
Behind the new `PROXY_POOL_EGRESS_OBSERVATION` flag (default off): a line under each proxy pool showing how many distinct egress IPs actually served it over 24h, backed by `GET /api/settings/proxies/pool/egress-observation`.

Maintainer rework before merge (kept the idea, no default behavior change):
- The route validates its query with Zod (unknown `scope` → 400 instead of silently `global`), error bodies go through `errorResponse()`, the OpenAPI entry documents security, parameters and responses, and the three UI strings exist in every locale.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 19:48:52 -03:00
Dizzle
6922332959 feat(proxies): stop re-serving a pool member the provider just refused (#13602)
Behind `PROXY_SKIP_RECENTLY_FAILED` (from #13578): a provider 429 received through a pool member sets that member aside and a 2xx clears it, for opencode providers.

Maintainer rework before merge (kept the idea, no default behavior change):
- `noteProxyOutcome` ran inside the fire-and-forget `safeLogEvents` after awaited dynamic imports, so a concurrent request could still pick the member; it now runs first, synchronously, before any `await`.
- The duplicate `177_proxy_logs_upstream_status.sql` the stack still carried alongside the renamed 179 was removed; the regression test the PR body named exists as `pool-ip-quota-429-path.test.ts`.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 18:45:54 -03:00
Dizzle
238cb1b076 feat(proxy-logs): keep the HTTP status the provider actually returned (#13580)
`proxy_logs` records `upstream_status`, the HTTP status the provider actually returned through the proxy, instead of only success/timeout/error.

Maintainer rework before merge (kept the idea, no default behavior change):
- The migration collided with the tip (177 was already taken): renumbered to `179_proxy_logs_upstream_status.sql`, the runner's already-applied check moved to `case "179"` (the old `"177"` would have skipped the tip's own 177), migration count bumped to 176 in README, AGENTS.md, llm.txt and its mirrors (operator-approved).
- A new test runs the real migration runner on the real SQL files and fails with the old case number.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 18:26:15 -03:00
Dizzle
29d66cbf8c feat(proxies): stop re-serving a proxy that just failed (#13578)
Behind the new `PROXY_SKIP_RECENTLY_FAILED` flag (default off), pool rotation and the opencode account rotation remember a proxy that just failed (refused probe or 429) and skip it for a doubling cooldown instead of re-serving it immediately.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original was on by default and re-queried the DB on every request while a member was set aside; selection now caches a refusal sequence number and re-runs the cascade once per set-aside event.
- `src/lib/db` no longer imports the heavy dispatcher for key normalization (a parity test guarantees the same key as `proxyConfigToUrl()`); `.env.example` and `ENVIRONMENT.md` document the default as false.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 18:09:02 -03:00
Diego Rodrigues de Sa e Souza
df87e9363b fix(auth): close the JWT_SECRET bootstrap chain — real-peer loopback, obsidian always-protected, DATA_DIR vault refusal (#13791)
GHSA-7pq4-8pvv-rx7r (critical). Every link of the reported chain held on the
release tip:

1. First boot without JWT_SECRET generates one and writes it in cleartext to
   $DATA_DIR/server.env.
2. With no password configured, isAuthRequired() returned false for
   POST /api/settings/require-login unconditionally — before the loopback
   check — so any network peer could switch requireLogin off.
3. With requireLogin off, POST /api/settings/obsidian/webdav accepted an
   arbitrary vault root and echoed freshly minted Basic credentials.
4. The WebDAV file service is served by the custom Node layer before Next.js,
   outside the authz pipeline.
5. Pointing it at DATA_DIR reads server.env, and JWT_SECRET forges an
   `{"authenticated":true}` admin session.

A second, worse problem surfaced while verifying: isLoopbackRequest() decided
"loopback" from nextUrl.hostname / the Host header, which the client controls.
`Host: localhost` from a remote address made the whole fresh-install bootstrap
reachable, not just the write path.

Three cuts, plus the root cause:

- isLoopbackRequest() now reads the trusted peer: the token-stamped real TCP
  peer the custom server writes (peerStamp), then the pipeline's own locality
  verdict once a stamp token exists, then a real socket peer. The bootstrap
  write path honours the same constraint instead of returning false, and
  managementPolicy hands down the peerContext verdict explicitly, because at
  policy time the original request still carries client-supplied headers.
- Host is consulted only when the process has no stamp token at all — no
  stamping server in front, which in practice means route handlers invoked
  directly by the unit-test harness. Every supported runtime (run-next dev and
  start, standalone-server-ws for Docker, the npm CLI and Electron) calls
  ensurePeerStampToken() at boot, so there a signal-less request fails closed.
  Without this fallback ~340 route tests that call handlers with
  `new Request("http://localhost/…")` turned into 401s.
- /api/settings/obsidian joins ALWAYS_PROTECTED_API_PATHS: issuing and rotating
  reusable WebDAV credentials is credential export, the same rationale as the
  GHSA-62vw entry for the password reveal.
- enableObsidianVaultSync() refuses a vault that is, sits inside, or contains
  DATA_DIR, comparing realpath-resolved paths so a symlink cannot dodge it.

Tests are red-first: remote stamped peer → auth required on the bootstrap
write; Host: localhost plus a forged locality header from a non-loopback
stamped peer → 401 through the full pipeline; the local operator keeps the
first-password flow; obsidian inventory and DATA_DIR overlap cases.
2026-09-15 16:58:24 -03:00
Dizzle
87d9d82b37 fix(sse): fail over to sibling connection on stream early EOF (#13153)
Behind `STREAM_EARLY_EOF_SIBLING_FAILOVER_ENABLED` (default off): after the bounded same-connection retry is spent, a stream that closed early fails over exactly once to a sibling connection.

Maintainer rework before merge (kept the idea, no default behavior change):
- The PR's own failover test was red on its head: the `/v1/chat/completions` route's early-stream keepalive dropped the `X-OmniRoute-Selected-Connection-Id` header on the first cold request. Tests now drive `handleChat()` directly; the assertion was kept.
- "One hop" was one hop per connection (a 3-connection pool made 4 dispatches); it is now a single sibling hop per request, and when the pool runs out the original `STREAM_EARLY_EOF` 502 is returned instead of a generic `bad_gateway`, so combo-level detection keeps working. The source-regex timeout test became a behavioral one; flag description in all 59 locales.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 16:49:06 -03:00
Dizzle
08fe9e5117 fix(usage): skip billing for estimated token usage, bill real output only on partial estimates (#13686)
Estimated token usage is now visible to operators: usage a provider marks as `estimated` carries an internal marker through extraction and the call log records `_omniroute.usageEstimated: true` on the logged response.

Maintainer rework before merge (kept the idea, no default behavior change):
- Billing is unchanged: the original skipped cost/budget/quota-share for estimated usage, which would have let streams without upstream usage and eight web executors spend $0 against API-key budgets; that part is reverted and no opt-in flag was added because it cannot be made budget-safe.
- Both open-sse TS2345 errors, the client-visible `estimated_prompt_tokens` field and the `as unknown as` casts are gone; four real `handleChatCore` cases assert the marker and unchanged spend.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 16:22:51 -03:00
Dizzle
931c9f9b9d fix(stream-recovery): log recovery traces with continuation attempt (#13650)
Recovery traces for mid-stream continuation: one `onContinueOutcome` hook reports suffix stitched, overlap rejected, terminal, empty, no-stream and refused (with reason), logged through `chatCore` at debug level; warn is reserved for the cases where recovery gives up.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original logged a warn-level latch line on every streamed tool call; nominal and tool-call streams are now silent, and the existing `mid-stream continuation attempt N/4` line keeps its format.
- `chatCore.ts` ends 3 lines shorter than the tip, so the baseline bump the PR carried was removed; the wiring is tested through a real `handleChatCore` continuation.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 16:19:10 -03:00
Dizzle
53ed8c4745 fix(stream-recovery): order-aware in-flight tool-call detection behind off-by-default flag (#13633)
Behind `STREAM_RECOVERY_TOOLCALL_ORDER_FIX` (default off), mid-stream continuation becomes tool-call safe: any tool call seen in the stream — in flight or finished — blocks a continuation, and an empty continuation stops after one attempt.

Maintainer rework before merge (kept the idea, no default behavior change):
- The empty-continuation short-circuit also ran with the flag off; it is now gated, so the flag-off path uses the whole budget exactly as before (regression test added).
- The latch re-arm that let a continuation fire after a completed `finish_reason: tool_calls` is gone; index-less tool calls on multi-choice payloads are now blocked too; ~150 lines of dead trace plumbing removed.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 16:10:52 -03:00
Dizzle
defa0f07b2 fix(routing): await stale provider-pin clears and gate swallowed errors plus fire-and-forget async (#13614)
Stale provider-pin (`clearStaleLKGP`) clears are no longer silent: the fire-and-forget promise carries a `.catch` that warns with combo, comboId and executionKey, and a `check:routing-error-guard` npm script keeps the inventory of swallowed catches in the routing hot path from growing.

Maintainer rework before merge (kept the idea, no default behavior change):
- The awaited DB writes in the fallback loop were reverted (they added latency and SQLite lock exposure on every skip); the clear stays non-blocking.
- The guard keys its allowlist by file + normalized catch body instead of line numbers (the PR's version broke on any edit) and is wired as an npm script only, not in CI; the unused stats counters were dropped.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:52:33 -03:00
Dizzle
611342609f fix(combo): return 502 for non-quota protected-priority stops (#13439)
Behind the new `PROTECTED_PRIORITY_INFRA_502_ENABLED` flag (default off), protected-priority combo stops caused by provably non-quota infrastructure (provider circuit open, predictive-TTFT latency) surface as 502 instead of a quota-looking 503.

Maintainer rework before merge (kept the idea, no default behavior change):
- The original branch made 502 the default for every stop, including model lockouts and cooldowns, and removed the #8133/#1731 provider-wide skip for 401/5xx without a connection id; both are restored with their regression tests untouched.
- Nineteen cases cover eight gate causes plus predictive latency, flag off and on.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:46:57 -03:00
Dizzle
ac52d4d9ea fix(sse): omit synthetic Retry-After and mark retry provenance on drain path (#13672)
Behind the new `RETRY_AFTER_PROVENANCE_ENABLED` flag (default off): `unavailableResponse` omits the synthetic `Retry-After: 1` when there is no real retry signal, marks `retry_after_provenance` on its bodies, and both combo drain readers parse prose retry hints from plain-text bodies too. With the flag off, headers and bodies are exactly as before.

Maintainer rework before merge (kept the idea, no default behavior change):
- A past `Retry-After` date is no longer labelled as an upstream signal with `Retry-After: 1`; non-JSON bodies (HTML 502 pages) log at debug instead of warning on every request.
- The provenance claim is narrowed to responses built by `unavailableResponse`, documented in the flag row.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:25:43 -03:00
Dizzle
cad0fcc65d fix(sse): bound daily quota cooldowns around daylight-saving transitions (#13671)
Fixes the DST-gap bug in `nextDailyResetAtMs`: a reset hour that does not exist on the transition day landed one hour early (New York 02:00 came out as 01:00; Havana/Santiago midnight as 23:00 the day before). The walk across the gap is bounded to one day and uses a cached formatter.

Maintainer rework before merge (kept the idea, no default behavior change):
- Dropped the 24h clamp in `getMsUntilTomorrow` (on a 25h fall-back day 24.5h is the correct wait; clamping expired the lock 30 minutes early) and the unreachable `ms <= 0` branch, with their tests; characterization tests pin ordinary and fall-back days.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:07:42 -03:00
Dizzle
520428c8ad fix(resilience): honor user daily-reset clock for non-TPD quota cooldowns (#13440)
Daily-quota lockouts on the non-TPD path honor the provider's configured daily-reset clock (`dailyQuotaResetTimezone`/hour) in combo routing instead of the host's midnight.

Maintainer rework before merge (kept the idea, no default behavior change):
- The process-lifetime clock cache is gone: the clock is resolved on each failure through the already-TTL'd `getCachedProviderNodes`, so a timezone change takes effect without a restart and a DB error is never cached as `{}` forever.
- Round-robin combos are threaded too (the PR left them out); an option on `recordModelLockoutFailure` that could never run was removed; tests prove both call sites pass the configured clock.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 15:02:55 -03:00
Dizzle
b283ed1830 fix(api): use shared SOCKS5 flag reader in settings routes (#13646)
Both settings routes use the shared `isSocks5ProxyEnabled()` reader instead of a copied check (identical logic, no behavior change).

Maintainer rework before merge (kept the idea, no default behavior change):
- The source-grep tests were replaced by behavioral tests of both routes across the flag on/off matrix (`GET /api/settings/proxies` reports `socks5Enabled`; `PUT /api/settings/proxy` accepts socks5 or returns 400).

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 14:10:45 -03:00
Dizzle
45c54ca8aa fix(dashboard): require provider free-tier for the Free badge (#13645)
The provider-page Free badge can require a free tier the provider actually honors: behind the new `FREE_BADGE_REQUIRES_PROVIDER_FREE_TIER` flag (default off) the display-name "free" heuristic, non-boolean `free` fields and `:free` suffixes on registered providers without a documented free tier no longer light the badge. With the flag off the historical rule is unchanged.

Maintainer rework before merge (kept the idea, no default behavior change):
- `:free` models on free-tier providers and on compatible nodes (OpenRouter-style endpoints) keep the badge in both modes — the original change dropped them.
- Test D derives its provider set from `FREE_MODEL_BUDGETS` instead of a hard-coded allowlist; a vitest render covers both sections with the flag endpoint on, off and erroring.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 13:57:39 -03:00
Diego Rodrigues de Sa e Souza
c8b24ffc30 fix(authz): gate the cli-tools status and skills execution routes to LOCAL_ONLY (#13745)
GHSA-35fw-cv32-2373 and GHSA-jx89-f37j-pq89 — the same defect class as
/api/acp/agents (GHSA-hf57): a route whose handler chain spawns a host process
was classified Tier 3 MANAGEMENT only, and requireManagementAuth() waives auth
when requireLogin=false. Hard Rules #15/#17 require the LOCAL_ONLY gate, which
runs on the stamped real peer before any auth check.

cli-tools (GHSA-35fw): 14 routes reach
getCliRuntimeStatus() -> locateCommand() -> runProcess("sh", ["-c",
'command -v -- "$1"']) -> spawn(), exactly like their six gated siblings
(forge/grok-build/jcode/qwen/omp/letta-settings):
all-statuses, status, and the claude/cline/codewhale/codex/crush/deepseek-tui/
droid/kilo/openclaw/pi/smelt-settings routes. The advisory counted 13; it
missed /api/cli-tools/detect, which is heavier — detectAllTools() runs
execFile(binary, ["--version"]) and execFile("which") per tool.

skills (GHSA-jx89): POST /api/skills/install stores the request's handlerCode
verbatim as the skill handler with no allowlist, so a value equal to a built-in
name (execute_command / eval_code) aliases the real sandboxed built-in;
POST /api/skills/executions then runs it. The sandbox is a real container, but
the spawn is transitive, which is why the 6A.8 source scan never flagged it.

Entries are exact paths, not a /api/cli-tools/ blanket prefix: apply, backups,
config, guide-settings, hermes-agent-settings, keys, logs, openclaw/auto-order
and codex-profiles do not spawn and remote dashboards use them. All 16 are
mirrored into SPAWN_CAPABLE_PREFIXES (no manage-scope bypass) and added to the
route-guard-membership roots so the gate enforces them from now on.

Functional trade-off, same one already accepted for grok/forge/jcode/qwen: a
dashboard served through a tunnel no longer shows the CLI Tools status badges.

Tests are red-first. Two existing negative controls pointed at routes that turn
out to spawn (/api/cli-tools/all-statuses, /api/skills/install); they now point
at routes that genuinely do not (/api/cli-tools/config, /api/skills/marketplace,
/api/skills/skillssh/install), so the non-over-gating assertions are kept.
2026-09-15 13:25:35 -03:00
Diego Rodrigues de Sa e Souza
e7f9fec251 fix(api): enforce API-key ownership on files and batches — null-owner records and anonymous listing (#13749)
GHSA-2jm2-mpx8-6523 and GHSA-m3hp-hq9g-fpmv, one root cause.

`getApiKeyRequestScope()` never rejects: with the default REQUIRE_API_KEY=false
the client-api policy admits both a missing and an invalid bearer as anonymous,
and the scope comes back `{ apiKeyId: null, isSessionAuth: false }`. The
`/v1/files` and `/v1/batches` routes then treated "null" as permissive in two
different ways:

- GHSA-m3hp — the list routes coerced `apiKeyId || undefined`, and the DB layer
  reads `undefined` as "no owner filter", so an anonymous or invalid-bearer
  caller got every tenant's file and batch metadata, the same unfiltered view as
  the operator's dashboard.
- GHSA-2jm2 — the single-record checks were `record.apiKeyId !== null && …`, so
  a record with no owner short-circuited to "allowed" for any caller: read,
  download raw content, delete, cancel, or use as a batch input. Null-owner
  records are common — every dashboard-session upload, and every batch output
  file inheriting a session batch's owner, which carries model responses.

`api_key_id` has existed since the table was created (migration 028), so a null
owner is not a legacy row; it is an unattributable write. No doc described it
as shared — API_REFERENCE says files are scoped per key — and batch_api.test.ts
pinned the by-id exposure as expected behaviour.

One rule now, in `_helpers/apiKeyScope.ts`:

- `canAccessOwnedRecord(scope, owner)`: a dashboard session is the instance
  operator and may act on any record; an API key acts on its own records only;
  a null owner is denied to every non-session caller. Applied to files
  GET / DELETE / content, batches GET / DELETE / cancel, and the batch-create
  input-file check.
- `resolveListScope(scope)`: an explicit union for list/count reads — scoped to
  the presented key (a key wins even alongside a session cookie), instance-wide
  only for a session without a key, and 401 otherwise, including for a bearer
  that does not resolve to a key. There is no default that widens a read.

This follows the GHSA-wvxc shape already used by the delete-completed sweep.

Behaviour change: the anonymous upload → batch → download flow no longer works
without an API key, because a null owner cannot be attributed.

Subsumes #13683: it moved `scopeCheck` into the shared helper so a session can
cancel any batch — kept, and its test ported — but it also kept null-owner
records open on the premise they predate ownership tracking, which migration
028 contradicts.

Tests are red-first. batch_api's by-id case is flipped to 404 with a negative
assertion; batch-deletion-route-logic now imports the real helper instead of a
local copy that had silently diverged from production; the two integration
tests present a real key, since their subject is limits and rate logging, not
auth.

Co-authored-by: Markus Hartung <mail@hartmark.se>
2026-09-15 13:25:13 -03:00
Diego Rodrigues de Sa e Souza
b97338a803 fix(security): pin the public-only guard on client-supplied image URLs (#13748)
GHSA-34rg-3pqj-35g9. `fetchRemoteImage()` defaults to
`getProviderOutboundGuard()` — the OPERATOR outbound policy, local-first by
design so self-hosted providers on loopback/LAN keep working. Since #11062 added
the `block-metadata` middle tier, a default install resolves to that mode: the
string check only rejects 169.254/16 and the IMDS hostnames, and the DNS
validation step is skipped entirely (it only runs under `public-only`).

Three sinks feed that default with CALLER input, so a request body could make
the server fetch `http://127.0.0.1:…` or any RFC-1918 host and forward the bytes
upstream:

- imageGeneration.ts `resolveImageSource()` — `image_url`, `mask_url`, message parts
- imageUpscale/shared.ts `resolveUpscaleImageSource()` — 14 body aliases,
  `provider_options.*`, message parts (Stability, Topaz)
- visionBridgeHelpers.ts `fetchRemoteImageAsDataUri()` — chat `image_url` parts
  inlined into the vision self-call

plus the NanoBanana result-URL download, which is upstream-supplied rather than
OmniRoute-controlled.

Same trust confusion as GHSA-3f8g / GHSA-j7j4 on the search base URL: operator
config and caller input must not share a guard. Each site now passes
`guard: "public-only"` (string check + DNS validation of every answer), matching
the siblings that already did it right — embeddings, the audio bridge and the
AI Horde result download.

`pinDns` is set only on the vision bridge. The other three sites use
`globalThis.fetch`, and connection pinning would swap that for a raw undici
fetch — the same reason the AI Horde site leaves it off. On the vision bridge a
`fetchImpl` is injected, so `pinDns` there validates every DNS answer but cannot
pin the connection; commented in place.

Blind SSRF rather than full read: the bytes go upstream or into the vision
self-call, not back to the caller — but the status oracle and upstream
exfiltration are real.

Tests are red-first — per sink, `http://127.0.0.1:1/x.png` and
`http://192.168.1.50/x.png` are rejected with the injected fetch never called,
and a public host whose DNS resolves to a public IP still downloads.
2026-09-15 13:24:50 -03:00
Diego Rodrigues de Sa e Souza
e498c349e3 fix(security): add Groq, xAI and OpenAI-compatible key shapes to the credential catalog (#13744)
GHSA-r4q7-7f24-m29p. `CREDENTIAL_PATTERNS` (open-sse/utils/credentialPatterns.ts)
is the single catalog iterated in order by both the opt-in credential-masker
guardrail and the public error sanitizer. It had no entry for Groq (`gsk_`) or
xAI (`xai-`), and only knew the exact 48-char OpenAI `sk-` form.

Measured on the release tip before this change:

| Shape                        | public sanitizer | guardrail |
|------------------------------|------------------|-----------|
| Groq  gsk_ + 52              | LEAK             | LEAK      |
| xAI   xai- + 80              | LEAK             | LEAK      |
| DeepSeek sk- + 32 hex        | redacted         | LEAK      |
| sk- + 20/36/40/51 (not 48)   | redacted         | LEAK      |

The public path already caught every `sk-` shape through STRONG_CREDENTIAL_TOKEN,
so the advisory's "both layers" framing only holds for gsk_/xai-; for the sk-
family the exposure was the guardrail.

Adds `groq` and `xai` after `anthropic_alt`, and a generic `openai_compatible`
`sk-` fallback as the LAST entry. Ordering matters: both consumers replace as
they iterate, so `openai_proj`, `openai` and `anthropic*` stamp their specific
label first and the fallback only sees shapes nothing else claimed. The
lookbehind mirrors STRONG_CREDENTIAL_TOKEN so `risk-…`-style words do not match.
All three regexes are a fixed prefix plus one bounded character class — linear,
no nested quantifiers.

Tests are red-first: the new guardrail cases (bare / sentence / JSON-body
contexts per shape, plus label-ordering and negative cases) and the catalog
coverage array in error-sensitive-redaction both failed on the tip.

Follow-ups deliberately left out of scope: `tskey-auth-` (Tailscale) was never in
the catalog, and the guardrail does not decode `\uXXXX` escapes the way the
public path does.
2026-09-15 13:24:30 -03:00
Diego Rodrigues de Sa e Souza
104a34c5f2 chore(deps): bump the adm-zip override to ^0.6.1 (#13737)
Dependabot #214 (GHSA-vwc7-r8mq-g2x9 / CVE-2026-76845, moderate): adm-zip
0.5.9–0.6.0 follows a symlink that already exists inside the extraction root
and writes through it, outside the root. The advisory still reports
`first_patched_version: null`, but 0.6.1 (published after the advisory) is the
fix — `util/utils.js` gains `assertPathSafe`, which walks every path component
below the root with `lstat` and throws on a symlink; `extractAllTo` calls it
before every write. Verified by diffing the two tarballs.

Reach in this repo: adm-zip is pulled only by `onnxruntime-node` (an
optionalDependency, itself pinned by override) and used only in its install
script to unpack the vendor's own runtime binary. No request path touches it.

The override already existed at ^0.6.0 (PR #7732, the previous adm-zip CVE);
this just raises the floor. Lockfile moves 0.6.0 → 0.6.1, nothing else.
2026-09-15 13:24:09 -03:00
Dizzle
62cd27720a fix(db): persist WAL busy counter across restarts (#13218)
The WAL busy counter survives restarts: it is persisted in `key_value` and restored at boot, so the health output no longer resets to zero after every restart.

Maintainer rework before merge (kept the idea, no default behavior change):
- `recordBusy()` no longer writes synchronously on the contended path (with `busy_timeout = 2000` that could block the event loop for up to 2s); it accumulates in memory and `flushBusyTotal()` upserts the delta on a clean passive/TRUNCATE tick or best-effort at stop.
- The boot wiring is tested for real: a child Node process drives `startWalMaintenance` against a real SQLite file (restore at boot, zero writes while busy, one flush at stop, restore after restart).

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 13:21:52 -03:00
Dizzle
7cabac4985 fix(quota): bound routing caches with shared boundedMap factory (#13280)
Seven routing/quota caches (quality states, account buckets, quota-fetcher/saturation/header caches, learned rate limits) sit behind a shared bounded map with LRU/TTL eviction instead of growing without bound. The learned-limits cap of 200 that the tip declared was never enforced.

Maintainer rework before merge (kept the idea, no default behavior change):
- Eviction logging goes through the project logger, aggregated (first eviction, then one summary line per minute per map) instead of a `console.warn` per eviction.
- `refetch-lazy` and `hard-expire` behaved identically and are collapsed into `ttl`; protected entries (saturated account buckets, evaluator quality scores) are never evicted; caps raised to 2048–4096 so normal deployments never evict, with tests showing 300 learned limits and 600 cached entries all kept.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 13:08:21 -03:00
Dizzle
215ac43a70 fix(proxy): keep proxy credentials holding a literal percent (#13605)
Proxy credentials containing a literal `%` no longer throw `URIError`: every `decodeURIComponent` on proxy user/password is guarded.

Maintainer rework before merge (kept the idea, no default behavior change):
- HTTP proxies still failed because undici's `ProxyAgent` decodes the credentials itself; the dispatcher now builds undici's `Basic` token with the safe decoder and passes it as `token`, so a literal `%` works there too.
- The three remaining unguarded sites (`mappers.ts`, `proxySubscription/parse.ts`, `subscriptionService.ts`) are guarded; tests run the real `createProxyDispatcher` against a local HTTP CONNECT proxy and a local SOCKS5 server that record what they received.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 12:59:27 -03:00
Dizzle
1d17c239d1 fix(build): stop the client bundle from reaching server-only modules, and make the guard find them (#13436)
Fixes the production build break from `node:fs` reaching client bundles (`oauth.ts → cursorAgentCliVersion.ts` through the codebuddy-cn registry) and widens the client-bundle guard so it finds any Node builtin, not just the one that broke.

Maintainer rework before merge (kept the idea, no default behavior change):
- The guard was 11× slower (3.8s → ~40s) because resolved edges were not cached; with resolved edges and per-file verdicts cached it runs in ~4.6s.
- Bare builtins that Next's client build polyfills (`path`, `os`, `crypto`, `buffer`, … from Next's own `resolve.fallback` list) are allowed consistently; `node:` imports are always flagged; a drift test fails if Next stops polyfilling an allowlisted name.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 12:52:41 -03:00
Dizzle
cb420db64b fix(call-logs): hide search rows without a live provider (#13641)
Search stats and recent searches stop surfacing ghost rows: NULL and `-` providers are always hidden, and, behind the new `SEARCH_STATS_HIDE_DELETED_CONNECTIONS` flag (default off), traffic of a keyed provider whose connection was deleted is hidden too. Totals use the same guard as the per-provider rows, so they always agree.

Maintainer rework before merge (kept the idea, no default behavior change):
- Keyless providers from the search registry (`duckduckgo-free`, `searxng-search`, anonymous `context7`) and providers served through a credential fallback (`perplexity-search` on a `perplexity` key) stay visible in both modes — the original filter dropped them because they have no `provider_connections` row.
- Tests use real registry ids and cover flag off (historical stats) and flag on, including the analytics route; #13281's changelog fragment restored.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 12:48:06 -03:00
Diego Rodrigues de Sa e Souza
831b485e08 test(batches): rename the two seeded-batch labels that gitleaks reported as secrets (#13729)
The labels wvxc-route-401/wvxc-route-500 sat right after a key*.id argument and cleared the gitleaks generic-api-key length and entropy floors; renamed to route401/route500 with a docblock stating the measured rule. Test-only; .gitleaks.toml untouched. Reviewed by 3 rounds of /omni-code-review (37 agents).
2026-09-15 12:28:29 -03:00
Dizzle
516927196c fix(call-logs): zod write-point guard for error_type (#13441)
A write-boundary guard for `error_type`: `toStoredErrorType()` validates what `saveCallLog` stores against the vocabulary (Zod enum built once), as defense in depth on top of #13281.

Maintainer rework before merge (kept the idea, no default behavior change):
- Dropped the redundant `SCHEMA_SQL` column (migration 158 already creates it) and the string-absence "migration 177" test; the real `PRAGMA table_info` test is back.
- Restored #13281's changelog fragment, which this branch had deleted, and renamed this PR's own fragment to `13441-error-type-write-guard.md`.
- The guard is now exercised for real: the exported function is tested with out-of-vocabulary values and an end-to-end drift test that changes a classifier family at runtime.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 12:20:19 -03:00
Dizzle
3266d163f4 fix(call-logs): versioned error_type vocabulary with unknown fail-open (#13281)
`error_type` becomes a versioned vocabulary: a failure the classifier cannot place is stored as `unknown` instead of NULL, and any stored value outside the vocabulary reads back as `unclassified` in the breakdown.

Maintainer rework before merge (kept the idea, no default behavior change):
- `PROVIDER_ERROR_TYPES` is `as const`, so `ErrorTypeContract` is a real union and the classifier functions return the narrowed type.
- The constants moved above the JSDoc that documents `getErrorTypeBreakdown`; the vocabulary SQL is built on first use so an import cycle cannot read it before it exists.
- Legacy NULL rows keep landing in the `pre_migration`/`unclassified` bucket without vanishing or double counting, and the log export / BigQuery row pass both NULL and `"unknown"` through unchanged — both covered by tests. Duplicate assertions removed; every seeded row is cleaned up in `finally`.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:26:38 -03:00
Dizzle
5a7a3d0121 fix(combos): abort combo test probes when the client disconnects (#13279)
Combo test probes are aborted when the dashboard client disconnects (`AbortSignal.any` over the route's own timeout and `request.signal`), instead of running to completion for nobody.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:24:35 -03:00
Dizzle
4c0e45d814 fix(models): return 503 with Retry-After when a cold catalog build exceeds its time bound (#13438)
A cold `/v1/models` catalog build that exceeds its time bound now answers 503 with `Retry-After` instead of a 500, and the timed-out build stays joinable so the next retry does not start another cold build. Seven cases; the first fails on the tip (500 → 503).

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:21:58 -03:00
Dizzle
c0e5b0a833 fix(connection-cooldown): skip cooldown for locally rejected token-budget 429s (#13606)
`token_limit_exceeded` joins `REQUEST_SCOPED_UPSTREAM_ERROR_CODES`: chatCore's local Tier-2 check answers 429 with that code, but `shouldSkipConnDisable` did not know it and cooled a healthy connection down for a request-sized problem. Combo exhaustion treats it as request-scoped too.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:18:10 -03:00
Dizzle
5395618aac fix(opencode-plugin-v2): write catalog snapshot atomically via temp file and rename (#13607)
The plugin v2 on-disk catalog snapshot is written to a temp file and renamed into place, snapshots from a newer format version are refused instead of parsed, and size-cap or I/O give-ups now warn instead of failing silently. Nine cases, four of which fail on the old code.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:15:24 -03:00
Dizzle
ca9254ea65 fix(opencode): read management token from environment with startup fallback warning (#13613)
The opencode plugin v2 reads its management token from `OMNIROUTE_MANAGEMENT_API_KEY` (the plugin option still wins) and warns once at startup when it has to fall back to the inference key. Eight cases through the real plugin setup, env isolated, asserting the Bearer header on `/api/*`.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:12:03 -03:00
Dizzle
5cc3362ef0 fix(providers): route Muse Spark 1.3 to the Responses API on OpenCode Zen, OpenCode and OpenCode Go (#13471)
Registers Muse Spark 1.3 (with its effort aliases) on OpenCode Zen, OpenCode and OpenCode Go with `targetFormat: openai-responses` and a 1M context window, so the model no longer falls back to `/chat/completions` (#12674, #12698). Superset of #12675, #12973 and #13111, whose authors are credited in the PR.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:08:16 -03:00
Dizzle
aaf0777a98 fix(proxies): keep the stored status when a write does not send one (#13577)
A write that does not send a proxy `status` (subscription refresh, bulk import, PATCH) no longer turns a disabled or dead proxy back on; the stored status is kept. Real DB-backed tests through `upsertProxy`, the subscription sync against a local feed and `handleProxyUpdate`.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 11:05:01 -03:00
Dizzle
3af620e68f fix(combos): stop dropping live keys and persisting dead ones (#13217)
The combo PUT route and the dashboard modal stop stripping nine live config keys (`queueDepth`, `maxComboDepth`, `fallbackDelayMs`, `handoffProviders`, `manifestRouting`, `complexityAwareRouting`, `pipeline_enabled`, `shadowRouting`, `evalRouting`, plus `queueTimeoutMs` in the modal); only the three dead keys that nothing reads are removed. Route tests assert every live key survives a PUT.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 10:59:34 -03:00
Diego Rodrigues de Sa e Souza
225da11fc6 ci(radar-export): publish on release-branch catalog pushes; daily schedule (#13736)
The radar-server only consumes the stable asset, so a catalog merge on the active release
branch waited for the Monday 06:17 UTC cron and the feed sat up to a week behind the README
(2026-09-14: the Together signup-credit row removed in d6e62ae only left the feed after a
manual dispatch). Trigger on release/** pushes for the same catalog paths and move the
schedule to 03:17 UTC daily, ahead of the server's 04:23 UTC publish cycle.
2026-09-15 10:20:55 -03:00
Dizzle
ee3fbaf3f6 fix(proxies): preserve inactive and dead statuses during pool validation (#13612)
Pool validation no longer rewrites `inactive` or `dead` proxies to `active`: `validateProxyPool` only ever touched rows that were already live. Covered by 8 status × probe combinations plus case and null variants.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 10:11:44 -03:00
Dizzle
1d03ba0ed2 fix(api): keep omitted fields on partial updates (#13582)
Partial updates keep the fields the client left out. Update/PATCH schemas that carried `.default()` re-applied those defaults under `.partial()`, so renaming a disabled reasoning-routing rule turned it back on, a playground preset lost its params and a proxy edit reset `family` to `auto`. The new `partialWithoutDefaults` helper strips defaults before `.partial()`; a scan test fails if any exported update schema ever leaks a default again.

Validated first on the combined board of all 38 PRs of this batch (10 merged as-is, 28 after the maintainer rework) on top of release/v3.8.51 c0f92ec: typecheck:core, check:open-sse-typecheck and check:dashboard-typecheck clean; ESLint clean on every changed file; file-size (rebaselined for the combined growth), complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync, migration-numbering and i18n new-key gates green; 735 focused node:test cases with the only batch-caused failure (a flag-count assertion) fixed. Then re-validated alone on the fresh release tip right before this merge: ESLint on the changed files, typecheck:core, check:open-sse-typecheck, the file-size/complexity/changelog gates and this PR's own tests.

Thanks @maxmad64bis!
2026-09-15 10:04:14 -03:00
Diego Rodrigues de Sa e Souza
58f88a83e4 feat(i18n): 7 new locales — Hausa, Yoruba, Igbo, Amharic, Uzbek, Georgian, Armenian (66 locales) (#13727)
Batch 3 (last) of the locale-expansion plan: ha, yo, ig, am, uz, ka, hy on every surface — dashboard catalog, docs mirror (22-file core + llm.txt + CHANGELOG), CLI catalog, README flag block, locale tables and 🌐 language bars. Also closes the key gap the batch-1 (43 keys) and batch-2 (10 keys) catalogs carried since their base merges, fixes the Igbo "Model" copy and allowlists the Uzbek cognate. Translation-ratio baseline covers 65 locales.

⚠️ base-red inherited: #12732
2026-09-15 09:50:01 -03:00
Diego Rodrigues de Sa e Souza
d36251cf1c fix(ci): clear five uncovered base-reds on release/v3.8.51 — auto-combo catalog event-loop pin, unweighted quota-share, resilience key set, pack-gate stamp, synced-catalog env doc (#13678)
* fix(combo): rotate unweighted quota-share targets instead of pinning the first

The combo resolver turns an unset step weight into 0 (comboStructure.ts), and
#10881 made normalizeWeight treat 0 as disabled plus return definition order when
the total weight is 0. A quota-share combo without explicit weights therefore had
no DRR quanta and dispatched every request to its first target — the
combo-matrix/quota-share integration suite saw openai six times out of six.

An all-zero set is now an unweighted combo and shares evenly; an explicit 0 still
disables a target when its siblings are weighted.

Refs #12732

* fix(models): resolve auto-combo target metadata once per catalog build

#12046 derives vision/modalities for the built-in auto/* combos by resolving
catalog metadata for every target of every combo. The ~40 auto combos share one
candidate pool and the loop neither memoized nor yielded, so the #9147 fixture
(60 connections, 720 synced models) went from a ~4s build with a 167ms longest
event-loop gap to ~11s and a 860-1070ms gap on an idle box — past the 800ms
contract and past #12628's 8s cold-build bound, which is why the test came back
500 catalog_build_timeout on every release-green run.

Metadata depends only on the target's provider/model/connection scope within a
build, so memoize it per build and yield between misses. Same fixture: 2.3-3.1s
build, 56-72ms longest gap. The 9147 test is unchanged.

Refs #12732

* test(resilience): list credentialHealthCheck in the configuration-only key set

#12043 added credentialHealthCheck.intervalMinutes to DEFAULT_RESILIENCE_SETTINGS
and to the /api/resilience GET projection. It is operator configuration (the
background sweep cadence), not runtime breaker state, but the exact key-set
assertion was never updated, so resilience-http-e2e failed on the release tip.
The providerBreakers/runtime absence checks stay as they were.

Refs #12732

* fix(ci): stamp BUILD_SHA before the release-green pack gate validates

check:pack-artifact assembles dist/ through build:cli, which never writes
dist/BUILD_SHA (only build:release does). #12959 pointed the provenance ref at
HEAD, but the #10427 guard still stops at 'dist/BUILD_SHA is missing' before it
ever reaches the ancestry check — reproduced on tip + #13635 + #13436, the first
tree whose Turbopack build compiles.

ci.yml sequences build -> stamp -> validate; the validator now does the same in
both entry points, keeping PACK_GATE_ENV for the validate step. The guard is
unchanged: an unstamped dist/ or one built from another commit still fails.
On that tree the stamped gate passes: 'BUILD_SHA 5cb3ae5d9 is on the release line'.

Refs #12732

* docs(env): document OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS

#13248 (#12849) added the override for when a connection's synced model list
stops being authoritative, but neither .env.example nor ENVIRONMENT.md lists it,
so the env/docs contract gate reports it as code-only. The other five vars that
gate reports are already added by #13635 and #13361; this touches a different
region of .env.example so it does not collide with either.

Refs #12732
2026-09-15 09:00:51 -03:00
Damian Pozimski
9442bdef0f fix(ci): clear the release/v3.8.51 base-reds on the PR fast path (#13635)
* docs: bring the provider count to the live 358 across the reference, diagrams and llm.txt mirrors

* chore(skills): regenerate the cli-tunnel SKILL.md for the tunnel create positional

* test: clear the ESLint errors in the volcengine upsert and resource-pressure tests

* test(autoCombo): complete the mode-pack ProviderCandidate fixtures for the open-sse typecheck

* fix(ci): allow the opencode-plugin-v2 workspace package in the pack artifact policy

* docs: list the WAL, vacuum, sql.js and pressure self-restart env vars in .env.example

* refactor(db): move the synced-model provider purge into its persistence module to break the models/providers cycle

* test(memory): use a plain label for the rerank loopback key fixture so gitleaks stays at zero

* chore(ci): register the eleven covering unit tests in stryker tap.testFiles

* test(grok-cli): run the reset-credit tests on a fixture clock inside the captured token window

* test(combo): seed real provider connections for the reset-aware strategy tests

* fix(db): keep operator custom models out of the listing-only synced catalog reader

* fix(i18n): translate the new settings and combo keys for vi and pt-BR and restore the zh-TW glossary term

* fix(sse): carry the upstream error code and type through the provider execution pipeline

* test(sse): re-point the chatCore and combo source guards at the split modules and refresh the translate-path golden

* fix(oauth): keep the server-only OAuth constants out of the provider detail client bundle

* test: align the sql.js, webpack, injection-scan and error-boundary guards with their merged contracts

* docs(changelog): record the v3.8.51 base-red sweep

* fix(sse): anchor the glued-prefix sk- credential pattern so error redaction scans in linear time

* docs(changelog): note the linear credential scan in the base-red sweep

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-15 08:51:45 -03:00
Diego Rodrigues de Sa e Souza
f782f3b1e0 chore(skills): regenerate cli-serve after --ready-timeout was added (#13722)
`check:agent-skills-sync` (ci.yml:lint) has been failing on release/v3.8.51
because the generated skills/cli-serve/SKILL.md no longer matches the command
catalog. fc111dc1 (make startup readiness budget configurable) added the
`--ready-timeout <ms>` flag to bin/cli/commands/serve.mjs without regenerating
the skill.

This is the generator's output, unedited: a single added line listing the
flag, which exists at bin/cli/commands/serve.mjs:62. No other skill changed
(Generated: 1 · Unchanged: 45 before, Generated: 0 · Unchanged: 46 after).
2026-09-15 08:51:08 -03:00
Diego Rodrigues de Sa e Souza
5c1e35b98f fix(api): give the #13376 model-test skip a real httpStatus (#13730)
2a6d0586 added an early return in runSingleModelTest that skips image/music/
video-only models so a chat test never triggers a billable generation. The
returned object omitted httpStatus, which SingleModelTestResult requires
(TS2741 — this is the API Route Typecheck red on release/v3.8.51).

It was not only a type error. src/app/api/models/test/route.ts passes
result.httpStatus straight to NextResponse.json(body, { status }), so the
status was undefined, Next fell back to 200, and a skipped test reached the
client as an HTTP success with status: "error" in the body.

Answer 422 rather than the 409 the managed-lease return uses: the request is
well-formed, the model's modality just cannot be exercised by a chat test.

TDD: the new case seeds an images-only custom model, replaces fetch with a
throwing stub, and asserts the skip fires with a numeric 422 and no dispatch.
It failed with httpStatus undefined before the one-line fix and passes after.

model-test-runner + model-test-modality-guard-13376: 35/35
check-api-typecheck: OK (the TS2741 is gone)
2026-09-15 08:50:58 -03:00
Diego Rodrigues de Sa e Souza
c0f92ec98a fix(security): resolve findings from omni-code-sec battery (fix/batches-delete-completed-authz) (#13684)
Batch sweep enforces the caller's API-key policy (allowedEndpoints/schedule/usage/rate limit; the /api/v1 pathname now resolves its endpoint category for every /v1 route), commits per 200-batch chunk in key mode, guards against no-progress loops, rejects a scope naming both a key and allTenants; 8 covering tests registered for the mutation gate. Remaining CI reds are release base-reds (#12732), reproduced identically on the base tip. Refs #12969, #13680, #13681, #13685, #13377
2026-09-15 01:06:16 -03:00
Diego Rodrigues de Sa e Souza
3311ad20c6 fix(docs): document OMNIROUTE_READY_TIMEOUT_MS and allowlist the test-only DISABLE_IOREG_STRATEGY (#13692)
Clears the two env-doc drifts the batch introduced. `check:env-doc-sync` is back to the pre-existing base-red set (#12732). `check-env-doc-sync` unit suite: 13 structural cases green; the 2 live-data cases stay red on the same inherited vars.
2026-09-15 00:25:31 -03:00
Koosha Paridehpour
4745fca27d fix(zed-hosted): map developer role and Gemini enums for Zed proxy (#13543)
Zed-hosted requests are re-mapped to the narrower enums the Zed proxy validates. Responses input items with `role: "developer"` go back to `system` (#13362). For Gemini, `safetySettings[].threshold: "OFF"` becomes `BLOCK_NONE` and `functionCallingConfig.mode` `VALIDATED`/`AUTO`/`ANY`/`NONE` become Zed's lowercase `auto`/`any`/`none` (#13363). Chosen over #13541, which carried the identical commit.

Maintainer changes: the inline mapping used five `as any` casts, which `no-explicit-any` rejects in `open-sse/`. It is now two typed helpers (`adaptGeminiRequestForZed`, `adaptResponsesRequestForZed`, exposed through `__test__`) plus a small mode table; behavior is unchanged. Added `tests/unit/zed-hosted-proxy-enums-13362.test.ts` (4 cases). Zed suites 47/47; `check:open-sse-typecheck` clean.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:11:18 -03:00
Koosha Paridehpour
2d1a281d12 fix(providers): add alwaysReasons metadata for reka-flash-3 (#13531)
`reka-flash-3` is marked `supportsReasoning` + `alwaysReasons` (with its 8192 output cap). `DefaultExecutor.ensureThinkingBudget` treats always-on reasoning as an implicit opt-in, so a small caller `max_tokens` gets the 4096 floor instead of being spent on reasoning and returning an empty answer (#13198). `alwaysReasons` is documented on `RegistryModel`.

Maintainer addition: `tests/unit/reka-flash-3-always-reasons-13198.test.ts` asserts the floor for `reka-flash-3` without any reasoning settings (fails on the release tip) and that `reka-flash` keeps the caller value. The branch also carried the #13532 Compare-scroll commit, which landed first; its hunks are identical, so this squash only adds the reka change.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:08:31 -03:00
Koosha Paridehpour
3b5ce24acb fix(playground): allow Compare columns to scroll independently (#13532)
Playground Compare columns scroll independently. Each column root is a bounded flex column (`min-h-0 overflow-hidden`), and the grid clips only horizontally (`overflow-x-hidden min-h-0`), so a long answer in one column no longer drags the whole comparison (#13316).

Maintainer addition: `tests/unit/playground-compare-column-scroll-13316.test.ts`, a source-level guard in the same style as the other layout guards, pins both class sets (fails on the release tip).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:07:50 -03:00
Koosha Paridehpour
56ae80a6bd fix(reasoning): expand deepseek-v4 regex to match v4.1+ and add command-code provider (#13430) (#13528)
DeepSeek V4 point releases (`deepseek-v4.1-flash`, `deepseek-v4-2-pro`, `…-free`) now match the reasoning-replay patterns, and `command-code`, which routes upstream DeepSeek models, joins the providers that replay `reasoning_content`. Without it those models 400 with "reasoning_content in the thinking mode must be passed back" (#13430).

Maintainer addition: `tests/unit/reasoning-replay-deepseek-v4-point-release-13430.test.ts` covers the point-release ids through both `isDeepSeekReasoningModel` and `requiresReasoningReplay`, a non-match guard (`deepseek-v4x-flash`) and the command-code provider (2 of its 3 cases fail on the release tip).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:06:58 -03:00
Koosha Paridehpour
1cbb77c30b fix(analytics): normalize provider aliases to canonical id in usage_history (#13545)
`usage_history` rows are written with the canonical provider id (`resolveProviderId`), both from live `saveRequestUsage` and from the legacy JSON import. Traffic logged under an alias (`af`) and under the id (`api-airforce`) no longer splits one provider into two analytics buckets (#13459).

Maintainer addition: `tests/unit/usage-history-provider-alias-13459.test.ts` saves one row under the alias and one under the id and asserts a single `api-airforce` bucket (fails on the release tip, passes with the fix).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:05:21 -03:00
Koosha Paridehpour
30b5bf18fb fix(guardrails): skip credential redaction for base64 image data URLs (#13550)
The credential masker skips `data:image/*;base64,` strings. Credential regexes run over the base64 transport bytes could match by coincidence (the report hit `AIza…` → `[REDACTED:google]` inside a PNG), corrupting the image so strict upstreams returned 400 on every retry of that conversation (#13462). Only image data URLs are exempt; the same key-shaped text anywhere else is still redacted.

Maintainer addition: `tests/unit/credential-masker-image-data-url-13462.test.ts` reproduces the collision with a Google-key-shaped run inside a PNG data URL (fails on the release tip, passes with the fix) and guards that plain text is still redacted.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:03:42 -03:00
Koosha Paridehpour
93a398f353 chore(deps): pin joi to ^18.2.8 via overrides (#13085) (#13302)
Pins `joi` to `^18.2.8` via `overrides` (Dependabot #13085). Checked with all three pins combined (#13300, #13301, #13302): `npm install --package-lock-only` leaves `package-lock.json` unchanged. The lock already resolves joi 18.2.8, csv-parse 7.0.2 and hono 4.13.7, so the overrides pin those versions against future lock regenerations without changing the installed tree.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:02:50 -03:00
Koosha Paridehpour
6a55d0a170 chore(deps): bump hono from ^4.12.34 to ^4.13.7 (#13148) (#13301)
Raises the `hono` override from `^4.12.34` to `^4.13.7` (Dependabot #13148). Checked with all three pins combined (#13300, #13301, #13302): `npm install --package-lock-only` leaves `package-lock.json` unchanged. The lock already resolves joi 18.2.8, csv-parse 7.0.2 and hono 4.13.7, so the overrides pin those versions against future lock regenerations without changing the installed tree.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:02:37 -03:00
Koosha Paridehpour
cd112e34d3 chore(deps): pin csv-parse to ^7.0.2 via overrides (#13117) (#13300)
Pins `csv-parse` to `^7.0.2` via `overrides` (Dependabot #13117). Checked with all three pins combined (#13300, #13301, #13302): `npm install --package-lock-only` leaves `package-lock.json` unchanged. The lock already resolves joi 18.2.8, csv-parse 7.0.2 and hono 4.13.7, so the overrides pin those versions against future lock regenerations without changing the installed tree.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:02:24 -03:00
Koosha Paridehpour
d6e62ae394 fix(free-tiers): remove obsolete Together $25 signup credit (#12526) (#13407)
Removes the obsolete Together $25 signup credit (`together-signup-credit`, 25M one-time tokens) from `FREE_MODEL_BUDGETS` and updates the Together notes in `FREE_TIERS.md` (#12526). Approved by the maintainer. Chosen over #12766, which targeted `main` and only touched the doc.

Maintainer additions: propagated the catalog change to every derived number instead of hand-estimating it. The regenerated budget card (`gen-budget-card-svg.mjs`) computes first month ~2.07B (the doc said ~2.08B), 443 catalog entries (436 active + 7 discontinued) and one-time credits ~601M. README, `FREE_TIERS.md`, `FREE-TIERS-GUIDE.md` (Together row removed) and `docs/diagrams/free-tier-budget.svg` were aligned to those values, and Prettier-formatted. `check:docs-counts` and the budget-card, docs-counts and free-catalog tests pass (99 cases).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-15 00:01:27 -03:00
Koosha Paridehpour
0b358dbf64 fix(backend): guard memory_fts_au trigger with WHEN clause to stop FTS bloat (#13326) (#13405)
Stops `memory_fts` bloat (#13326). The `memory_fts_au` trigger re-indexed the FTS row on every UPDATE of `memories`, including the `access_count` / `last_accessed_at` bumps from `recordMemoryAccess()`, so FTS data rows grew without bound. The trigger is recreated with `WHEN old.content IS DISTINCT FROM new.content OR old.key IS DISTINCT FROM new.key`, an `optimize` pass compacts existing tombstones, and `cleanupMemoryEntries()` runs `optimize` after retention deletes.

Maintainer additions: migration 177 was already taken by `177_provider_connection_synced_models_at.sql` (#13248), and a duplicate number aborts every DB open. Renumbered to `178_memory_fts_au_conditional.sql` and bumped the migration count 174→175 in README, AGENTS.md, `llm.txt` and its 58 locale mirrors (protected surfaces, operator-approved for this PR). The migration also applied cleanly on a real long-lived dev database. `check:migration-numbering`, `check:docs-counts` and `check-docs-sync` pass.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:58:27 -03:00
Koosha Paridehpour
a0d8ad7082 fix(antigravity): derive stable session ID from credentials for prompt-cache reuse (#13305) (#13424)
`getAntigravitySessionId` derives a stable session id from the account key (FNV-1a over the existing `getAntigravityAccountKey`) when credentials are present, so repeated requests from one account can reuse Antigravity prompt caching (#13305). An explicit fallback id still wins, and calls without credentials still get a random id. This deliberately reverses the per-request random id from #10443; the maintainer approved the trade-off.

Maintainer addition: `credentials` is now used, so the frozen `no-unused-vars` suppression for `open-sse/services/antigravityIdentity.ts` became stale. ESLint fails on stale suppressions, so it was pruned. Antigravity suites: 34 cases green.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:56:35 -03:00
Koosha Paridehpour
fb3f298489 fix(security): empty whitelist allows through to prevent admin lockout (#13534)
In `whitelist` mode, an empty whitelist now lets requests through instead of blocking everyone. Switching to whitelist mode before adding any IP no longer locks the admin out of the dashboard; enforcement starts with the first entry (#13176). Approved by the maintainer as a policy change.

Maintainer note: the existing `addToWhitelist/removeFromWhitelist: dynamic updates` test asserted the old fail-closed contract (remove the only entry → blocked). It now uses two entries to keep checking that removal blocks, plus an explicit assertion that removing the last entry returns to the new open state. That is the operator-visible consequence of this change: clearing the list disables enforcement. IP-filter suites: 53 cases green.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:55:21 -03:00
Koosha Paridehpour
918546acf2 fix(resilience): increase requestQueue.maxWaitMs default from 15s to 30s (#13553)
Raises the default request-queue wait (`RATE_LIMIT_MAX_WAIT_MS`) from 15s to 30s, so bursts behind a rate-limited provider queue a bit longer before being rejected (#13504). The env var still overrides it, and `executionMaxWaitMs` (the 10-minute execution backstop) is unchanged. Approved by the maintainer as a policy change.

Maintainer additions: updated the documented default in `.env.example` and `docs/reference/ENVIRONMENT.md` to match.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:53:18 -03:00
Koosha Paridehpour
d4a1470e40 fix(compression): run user preservePatterns before built-in patterns (#13457) (#13525)
`cavemanConfig.preservePatterns` regions are captured before the built-in patterns. Previously the built-ins ran first and turned inline code, URLs and CONST_CASE identifiers inside the user region into sentinels, and `replacePattern` silently skipped the user match (#13457).

Maintainer adaptation: after #13523 moved the instruction envelopes into a region pass that runs before fenced-code extraction, the user patterns now open that same pass. User regions win over the built-in envelopes, and fenced code inside a user region stays with the region too. This keeps the "run user patterns first" semantics of your change. Also fixed the new test's `../../open-sse` import path (it had never run) and three unused `tombstoned` bindings. Reset to the release tip plus only this commit (the branch carried ~2.6k unrelated commits), authorship preserved. All 5 new cases pass; full compression suite 1,531/1,531; budget gate clean.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:52:14 -03:00
Koosha Paridehpour
56fedf985e fix(compression): preserve polarity words and newlines in ultra heuristic (#13524)
The ultra heuristic no longer prunes polarity and modality words (`never`, `always`, `not`, `must`, `should`, `do`/`does`/`did`, contractions…). They now score 1.0 instead of the 0.1 stopword score, so "must never be deleted" can no longer collapse into "must deleted". It also collapses only runs of spaces and tabs, keeping the newlines that carry bullets, headings and fences (#13454).

Maintainer fixes: (1) the new test imported `../../open-sse/…` from `tests/unit/compression/`, which does not resolve, so it had never run. It now passes with the rest of the compression suite (1,526 cases). (2) Dropped the unused `STOPWORDS` import. (3) `check:compression-budget` flagged the expected trade-off: ultra tokens-per-task goes prose 92→97, tool-output 116→117, json 117→126, the cost of keeping meaning-bearing words and line structure. The baseline was refreshed with `--update`, which also records a pre-existing caveman tightening (129/127/160 → 119/114/136). (4) The branch also carried the unrelated `feat(sveltekit)` commit and the #13523 commit, so it was reset to the release tip plus only this commit, authorship preserved.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:44:26 -03:00
Koosha Paridehpour
c46ca1dc75 fix(compression): preserve instruction blocks from lossy engines (#13523)
Lossy compression engines leave `<system-reminder>`, `<instructions>` and `<project-instructions>` envelopes byte-identical. Agentic CLIs inject these into user messages, and compressing them inverted negations, dropped emphasis and broke the tags (#13453).

Maintainer fixes: (1) the new test imported `../../open-sse/…` from `tests/unit/compression/`, which does not resolve, so it had never run. With the path fixed, the main case failed: the envelopes were appended to the built-in list, so fenced code and inline code inside the reminder had already become sentinels, and `replacePattern` skips any match containing one. The envelopes now form a region pass right after frontmatter, before fenced-code extraction; all 4 cases and the full compression suite (1,518) pass, and the compression budget gate reports no regression. (2) Dropped an unused `tombstoned` binding. (3) The branch also carried an unrelated `feat(sveltekit)` commit (`apps/web/**`), so it was reset to the release tip plus only this commit, authorship preserved.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:36:36 -03:00
Koosha Paridehpour
bf6658984b fix(auth): check model lockout before returning synthetic noauth connection (#13547)
No-auth providers now honor a recorded model-only lockout before `getProviderCredentials` hands back the synthetic `noauth` connection (#13483). That early return skipped the per-connection status pass, so a `model_capacity` lockout was recorded but never enforced, and every request re-tried the locked model for a wasted upstream round-trip before failing over.

Maintainer additions: carried your `tests/unit/noauth-model-lockout.test.ts` from #13527. 3 of its 4 cases fail on the release tip without the fix and pass with it. Rebaselined `src/sse/services/auth.ts` 3542→3556 in `file-size-baseline.json` with a dated annotation.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:28:25 -03:00
Koosha Paridehpour
97ae10179c fix(combo): use traceId for call log id to prevent UNIQUE constraint failure (#13546)
Combo attempt call logs are keyed on the per-attempt `traceId` instead of the shared `pendingRequestId` (#13481). Every attempt of a failed-over combo reused the same id, so the successful member hit the `call_logs` UNIQUE constraint and was silently dropped: the dashboard showed only the failed steps.

Maintainer additions: carried your regression test from #13526 (`combo attempt uses traceId as the log id, not pendingRequestId`, two attempts sharing one request id both persist) into this cleaner branch. Removed the now-unused `pendingRequestId` destructure (`no-unused-vars`) and added a short comment at the call site.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:27:15 -03:00
Koosha Paridehpour
cce3a958b5 fix(tests): add DISABLE_IOREG_STRATEGY env var for macOS test compatibility (fixes #13467) (#13539)
Adds a `DISABLE_IOREG_STRATEGY=1` escape hatch to the macOS `ioreg` strategy in `getMachineIdRaw()`, so `machineId.test.ts` can reach the fallback strategies on darwin instead of always resolving the real hardware UUID (#13467).

Maintainer note: this branch also carried the "force passed=false when upstream call failed" eval commit, which already landed in #13413. The branch was reset to the current release tip plus only the ioreg commit (authorship preserved) so the squash carries just this change.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:25:33 -03:00
Koosha Paridehpour
222fe4314e docs(providers): add Microsoft 365 Copilot (BizChat) provider guide (#12779) (#13340)
Adds the Microsoft 365 Copilot (BizChat) provider guide for the existing `copilot-m365-web` provider (alias `m365copilot`, `open-sse/config/providers/registry/copilot-m365-web`), including the WebSocket credential capture flow, and links it from the docs index and the web-cookie guide (#12779).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:24:59 -03:00
Koosha Paridehpour
fc111dc196 feat(cli): make startup readiness budget configurable (#13369) (#13433)
The CLI readiness budget is configurable through `OMNIROUTE_READY_TIMEOUT_MS` or `omniroute serve --ready-timeout <ms>` (default unchanged at 60s). The timeout warning prints the budget it actually used and suggests a larger value, for slow cold starts such as Windows (#13369). Documented in `ENVIRONMENT.md` and `TROUBLESHOOTING.md`, with 11 resolver cases.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:24:46 -03:00
Koosha Paridehpour
1738beb0fe fix(memory): merge memory into top-level system field instead of unshifting at messages[0] (#13425) (#13427)
Memory injection merges into an Anthropic-shaped top-level `system` field (string or text-block array) instead of prepending a `role: "system"` message at `messages[0]`, which Anthropic rejects with a 400 (#13425). Handled on both the system-first path (xiaomi-mimo) and the general path. Chosen over #13549, which covered only the string case.

Maintainer fix: widened `ChatRequest.system` to `string | Array<{ type; text?; … }>`. Assigning the block array to the `string`-typed field raised 2× TS2322 under `check:open-sse-typecheck`.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:24:33 -03:00
Koosha Paridehpour
38ce44992e fix(providers): de-duplicate agy and antigravity model catalogs (#13410)
De-duplicates the `agy` and `antigravity` model catalogs into `antigravitySharedModels.ts`, with an explicit per-surface add/remove delta (#12724). Verified behavior-neutral: every exported catalog constant of both modules serializes byte-identically before and after (201-line JSON dump).

Maintainer fix: `buildSurfaceCatalog` returned `readonly { id: string; [k: string]: unknown }[]`, which erased the element type. `name` became `unknown`, and the `RegistryEntry.models` assignments failed with 4× TS2322 under `check:open-sse-typecheck` (not covered by `typecheck:core`). It is now generic (`<T extends { id: string }>`), so the literal model shape flows through.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:21:56 -03:00
Koosha Paridehpour
3a186c1326 fix(dashboard): send provider in body for mini-playground web search (#13411)
The media-providers web-search example card now sends `provider` in the request body. `POST /v1/search` selects the provider from `body.provider`, so the card was silently hitting the default provider (#13245).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:18:44 -03:00
Koosha Paridehpour
76c928dd35 fix(backend): eval runner opts out of output-style and memory injection (#13414)
The eval runner sends `x-omniroute-compression: off` and `x-omniroute-no-memory: true`, so cases measure the model rather than injected output styles or retrieved memory (#13139). Both headers are the existing per-request opt-outs honored by `chatCore` (`open-sse/handlers/chatCore/headers.ts`).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:18:32 -03:00
Koosha Paridehpour
fdfa921bbb fix(backend): eval runner: failed calls no longer score as passed + dotAll regex (#13413)
Eval runner: a case whose upstream call failed can no longer score as passed when the grading regex happens to match the error text (#13137), and regex grading compiles with dotAll so `.` spans newlines in multi-line answers, for both string and `RegExp` patterns (#13138). Chosen over #13542 / #13530, which each covered half.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:18:20 -03:00
Koosha Paridehpour
f938a08686 fix(resilience): add CJK quota-exhaustion patterns to 429 classifier (#13412)
Adds CJK quota-exhaustion messages (GLM/z.ai 5-hour window, Kimi, Qwen/DashScope, MiniMax, plus Japanese and Korean phrasings) to the 429 quota classifier. They are now `quota_exhausted` (cooldown + failover) instead of a transient `rate_limit` retry loop (#13194). The patterns go into the recoverable `QUOTA_PATTERNS` only, not the terminal set, and a guard case keeps the Chinese transient "请求过于频繁" as `rate_limit`. Chosen over #13536.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:18:07 -03:00
Koosha Paridehpour
2a6d0586cd fix: skip non-chat generation models in Test all models (#13409)
"Test all models" no longer sends image/music/video generation-only models through a chat completion, which triggered real billable generations (#13376). `detectTestKind` flags them via `isNonChatGeneration` and `runSingleModelTest` skips them; chat+image models, embeddings and models without metadata are unaffected (8 new cases plus updated `model-test-runner` shapes).

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:17:55 -03:00
Koosha Paridehpour
43b26615e1 feat(mcp): carry context_length through MCP list_models_catalog projection (#12776) (#13406)
Carries `context_length` through the MCP `list_models_catalog` projection when the upstream model record has it, and omits it otherwise (#12776). Covered by the new case in `mcp-model-catalog.test.ts`.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:17:42 -03:00
Koosha Paridehpour
04bea60303 fix(backend): prune old backups after health-check-repair snapshots (#13308) (#13404)
Prunes `db_backups/` after the health-check-repair `VACUUM INTO` snapshot (#13308). That path never ran retention, so every restart of a healthy database added another full-size copy. It now uses the same `DB_BACKUP_MAX_FILES` / `DB_BACKUP_RETENTION_DAYS` limits as `backup.ts`, importing `backupRetention` directly to avoid the import cycle.

Maintainer additions: merged the current release branch and rebaselined `src/lib/db/core.ts` 1745→1767 in `file-size-baseline.json` with a dated annotation (legitimate growth). Chosen over #13540, which did the same with a fire-and-forget dynamic import.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:17:11 -03:00
Koosha Paridehpour
65f513112a fix(tests): correct stale Jina catalog prefix in custom-models test (#13403)
Fixes the stale `jina-ai/` prefix in the second Jina assertion of `models-catalog-route.test.ts`. The catalog emits `jina/…` (the first Jina test already expected it), so this clears a base red on the release branch.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:14:47 -03:00
Koosha Paridehpour
8bad85f58e fix(logging): mask *-api-key headers in request-log pipeline (#13273) (#13401)
Masks every `*-api-key` header spelling (`x-goog-api-key`, `api-key`, `xi-api-key`) in the request-log pipeline by matching on the hyphen-compacted key, and adds `xi-api-key` to the payload redaction set (#13273). The new test drives the real `createRequestLogger` / `protectPayloadForLog` paths.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:14:35 -03:00
Koosha Paridehpour
67cb0ac96b fix: chain serial quarantine step in npm test script (fixes #13286) (#13418)
Chains `npm run test:unit:serial` onto the plain `npm test` script, so the quarantined serial suite is no longer skipped when contributors run `npm test` (#13286). The existing `test-serial-quarantine` guard now covers `test` too.

Validated in one consolidated batch of this series (37 PRs boarded together on `release/v3.8.51`): `typecheck:core`, `check:open-sse-typecheck` and `check:dashboard-typecheck` clean; ESLint clean on every changed file; file-size, complexity, cognitive-complexity, changelog-integrity, docs-counts, docs-sync and migration-numbering gates green (only the pre-existing `open-sse/utils/stream.ts` file-size red remains, inherited from the base); 3,743 focused `node:test` cases plus 34 vitest cases green.

Thanks @KooshaPari!
2026-09-14 23:13:36 -03:00
Diego Rodrigues de Sa e Souza
5ca724d2cf fix(api): scope batch bulk-delete to the calling API key (#12969)
Merged after reconciling the whole stack onto the tip, operator-reviewed before merge.

The core ownership fix for GHSA-wvxc-jp3v-5mg5 had already landed via #13211 with a different implementation of the same endpoint. This branch carries a stricter one, built up in layers: this PR, #13262 (explicit scope, audit log, atomic sweep), #13297 (revoked, deactivated, banned or expired keys rejected) and #13374 (owner-scoped file half, chunked instance sweep — SEC-C/SEC-D). The stack's implementation was kept over #13211's because it is stricter on every point:

- **route:** with #13211, a request carrying BOTH a dashboard session cookie and an API key swept the whole instance. Here a presented key always scopes the sweep to that key; only a session without a key sweeps all tenants; neither returns 401. Instance-wide and row-deleting sweeps log at warn as an audit trail, and a failing sweep returns a sanitized 500.
- **`deleteCompletedBatches`:** takes an explicit `{ apiKeyId } | { allTenants: true }`. An omitted or blank key throws instead of widening, and passing both throws.
- **files:** a key sweep only soft-deletes files the caller owns (`deleteFileOwnedBy`), so a batch referencing another tenant's or an unowned file never nulls its content.
- **transactions:** key mode is all-or-nothing across chunks; instance mode commits per 200-id chunk so a large sweep never holds one write lock on the table.

#13211's own test was aligned to the explicit-scope API with its assertions unchanged. Its seed now creates the file with the batch's owner, as an upload through that key does in production — without that, SEC-C correctly leaves the unowned file intact.

- 51/51 across the six batch suites (#13211's test, this stack's ownership and route-scope tests, `batch-deletion`, `batch-deletion-route-logic`, `files-delete-owned-by`)
- ESLint, `typecheck:core`, complexity, cognitive-complexity, changelog integrity: clean

⚠️ base-red inherited: #12732
2026-09-14 19:25:46 -03:00
Diego Rodrigues de Sa e Souza
895dda383e fix(ci): clear the base-reds on release/v3.8.51 — doc counts, catalog test flake, pack provenance + pack policy (#12959)
Merged after salvaging what still applies. The doc-count half of this PR is superseded — it moved migrations 169 → 171 while the tip is already at 174, and the provider count landed via #13216 — so every docs, diagram and llm.txt mirror change was reset to the tip's version and its changelog fragment dropped. Merging it as it stood would have regressed the counts.

Kept, none of it on the tip:

- `scripts/build/pack-artifact-policy.ts`: `@omniroute/opencode-plugin-v2/` added to the allowlist — #12870 shipped the v2 plugin without widening it, so every packed file under it read as unexpected
- `scripts/quality/validate-release-green.mjs`: points the pack provenance guard at `HEAD` instead of `origin/main`, which is structurally unreachable from a release branch mid-cycle
- `12058-models-catalog-canonical-self-aliased.test.ts`: pins `CATALOG_BUILD_TIMEOUT_MS` out of the way of a cold catalog build on a loaded runner; assertions unchanged

54/54 across those three suites, ESLint exit 0, changelog integrity OK.

⚠️ base-red inherited: #12732
2026-09-14 19:04:28 -03:00
Diego Rodrigues de Sa e Souza
8786c1732f fix(ci): clear the orphan base-reds on release/v3.8.51 — the #12867 pipeline extraction, a legacy-DB boot abort and the catalog readers (#13349)
Merged after a real reconciliation — this PR and #13069 fixed the same #12867 regression (the non-streaming leg losing failure classification and credential refresh) with different strategies, and #13069 landed first. Rather than stacking two implementations, the tip's was kept and this PR was trimmed to what the tip still lacked.

**Dropped, already on the tip:**
- non-streaming credential refresh and failure-state persistence → #13069 (`applyProviderFailureClassification`); the `persistProviderFailureState` hook this PR added would have been dead code
- body-derived rate-limit lock and non-JSON body message → already in the tip's pipeline (`chat-rate-limit-body-lock` passes there)
- codex image-generation stringify of a sanitized error body → #12945 (`stringifyImageErrorForLog`, which would otherwise be declared twice — TS2393)
- the two test realignments (`hard-session-lease` inventory wording, kiro stream reader) → #12945

**Kept, missing on the tip:**
- **upstream error `code`/`type` in the pipeline error outcome** — ported onto the tip's implementation. Running this PR's own test against the tip returned `errorCode: undefined` instead of `missing_project_id`, so a config-class Antigravity 422 still degraded into an account cooldown.
- legacy `call_logs` boot abort (index created after column healing)
- malformed operator custom-models row no longer kills every `auto/*` pool (`virtualFactory.ts` 1219 → 1230, annotated)
- realigned guards

**Evidence on the reconciled tree**
- 104 of 106 focused assertions. The 2 red (`models-catalog-route`, `provider-node-reserved-prefix`) fail identically on the pure tip. Against the tip, this PR turns 7 previously red cases green.
- ESLint, `typecheck:core`, `check:open-sse-typecheck` (0 errors), `check:changelog-integrity`: all clean
- changelog fragment rewritten to claim only what this PR still delivers

Unblocking this also surfaced a repo-wide red: the eight llm.txt mirrors added by #13660 kept the pre-#13216/#13248 counts, which failed the pre-commit `docs-sync` gate for everyone. Fixed separately in #13674.

`skills/cli-tunnel/SKILL.md` needed no change — #13216 already landed the identical edit.

⚠️ base-red inherited: #12732
2026-09-14 19:02:09 -03:00
Diego Rodrigues de Sa e Souza
8f3621156d docs(i18n): sync provider and migration counts in the eight new llm.txt mirrors (#13674)
Unblocks the pre-commit docs-sync gate on the release tip.
2026-09-14 19:00:38 -03:00
Diego Rodrigues de Sa e Souza
0d13ef4fbb feat(i18n): 8 new locales — Kannada, Malayalam, Odia, Punjabi, Nepali, Sinhala, Burmese, Khmer (59 locales) (#13660)
Batch 2 of the locale expansion across the dashboard catalog, docs mirrors, CLI catalog, README, locale index and the site. 51 → 59 locales.

Also: the translator restores the ICU literal escape around angle placeholders, and the docs chunker splits oversized sections before translating.

⚠️ base-red inherited: #12732 — the eight red checks fail identically on unrelated PRs cut from the same base (e.g. #13197); every gate is green locally after merging the base.
2026-09-14 18:22:01 -03:00
Diego Rodrigues de Sa e Souza
963d3c46ef fix(ci): clear three base-reds on release/v3.8.51 — open-sse typecheck, docs provider count, agent-skills sync (#13216)
Merged after reconciling against the tip. The one conflict was `docs/reference/PROVIDER_REFERENCE.md`, a generated file — regenerated with `npm run gen:provider-reference` (358 unique IDs) rather than hand-merged.

The three base-reds this PR targets, re-checked on the reconciled tree:

- `check:docs-counts`: **exit 0** — provider count 356 → 358 in AGENTS.md, llm.txt, the i18n mirrors and the SVG diagrams was its last red
- `check:open-sse-typecheck`: OK, 0 errors
- `check:agent-skills-sync`: exit 0 (`skills/cli-tunnel/SKILL.md` → `tunnel create [type]`)
- `autoCombo.test.ts` under the mcp vitest config: 63/63
- ESLint over the changed files: exit 0

AGENTS.md, llm.txt and SKILL.md are agent-instruction surfaces; the operator approved them for this PR explicitly.
2026-09-14 14:13:45 -03:00
Diego Rodrigues de Sa e Souza
30bf6affe9 fix(nvidia): fail open when a synced model catalog goes stale (#12849) (#13248)
Merged after renumbering. `176_provider_connection_synced_models_at.sql` collided with `176_xp_action_counts.sql` (#12651), which made the migration runner abort on every DB open. Renamed to **177**; the doc count moves 173 → 174 across README.md, AGENTS.md, llm.txt and the i18n mirrors (operator-approved, 206 numeric substitutions and nothing else).

- `check:migration-numbering`: OK, 174 migrations, no duplicates
- `check:docs-counts` migrations: ✓
- 84/84 across the NVIDIA suite plus the seven DB-touching suites the collision had taken down
- ESLint and `typecheck:core`: exit 0

Heads-up for whoever lands next: **177 is claimed by eight other open PRs** (#13610, #13602, #13580, #13554, #13405, #13331, #13177, #13116) and 176 by #13373 and #13102. With this merged, all of them need to renumber at merge time — `check:migration-numbering` forbids new gaps, so the next free number is always the only valid one.

⚠️ base-red inherited: #12732
2026-09-14 14:09:14 -03:00
Diego Rodrigues de Sa e Souza
f7dbfc88c7 chore(tests): revive 51 vitest files parked behind a closed tracker (#13204) (#13233)
Merged after reconciling against the tip. `docs/architecture/QUALITY_GATES.md` conflicted: this PR adds the `check:vitest-exclusions` row while the tip had meanwhile rewritten the `check:model-lifecycle` description (#12535). Resolved to the tip's table plus the new row.

- `check:vitest-exclusions`: OK, 11 excluded files, each tracked and referenced
- ESLint over the changed files: exit 0
- `npm run test:vitest` (the config this PR edits): 472 of 473. The one red, `tests/unit/autoCombo/provider-family-combos.test.ts`, is not touched by this PR and passes 11/11 in isolation on both this branch and the pure tip — a load flake from the full run, not a regression.

⚠️ base-red inherited: #12732
2026-09-14 13:40:46 -03:00
Diego Rodrigues de Sa e Souza
152d95108c fix(cache): include tool_choice/tools/response_format in semantic cache signature (#12734) (#13267)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:29 -03:00
Diego Rodrigues de Sa e Souza
cf2d29d2ad fix(memory): authenticate internal /v1/rerank loopback call (#12745) (#13268)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:25 -03:00
Diego Rodrigues de Sa e Souza
1b2dd3d282 fix(a2a): accept dashboard session auth on /a2a route (#12888) (#13271)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:21 -03:00
Diego Rodrigues de Sa e Souza
7741aefbf6 fix(i18n): add the Phase 3 orchestration keys to the nine EU locales (#13294)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:17 -03:00
Diego Rodrigues de Sa e Souza
db6feb6fad fix(auth): dashboard session requires the authenticated claim — Cursor CLI tokens are not sessions (#13375)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:12 -03:00
Diego Rodrigues de Sa e Souza
6dc66921a7 chore(electron): prune the root-level files of every translated docs mirror from the desktop bundle (#13235)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:09 -03:00
Diego Rodrigues de Sa e Souza
b97bc59f4f fix(docker): require a per-session token on the VNC browser CDP bridge (#12571) (#13241)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:05 -03:00
Diego Rodrigues de Sa e Souza
1fb7d5dff2 fix(providers): refresh Trae's stale Referer/Origin and forward user timezone (#12190) (#13255)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:29:02 -03:00
Diego Rodrigues de Sa e Souza
fb8f7d7fa2 chore(deps): patch toml and esbuild in the opencode plugin lockfiles (#12965)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:28:58 -03:00
Diego Rodrigues de Sa e Souza
95ebed770b test(plugin): wait for the v2 lazy-refresh tier instead of sleeping past it (#12984)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:28:54 -03:00
Diego Rodrigues de Sa e Souza
01a66e26c6 fix(ci): clear two base-reds on release/v3.8.51 — mutation-coverage gate + image-only-model guard (#12945)
Merged as part of the owner batch of 2026-09-11.

This PR had a live worktree in another session, so it sat outside the main 39. Merged on your explicit call, validated first rather than taken on trust: boarded with the other 10 worktree-held PRs into a consolidated worktree off `release/v3.8.51`.

- ESLint over every changed file: no errors
- `typecheck:core` clean; `check:dashboard-typecheck` OK; `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437
- 203 of 208 assertions green. The 5 remaining (`guide-settings-route` ×4, `hard-session-lease-bypass-inventory` ×1) reproduce on the pure tip with nothing from this batch applied.
- `imageGeneration.ts` rebaselined 3259 → 3293 for #12945's image-only-model guard, landed separately in #13392 so nothing was pushed onto a live branch.

⚠️ base-red inherited: #12732 — provider count 356 vs 358 and `open-sse/utils/stream.ts` 3115 > frozen 3098, both reproducing on the pure tip.
2026-09-11 22:28:50 -03:00
Diego Rodrigues de Sa e Souza
84bc929dd8 chore(quality): rebaseline imageGeneration.ts for the #12945 image-only-model guard (#13392)
Unblocks #12945.
2026-09-11 22:28:32 -03:00
Diego Rodrigues de Sa e Souza
f5e8c149cb fix(sse): parse Ollama Cloud's current usage markup (#12749) (#13260)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:29 -03:00
Diego Rodrigues de Sa e Souza
0bdbe48de2 fix(providers): stop devin-cli spawn error from double-closing SSE controller (#12517) (#13261)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:26 -03:00
Diego Rodrigues de Sa e Souza
2acae48a5a fix(sse): route shell-reported Auggie CLI-not-found through the actionable message (#12645) (#13263)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:23 -03:00
Diego Rodrigues de Sa e Souza
8c5eff1bb4 fix(oauth): align codebuddy-cn OAuth User-Agent with chat/usage (#12702) (#13264)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:21 -03:00
Diego Rodrigues de Sa e Souza
bdc2fb76f6 fix(oauth): warn before the dead localhost:8080 redirect in antigravity/gemini oauth start (#12413) (#13265)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:18 -03:00
Diego Rodrigues de Sa e Souza
2cb02d26c6 fix(sse): exempt tiny-budget reasoning probes and trace persisted-cooldown skips (#12659) (#13269)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:15 -03:00
Diego Rodrigues de Sa e Souza
57d9e74881 fix(dashboard): surface auth-required banner for guest-session database settings (#12709) (#13270)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:12 -03:00
Diego Rodrigues de Sa e Souza
1361a9dd88 fix(sse): add first-byte watchdog to the TLS-fingerprint transport (#12656) (#13272)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:09 -03:00
Diego Rodrigues de Sa e Souza
0569f420be fix(providers): route opencode-go/gpt-5.6-luna to /responses (#12196) (#13275)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:06 -03:00
Diego Rodrigues de Sa e Souza
8751f111b2 fix(routing): stop reactive-compaction log from lying when compression is disabled (#11977) (#13276)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:06:03 -03:00
Diego Rodrigues de Sa e Souza
02128f3343 fix(sse): register Arcee AI in the executor provider registry (#12784) (#13277)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:59 -03:00
Diego Rodrigues de Sa e Souza
c5707e65de fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129) (#13278)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:56 -03:00
Diego Rodrigues de Sa e Souza
13ee0f1e73 fix(sse): cap HuggingChat NDJSON body size and bound the read loop (#12577) (#13282)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:53 -03:00
Diego Rodrigues de Sa e Souza
a5db197663 fix(routing): stop round-robin combo opencode targets from collapsing onto opencode-zen (#11912) (#13283)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:50 -03:00
Diego Rodrigues de Sa e Souza
660137b3ce fix(routing): recognize CLIProxyAPI unknown-provider 400 as fallback-worthy (#12800) (#13284)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:47 -03:00
Diego Rodrigues de Sa e Souza
7a938fe39f fix(sse): surface an error for a truly empty Claude stream (#12398) (#13285)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:44 -03:00
Diego Rodrigues de Sa e Souza
39abab6681 fix(dashboard): honor active compression profile in effective-pipeline preview (#12063) (#13287)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:40 -03:00
Diego Rodrigues de Sa e Souza
2e872c50aa fix(dashboard): surface combined-pipeline error in Compression Studio Run (#12061) (#13288)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:34 -03:00
Diego Rodrigues de Sa e Souza
0431db3c62 fix(tests): align model-sync log assertion to shipped path redaction (base-red) (#13227)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:29 -03:00
Diego Rodrigues de Sa e Souza
cb55187eda chore(ci): make worktrees carry the hooks that enforce the identity gate (#13236)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:25 -03:00
Diego Rodrigues de Sa e Souza
b3bee580a8 fix(api): create DB export temp paths with mkdtemp instead of predictable timestamps (#12579) (#13237)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:22 -03:00
Diego Rodrigues de Sa e Souza
b8b638e649 fix(api): remove hardcoded wildcard CORS in openai-to-gemini-sse.ts (#12573) (#13239)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:18 -03:00
Diego Rodrigues de Sa e Souza
57a37ed858 fix(docker): bind app/sidecar compose ports to loopback by default (#12568, #12578) (#13240)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:14 -03:00
Diego Rodrigues de Sa e Souza
d0b22ddf86 fix(open-sse): write Adobe Firefly session tokens and cookie jars with 0700/0600 perms (#12572) (#13242)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:11 -03:00
Diego Rodrigues de Sa e Souza
bc28bb06b1 fix(api): enforce API key policy on ElevenLabs proxy routes (#12574) (#13244)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:07 -03:00
Diego Rodrigues de Sa e Souza
6207868dc1 fix(cli): setup-opencode respects --api-key/OMNIROUTE_API_KEY over context token (#12783) (#13246)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:03 -03:00
Diego Rodrigues de Sa e Souza
954f12b202 fix(providers): correct opencode-zen muse-spark context length and Responses auth header (#12681, #12633) (#13247)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:05:00 -03:00
Diego Rodrigues de Sa e Souza
480b190fdc fix(providers): minimax-m3 collapses manual thinking.type:enabled to adaptive (#12132) (#13249)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:57 -03:00
Diego Rodrigues de Sa e Souza
10099d037f fix(db): scope local-provider apiKey dedup to base URL for multi-account LM Studio (#12173) (#13250)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:54 -03:00
Diego Rodrigues de Sa e Souza
d3a59621bf fix(electron): relativize standalone symlink targets for Windows manifest verify (#11979) (#13251)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:50 -03:00
Diego Rodrigues de Sa e Souza
15b26879e6 fix(dashboard): allow deleting the last extra-upstream-header row even when invalid (#12251) (#13252)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:47 -03:00
Diego Rodrigues de Sa e Souza
8d52dcabe2 fix(cli): run Node runtime guard before heavy import chain (#12296) (#13253)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:44 -03:00
Diego Rodrigues de Sa e Souza
ced6ee72b3 fix(providers): scope TinyCMS DOM shims to the call, not the process (#12072) (#13254)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:41 -03:00
Diego Rodrigues de Sa e Souza
907ffccd55 fix(dashboard): refresh the providers list after deleting a compatible provider node (#12298) (#13256)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:38 -03:00
Diego Rodrigues de Sa e Souza
0b7a6abf48 fix(usage): fail closed on budget enforcement for unpriced auto alias (#12341) (#13257)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:34 -03:00
Diego Rodrigues de Sa e Souza
c79caa913a fix(db): scope model-hidden overrides by modality (#12172) (#13258)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:31 -03:00
Diego Rodrigues de Sa e Souza
9982e37e03 fix(guardrails): stop Vision Bridge from re-selecting a model locked after a 404 (#12111) (#13259)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:28 -03:00
Diego Rodrigues de Sa e Souza
2ecb3d5c2d test(combo): freeze the clock where quota-weighted scores are compared (#12946)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:25 -03:00
Diego Rodrigues de Sa e Souza
f1fc54eeb2 fix(api): close DNS-rebinding SSRF gap in webhook outbound-URL guard (#12569) (#13243)
Merged as part of the 39-PR owner batch of 2026-09-11, validated as a unit.

Boarded into one consolidated worktree cut from `release/v3.8.51` with the other 38 — zero conflicts between them.

- ESLint over every changed file: no errors (the only finding was one suppression entry the batch emptied, pruned on #13243)
- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK
- complexity 2821 / baseline 3218 and cognitive-complexity 1272 / baseline 1437 — both under baseline
- 256 assertions green: 246 under node:test and 10 under vitest, which is where `tests/unit/**/*.test.tsx` actually runs
- `check-file-size`: `chatCore.ts` rebaselined 6144 → 6146 for #13278 and #13276, annotated and landed on #13243

⚠️ base-red inherited: #12732 — the provider count (356 in the docs vs the 358 the modules define) and `open-sse/utils/stream.ts` at 3115 > frozen 3098 both reproduce on the pure tip with zero contribution from this batch.
2026-09-11 22:04:21 -03:00
Bob.Hou
fe73a3ad35 fix(responses): wrap forced-non-streaming web_search JSON as SSE (#13050)
Nasty shape: HTTP 200, upstream billed, and Codex times out waiting for `response.completed` that never comes because the body is JSON. Good catch that wiring the wrap in `handleResponsesCore` would not have fixed it — production goes through `handleChat`, and that helper is tests-only. Stamping the client's SSE intent before flipping `stream` off is the right place to remember it.

Reconciled against the tip after the batch landed: only the file-size baseline conflicted, resolved to the batch's rebaselined value. 5/5 afterwards.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 20:45:24 -03:00
Bob.Hou
c4eafaa26d fix(auth): probe a pinned inactive connection after quota top-up (#13017)
The one-shot framing is what makes this safe: a pin is an explicit operator act, so taking the inactive row only for that request, with siblings out of the pool and a 60s per-connection storm gate, keeps the blast radius at one request. Dashboard deactivate staying off is the right carve-out.

Reconciled against the tip after the batch landed: the `chatHelpers.ts` import block conflicted with `buildExhaustionOptions` (#12975) and both imports were kept. 41/41 across the probe suites afterwards.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 20:45:21 -03:00
Bob.Hou
359b9b520b fix(db): self-heal under sustained memory pressure, VACUUM gate, WAL housekeeping (#13011)
A 36-minute outage with a full host-level write-up is the best kind of bug report. The WAL at 2.98 GB being the high-water mark a VACUUM leaves behind — rewriting a 3 GB database after deleting ~125 rows, with auto-checkpoint never shrinking the file and TRUNCATE only every 6 hours — is exactly the kind of thing that is invisible from inside the process, where the V8 heap read 300 MB while RSS was dominated by 1.9 GB of glibc main-heap.

Reconciled against the tip after the batch landed: the `ENVIRONMENT.md` table conflicted with #13035's `OMNIROUTE_SQLJS_WASM_PATH` row and both sides were kept.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 20:45:16 -03:00
Diego Rodrigues de Sa e Souza
0b56765e4c chore(quality): retighten the chat.ts unused-vars suppression to 8 (#13385)
Restores the count #12975 had already tightened.
2026-09-11 20:43:32 -03:00
Diego Rodrigues de Sa e Souza
00f16a2719 chore(quality): drop two stale ESLint suppression entries (#13384)
Unblocks the pre-commit hook; both suppressions had nothing left to suppress.
2026-09-11 20:38:13 -03:00
Diego Rodrigues de Sa e Souza
b85185df59 chore(quality): clear the ESLint errors the 2026-09-11 batch introduced (#13379)
Merged to restore the ESLint gate on the release tip. Verified the errors were absent at ba1ee6617 (pre-batch) and present after, so they belong to today's merges.
2026-09-11 19:46:37 -03:00
Bob.Hou
d2bc688b0f fix(i18n): backfill missing usage.kiloPass strings in 39 locales (#12561) (#13027)
Good archaeology: #12178 added seven strings and only `de` and `vi` ever got them, with `pt-BR` left holding placeholders. Restoring the feature-flag description missing from `pt.json` alone is the kind of thing only a full-catalog diff finds.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:45 -03:00
Bob.Hou
0c2a5f3062 fix(i18n): translate pre-existing __MISSING__ keys for combo.sort and credential health (#12272) (#13034)
Filling 20 sentinels across 39 locales, keeping `{minutes}` intact, and deliberately not rerunning `sync-ui-keys.mjs` because it restamps the sentinel — that last point is the one that usually gets this kind of PR wrong.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:38 -03:00
Bob.Hou
73dee8db43 fix(vision): honor Custom Models vision flag on advertised aliases (#12974)
`parseModel` splitting on the first `/` is the trap, and recovering the custom row for all three id forms without changing `parseModel` is the right way around it — that function has too many callers to widen. Lifting the lookup into `customVisionOverride.ts` because `models.ts` is frozen is the correct response to the cap, not a workaround.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:32 -03:00
Bob.Hou
6788b437f5 fix(combos): keep dashboard combo tests off reasoning and rate limits (#13001)
Correct diagnosis on both counts. A probe that asks a reasoning model to add two numbers with a 2048-token budget will spend the budget thinking and return no text, and `Promise.all` across ten steps against one free-tier pool is a self-inflicted burst. The dashboard reporting `error` on a combo that serves real traffic fine is worse than no probe.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:29 -03:00
Bob.Hou
d92bc8ef7a feat(sse): restore GPT-6 Astra effort aliases and Codex 0.153.4 pin (#13026)
Three related things that genuinely belong together: the registry and suffix splitter not knowing Astra while the live catalog serves it, the Codex client pin Astra requires, and dropping `next/font/google` so a production image build does not reach fonts.googleapis.com.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:25 -03:00
Bob.Hou
f4fd9190b3 fix(catalog): degrade unknown combo targets in LCD modalities (#12613) (#13042)
`Array.every` on modality arrays meant one unknown target wiped `input_modalities` for the entire combo — degrade-not-wipe is the correct semantic for a least-common-denominator. Upserting OpenRouter's live `architecture.input_modalities` without a full-table replace, and only invalidating cache when SQLite reports a change, are both the careful version.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:21 -03:00
Bob.Hou
171421439d fix(dashboard): expose Volcano console cookie field and unwrap error objects (#13107)
Both halves are real. A quota fetcher that needs console cookies with no way to enter them is unusable on any headless or remote install, and `new Error($'{'}object{'}'}` rendering `[object Object]` in a toast is exactly what makes a route-guard rejection unreadable.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:17 -03:00
Bob.Hou
0cd88ec998 fix(models): give Gemini 3.8 Flash its own output spec (#13195)
Straightforward and well-scoped: a live catalog model falling through to the conservative 16384 clamp because `MODEL_SPECS` never got a 3.8 entry. Keeping the fallback for genuinely unknown ids is right, and the note on how this relates to #12663, #12499 and #12672 saved the review.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:13 -03:00
Bob.Hou
9b4210a53f feat(providers): Agnes 3.0 Flash catalog, CN host, live /v1/models (#13120)
Live-verified end to end: 1.5 gone from `GET /v1/models` with a 503, 3.0 answering 200 on a pong probe, and the CN-region host difference across 10 accounts. Aliasing 1.5 to 3.0 through `BUILT_IN_ALIASES` rather than leaving a dead id is the right retirement path, and noting that the #11503 as-is guard must not skip the rewrite is the detail that makes it work.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:10 -03:00
Bob.Hou
9adc2f87d0 fix(dashboard): list and purge leftover gemini-cli rows (#13197)
A catalog drop with no DELETE path is a real dead end for operators — no card, no route, no way out. Good that by-provider delete now bumps the proxy generation and purges synced models the way single-row delete already did, and that a cleanup error there does not turn a successful row delete into a 500.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:05 -03:00
Bob.Hou
aedc506cce fix(docs): keep operator-internal security writeups out of public /docs (#13136)
Security-relevant and the right fix. A Basic gate on the whole `/docs` path was a deploy bandage that also locked LAN operators out of `https://<lan>:20128/docs`; dropping the four operator-internal writeups from the fumadocs glob and the Docker image removes the reason for the gate instead of papering over it. Keeping them in git and citing repo paths from the public pages is the right trade. The `.dockerignore` matcher walking rules in file order with last-match-wins is a genuine correctness fix in the test's own matcher.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:28:02 -03:00
Bob.Hou
49b6c3e59e fix(combo): stop quota-weighted routing onto out-of-credit connections (#13006)
Both gaps are real and they compound: `executeTargetAttempt` already classified the 402 through `isQuotaExhaustionResponse` and then dropped it, so the only writer into the quota cache was the 429 path in `chat.ts`. A snapshot reading `remaining=1%, is_exhausted=0` five hours stale is then exactly what quota-weighted routing will keep picking.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:57 -03:00
Bob.Hou
fc3ce839fe fix(db): resolve sql-wasm.wasm across global and hoisted layouts (#12960) (#13035)
Correct root cause chain, and it explains why this only bites global installs: npm 11 skips `optionalDependencies` install scripts, the server child runs with `cwd: <packageRoot>/dist`, and `sql.js` sits at `<packageRoot>/node_modules`. Probing the parent directory is the missing rung.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:52 -03:00
Bob.Hou
541a6481a9 fix(resilience): classify Cline 401 as refreshable OAuth (#12594) (#13060)
Two real defects in one report, both correctly separated: the missing phrase in `OAUTH_INVALID_TOKEN_SIGNALS` made a recoverable Cline 401 terminal even though `refreshClineToken` already existed, and the cooling panel printing "429 (rate-limit)" for every future cooldown hid what actually happened.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:45 -03:00
Bob.Hou
f0b83b86ba fix(grok-cli): park a 402 on the empty Grok Build login, not grok-4.6 (#13061)
The live evidence makes this: four Grok Build logins at 32-93% weekly remaining, skipped as "model locked by resilience" because the one login at 1% answered 402 and `passthroughModels: true` made that read as a per-model billing miss. A shared weekly wallet is a connection-wide signal, not a model verdict — `isSharedWalletCredits402` puts the scope where the biller put it.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:40 -03:00
Bob.Hou
a31c7880c4 fix(chatcore): restore provider failure classification and credential refresh on non-streaming leg (#13043) (#13069)
This is the one that mattered most. The non-streaming leg returning error outcomes without classification meant 402/429 never reached model lockout or rate-limit bookkeeping, and a 401 on a refreshable provider failed instead of refreshing — on a leg that serves real traffic. Extracting `applyProviderFailureClassification` and wiring both legs through it is the right shape: the asymmetry was the bug, so the fix has to remove the asymmetry rather than patch one side.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:36 -03:00
Bob.Hou
c75e293a47 fix(api): thread X-OmniRoute-Fallback-Attempts through combo chat (#12339) (#13038)
Right call not to reuse the combo loop's `fallbackCount`: it only increments after a leg fails or is skipped, so the second target would still report 0 at dispatch. Stamping the ordered index (or round-robin offset) at the gate is the only place the number is actually known. This PR also carries the batch's file-size rebaseline, since it merges first and the ceiling has to cover every intermediate state.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the other 19 PRs of this batch. Two in-batch conflicts, both additive and resolved by keeping each side: the `ENVIRONMENT.md` table (#13035 + #13011) and the `chatHelpers.ts` import block (#12975 on the tip + #13017).

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, within baseline); `check:changelog-integrity` OK; `check:docs-counts` migrations ✓
- complexity 2816 / baseline 3218 and cognitive-complexity 1271 / baseline 1437 — both under baseline
- 531 of 532 focused assertions green across the batch's 46 test files
- `check-file-size` rebaselined for the batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_houminxi`, landed on #13038), attributed per PR

The single red is **not this batch**: `tests/unit/combo/quota-weighted-strategy.test.ts` → "A/B isolation: 7 hard-empty + 2 at 0.5% + 1 at 40%, floor=1" asserts an order between two connections of identical weight and flakes on the pure tip too — 2 failures in 4 runs at `origin/release/v3.8.51` with nothing from this batch applied.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, untouched here).

Thanks @HouMinXi — the live evidence on these (X500 logs, `storage.sqlite` state, real `/v1/models` probes, the 36-minute outage write-up) is what let a 20-PR batch be reviewed as a unit.
2026-09-11 19:27:33 -03:00
Paco Cartones
ba1ee66174 fix(i18n): quote raw <name> in the auto-sync profiles description (#12549)
Merged for the half that is still live. The 42 locale catalogs were fixed upstream while this sat open — all 51 now carry `'<name>'` — but the TypeScript default at `featureFlagDefinitions.ts:625` still had the raw tag, so the Feature Flags card kept failing to compile wherever the default is the source. Against the current tip this PR lands exactly three files: that one-character fix, the changelog fragment, and your 135-line regression test, with zero locale files touched.

The analysis is what makes it worth keeping. `next-intl` parsing `<name>` as a rich-text tag, `FeatureFlagsGrid.tsx` rendering the description through plain `t()` with no tag element, and the result being `INVALID_MESSAGE: UNCLOSED_TAG` — the card showing the raw key instead of the description, in every language — is a failure mode that is easy to misread as a missing translation. Verifying it against `use-intl`'s `development` build, the one Turbopack dev mode actually loads, is the detail that makes the reproduction trustworthy. Wrapping in ICU single quotes matches what #12369 did for `ccOnboardingKeyPlaceholder`.

5/5 on the regression suite against the tip.

---

⚠️ base-red inherited: #12732.

Thanks @pacocartones — 18 more of yours merged today.
2026-09-11 18:13:18 -03:00
Paco Cartones
374bbe3ee7 fix(i18n): translate the home Recent Requests panel and topology legend (#12551)
The diagnosis is the valuable part here: a verbatim English copy is invisible to every i18n gate — `check-ui-keys-coverage` counts it as covered, `sync-ui-keys` only backfills absent keys, and `check-ui-value-drift` only reacts to English values that change. That is exactly how five keys shipped in #10900 stayed English in 39 catalogs without anything noticing, and a static test asserting "not equal to the English value" is the right instrument for it.

Two things needed reconciling before merge, both pure drift from the time this sat open:

**Nine locales did not exist when you cut this branch** — el, et, ga, hr, lt, lv, mt, sl, sr. They arrived with the Recent Requests keys but not the topology legend ones, so your own first assertion failed on them. I filled the three keys from each catalog's existing approved translations of the same words (`common.active`, `common.recent`, `analytics.modelStatusError`) rather than a fresh translation pass, so the legend reads the same as the rest of that language's dashboard.

**Two cognates were being failed for being correct** — `hr.recentRequestsModel` and `sl.recentRequestsModel` are "Model", which is the right word in Croatian and Slovenian. Your `COGNATES` set already existed for exactly this (`es.topologyLegendError`), but the third assertion swept every locale without consulting it. It does now.

5/5 on the suite afterwards, and the diff stayed at the nine locale files plus the test — no collateral sync.

---

Validated in the consolidated worktree for this batch. `typecheck:core` clean, `check:dashboard-typecheck` OK, complexity and cognitive-complexity under baseline.

⚠️ base-red inherited: #12732 — and separately, `npm run i18n:check` reports 75 doc-translation drift entries across 35 files on the pure tip (40 of them `docs/reference/ENVIRONMENT.md`). That is the docs pipeline, untouched by this PR.

Thanks @pacocartones — 17 more of yours merged today.
2026-09-11 18:09:58 -03:00
Diego Rodrigues de Sa e Souza
3f62e43696 test(compression): assert idle eviction terminates at the resource level (#13371)
Merged as the credit vehicle for #12542. Reverse-TDD verified on the tip: 8/8 with the fix, 7/8 with `terminate()` disabled.
2026-09-11 17:46:22 -03:00
Paco Cartones
16c68bad49 feat(gamification): pay the documented streak and badge XP rewards (#12522)
`XP_REWARDS` documented `streak_bonus` and `badge_unlock` and the pipeline paid neither — closing that gap is right. The idempotency design carries it: `advanceStreak()` reporting `extended` only on the call that moves the record to today is what keeps a same-day repeat from paying twice.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:32 -03:00
Paco Cartones
6caf836092 fix(providers): include Agnes model in video polling (#12356)
Straightforward and complete: polling by `video_id` without `model_name` could not identify the job, and URL-encoding in the shared builder covers the custom-provider preset as well as the built-in.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:28 -03:00
Paco Cartones
7cd2fab253 fix(routing): honor edited custom-node API type (#12358)
Forwarding only an explicit custom-model DB override as `modelInfo.targetFormat` is the key distinction — it lets chat core's credential-aware resolution pick the live connection setting instead of the format baked into the node id at creation, which is exactly what #11884 was about. I rebaselined the integration test file for the new case.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:25 -03:00
Paco Cartones
afd2d993e6 fix(rerank): clamp Voyage top_k and honor NVIDIA return_documents (#12523)
Both defects are real adapter bugs: `top_k` computed from the unfiltered array after the adapter drops empty strings makes Voyage reject a request that is valid under the Cohere-style contract this endpoint exposes. Good that the NVIDIA `return_documents` half rides along rather than waiting.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:21 -03:00
Paco Cartones
e09cb5a768 chore(lifecycle): gate DEFAULT_DEGRADATION_MAP against retired ids (#12535)
Third hand-maintained table naming model ids and the only one outside the retired-model gate — extending `check-model-lifecycle.mjs` to cover it is the durable fix, and the three retired rows it flushed out were already dead code behind the 410 `model_shutdown` answer.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:16 -03:00
Paco Cartones
0831d487c0 fix(audio): resolve combo names on /v1/audio/translations (#12536)
Translations was the one audio route the combo-resolution fixes never reached, so the same combo name worked on `/v1/audio/transcriptions` and failed here. Following the #9382 shape rather than inventing a new one is the right call.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:12 -03:00
Paco Cartones
b23b0ca68e fix(gemini): strip prefixItems from Gemini tool schemas (#12540)
High-impact and precisely diagnosed: `prefixItems` missing from the strip-list rejected every tool-bearing Claude Code request routed to Gemini before generation, because Claude Code's built-in tools describe line ranges as tuples. All three tool shapes going through the same cleaner is what makes the one-key fix sufficient.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:08 -03:00
Paco Cartones
7ad5e1120e fix(api-manager): accessible loading status on the skeleton gate (#12541)
A real accessibility hole, not a cosmetic one: for the whole loading window the page exposed nothing but the sidebar, which is exactly the "API Keys link does nothing" report. Reusing the `role="status" aria-live="polite"` container the other dashboard loading states already use keeps it consistent.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:05 -03:00
Paco Cartones
a606df0b5d fix(video): clamp estimateJpegFrameBytes, reuse the shared JPEG prefix (#12543)
A documented byte estimate should never come back negative, even where today's usage makes it harmless — `Math.max(0, …)` is the honest floor. Replacing the three hardcoded `data:image/jpeg;base64,` literals with the shared constant is byte-identical and removes three chances to drift.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:42:00 -03:00
Paco Cartones
00421fde0c fix(devin): accept Windows sandbox paths in the agentic home check (#12545)
`DEVIN_AGENTIC_HOME` was unusable on every Windows host whatever its value, because a forward-slash `.sandbox` check can never match a backslash path. Normalizing separators before comparing, the same way `normalizeCommandToken` does, is the consistent fix; keeping `path.isAbsolute()` untouched keeps the guard intact.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:56 -03:00
Paco Cartones
02884ed8d2 fix(db): auto-clean conversation_turn_nodes and orphaned conversations (#12548)
Two tables with no retention path at all and 775 MB of a 1.1 GB database is a real operational failure. Tying them to the existing `retention.callLogs` window rather than inventing a knob is right, and the reasoning is what makes it safe: once `cleanupCallLogs` purges the row `last_correlation_id` points at, the node can never render again.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:53 -03:00
Paco Cartones
3198c54146 fix(orchestration): emit the real task status on non-status updates (#12550)
Correct: `state: "updated"` mapped to no `OrchState`, so the channel carried a value no consumer could interpret. Reading the row back only on the no-status path, and publishing nothing when no row matched, both match what the A2A side already does.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:48 -03:00
Paco Cartones
2e10346f45 docs(reference): sync FEATURE_FLAGS.md with featureFlagDefinitions (#12552)
The doc promised a 1:1 catalog and had drifted to 20 missing keys, two rows that were not flags at all, and a port number that disagreed with the code. Good call adding a static sync test — it caught its own drift immediately: I added the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after you wrote this.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:43 -03:00
Paco Cartones
616d54cf19 fix(config): persist background-degradation entry deletions (#12647)
The tell is convincing: `detectionPatterns` in the same function already treats a present stored value as authoritative, so the two halves of one object disagreed. Making `degradationMap` stored-authoritative-when-present is the smaller change and the consistent one.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:37 -03:00
Paco Cartones
a0c52ba54d fix(images): fall through combo edit targets on /v1/images/edits (#12653)
The asymmetry was the bug: `/generations` iterated every image-capable target while `/edits` only ever tried the first. Extracting the iteration into `runImageComboTargets` and proving `/generations` byte-identical before wiring `/edits` onto it is the right order. Missing credentials skipping rather than hard-401 now matches generations too.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:33 -03:00
Paco Cartones
4309a2fd56 fix(antigravity): preserve thought token usage (#13055)
Right fix: `thoughtsTokenCount` is real output the caller paid for, so folding it into `completion_tokens` and surfacing it as `completion_tokens_details.reasoning_tokens` matches what every other reasoning-capable provider reports.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:29 -03:00
Paco Cartones
abe234e094 fix(gamification): durable action-count badges that survive xp_audit_log pruning (#12651)
Genuinely subtle: counting over `xp_audit_log` meant a 30-day prune silently redefined "lifetime" milestones as "last 30 days", so a key doing 900 req/month could never reach Token Consumer. A durable counter backfilled on migration, mirroring the `user_levels.total_xp` pattern, is the right shape. I renumbered the migration to 176 — 173 was taken by a migration that landed after you opened this — and synced the doc count, which the operator approved.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded with the rest of this batch — zero conflicts between the 19 PRs.

- `typecheck:core` clean; `check:dashboard-typecheck` OK (206 pre-existing, all within the frozen baseline); `check:changelog-integrity` OK
- complexity 2802 / baseline 3218 and cognitive-complexity 1267 / baseline 1437 — both under baseline
- 226 of 228 focused assertions green across the batch's 23 test files. The 2 remaining belong to #12551, which is held separately.

Two batch-owned defects were found and fixed in flight, both pure base drift: `173_xp_action_counts.sql` collided with `173_call_logs_video_content_removed.sql` (renumbered to 176 on #12651 — it aborted every DB open, which is what 53 of the first run's failures were), and the feature-flag catalog was missing the `SERVER_OWNED_TOOL_LOOP_ENABLED` row the base gained after #12552 was written.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` reproduce on the pure tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098, which this batch does not touch).

Thanks @pacocartones — the `file:line` citations and the explicit out-of-scope notes on every one of these made a 19-PR batch reviewable in one pass.
2026-09-11 17:41:25 -03:00
Dizzle
cfa2fc7548 fix(sse): rotate opencode accounts on transient 5xx (#12975)
Reconciled and merged. This branch was stacked on #12941, which has since landed, so it read as 1484 additions across 16 files and CONFLICTING. I merged the current `release/v3.8.51` into it rather than rewriting your branch: `open-sse/executors/opencode.ts` conflicted in six places where your side was a strict superset of the squashed #12941, and the tip had touched that file through nothing but #12941, so your side was taken whole. The PR now reads as its real 12 files, +839/-25.

The change itself is right: a flapping upstream 5xx aborting the whole agentic chain is exactly the case where rotating to the next healthy account is safe, and keeping it a separate arm from the 400-empty branch matters because that one has to clone-read the body while this one never touches it. Threading `correlationId` through so interleaved requests stay attributable — and never fabricating one when absent — is the right discipline.

Both geo-block regression suites pass alongside the new ones (43/43 across the five opencode test files), which is what proves the conflict resolution preserved #12941's behaviour.

I also tightened the batch's file-size rebaseline here: `src/sse/handlers/chat.ts` needed no bump at all (it lands at 2452, under its existing 2458 freeze) and `open-sse/executors/base.ts` needed only your +2. An earlier measurement had included a local prettier reformat that is not part of this branch.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the rest of this batch — zero conflicts between them.

- `typecheck:core` clean; `check:changelog-integrity` OK
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 86 focused assertions green across the batch's 10 unit test files, plus 16/16 on the v1 plugin option schema and 16/16 on the v2 option tests
- `check-file-size` rebaselined for this batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode`, landed on #13141). `open-sse/utils/stream.ts` was deliberately left frozen: it is already 3115 > 3098 on the pure tip with zero contribution from this batch.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and the `stream.ts` freeze above). None of them touch these diffs.

Thanks @maxmad64bis.
2026-09-11 13:57:28 -03:00
Dizzle
a19bb2227f fix(providers): lock opencode model on upstream 400 model-unavailable (#13146)
Scoping the lock to the MODEL rather than the connection is the right layer for a `400 "Model is unavailable"` — a multi-day upstream outage on one model should not darken the account. Good discipline on the two allowlist-adjacent changes: gating the `markAccountUnavailable` branch on `ruleScope === "model"` AND `status === 400` leaves every other status on its existing path, and deliberately not widening `FULL_TEXT_RULE_PROVIDERS` keeps the #10880 egress-bucketed 429 classification intact. Reading the cooldown from the rule instead of a literal at the call site is what makes it self-healing.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the rest of this batch — zero conflicts between them.

- `typecheck:core` clean; `check:changelog-integrity` OK
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 86 focused assertions green across the batch's 10 unit test files, plus 16/16 on the v1 plugin option schema and 16/16 on the v2 option tests
- `check-file-size` rebaselined for this batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode`, landed on #13141). `open-sse/utils/stream.ts` was deliberately left frozen: it is already 3115 > 3098 on the pure tip with zero contribution from this batch.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and the `stream.ts` freeze above). None of them touch these diffs.

Thanks @maxmad64bis.
2026-09-11 13:51:24 -03:00
Dizzle
cc4f7ed1c4 fix(logging): prefer the pipeline over the raw bodies in call-log storage and detail enrichment (#13147)
Both halves are right, and shipping them together is justified: the ladder dropping `pipeline` first threw away the upstream answer while keeping a prompt it already had a copy of, and `resolvePreviousResponseState` rebuilds continuation history out of exactly that field. The detail-panel fix has to ride along because the size-limit placeholder is a non-empty string, so fixing the ladder alone would let it overwrite a good payload. Re-checking emptiness per side after reading is the actual bug — `responseBody` being one value for both sides is what let a provider payload show as the client response.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the rest of this batch — zero conflicts between them.

- `typecheck:core` clean; `check:changelog-integrity` OK
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 86 focused assertions green across the batch's 10 unit test files, plus 16/16 on the v1 plugin option schema and 16/16 on the v2 option tests
- `check-file-size` rebaselined for this batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode`, landed on #13141). `open-sse/utils/stream.ts` was deliberately left frozen: it is already 3115 > 3098 on the pure tip with zero contribution from this batch.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and the `stream.ts` freeze above). None of them touch these diffs.

Thanks @maxmad64bis.
2026-09-11 13:51:21 -03:00
Dizzle
3156643f6c fix(opencode): require an http(s) baseURL in both OpenCode plugins (#13142)
Real and nasty precisely because it is silent: `z.string().url()` accepts `localhost:20128` as scheme `localhost:` plus a path, every model gets published with an unusable api url, and the failure happens inside the client so the gateway logs show nothing. Backing the option schema, the publish boundary and the snapshot filter with one `isHttpUrl` in v2 is the right call — those three cannot drift apart. Duplicating the predicate in v1 rather than sharing it is also correct, since the two packages ship independently.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the rest of this batch — zero conflicts between them.

- `typecheck:core` clean; `check:changelog-integrity` OK
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 86 focused assertions green across the batch's 10 unit test files, plus 16/16 on the v1 plugin option schema and 16/16 on the v2 option tests
- `check-file-size` rebaselined for this batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode`, landed on #13141). `open-sse/utils/stream.ts` was deliberately left frozen: it is already 3115 > 3098 on the pure tip with zero contribution from this batch.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and the `stream.ts` freeze above). None of them touch these diffs.

Thanks @maxmad64bis.
2026-09-11 13:51:17 -03:00
Dizzle
9f0d54a48f fix(combo): parse numeric-epoch rateLimitedUntil in hasFutureRateLimitUntil (#13141)
Correct and well-traced: `rate_limited_until` is TEXT but one write path stores a bare epoch that SQLite coerces to `"1781696905131.0"`, which `new Date()` alone reads as `NaN` — so a still-cooling connection looked available and combo fed it traffic that could only come back 429. Routing both readers through the existing tolerant normalizer is the minimal fix, and keeping unreadable values fail-open is the right default. The #3954/#3995 lineage explains exactly why this function never inherited the normalization.

This PR also carries the batch's file-size rebaseline, since it merges first and the ceiling has to cover every intermediate state.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the rest of this batch — zero conflicts between them.

- `typecheck:core` clean; `check:changelog-integrity` OK
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 86 focused assertions green across the batch's 10 unit test files, plus 16/16 on the v1 plugin option schema and 16/16 on the v2 option tests
- `check-file-size` rebaselined for this batch's real growth (annotation `_rebaseline_2026_09_11_mergebatch_v3851_maxmad_opencode`, landed on #13141). `open-sse/utils/stream.ts` was deliberately left frozen: it is already 3115 > 3098 on the pure tip with zero contribution from this batch.

⚠️ base-red inherited: #12732 — `Docs Gates`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and the `stream.ts` freeze above). None of them touch these diffs.

Thanks @maxmad64bis.
2026-09-11 13:51:13 -03:00
anhtahaylove
20abd89d7c fix(acp): bound session output buffers and reset stderr per prompt (#13100)
Keeping the tail is the right direction — `sendPrompt` resolves with the stdout collected since the prompt was written and stderr is read for diagnostics after a failure, so the newest output is what callers actually use. The `[...output truncated...]` marker keeps it from being silent. Resetting `stderrBuffer` alongside `stdoutBuffer` fixes the subtler half: diagnostics for one prompt were carrying stale output from every earlier one.

Verified on the tree that actually ships — your branch merged onto the current tip, which already carries #13096: `appendCapped()` and `settle()` coexist cleanly and all 7 assertions across both ACP test files pass together.

I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:32:51 -03:00
anhtahaylove
e1cfdb5e48 fix(acp): release listeners, timers and sessions on every sendPrompt outcome (#13096)
`acpManager` being a module-level singleton is what turns this from a per-call leak into unbounded growth — the `MaxListenersExceededWarning` at 11 is the visible symptom. Routing every outcome through one `settle()` is the right shape, and deleting the session on the child's own exit fixes the map growth that `getActiveSessions()`'s `alive` filter was hiding.


I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:30:44 -03:00
anhtahaylove
d61b1727cd fix(cli-helper): clear the log stream timeout on the abort path (#13114)
`stop()` is the normal lifecycle for a `follow: true` stream, not an edge case, so the `signal.aborted` early return skipping `clearTimeout` leaked one armed timer per stop. Cancelling the reader on the early loop exit closes the second half.


I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:30:38 -03:00
anhtahaylove
99fb441434 fix(db): release process listeners when a node:sqlite adapter closes (#13109)
Right precedent — #7494 fixed exactly this for the sql.js adapter and the `node:sqlite` one never got the same treatment, even though it is the default driver whenever better-sqlite3 is unavailable. `/api/db-backups/import` opening a throwaway adapter per request makes it reachable.


I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:30:32 -03:00
anhtahaylove
67618978b0 fix(gamification): close the badge SSE stream when the signal is already aborted (#13106)
Correct: an `abort` listener registered on an already-aborted signal never fires, and `safeEnqueue` can't save it because enqueuing into an unread stream only buffers. The abort-later test earning its keep as a guard on the healthy path is the right instinct.


I reformatted the changelog fragment to the `changelog.d` convention (`- **fix(scope):** …`) before merging — `check:changelog-integrity` rejects a fragment that does not start with a markdown bullet, which is the same gate your #13158 was about. Wording is yours, unchanged in substance.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:30:27 -03:00
anhtahaylove
85a5126dba fix(compression): terminate idle workers on eviction (#13091)
`remove(slot, false)` dropped the slot without `terminate()`, leaving the OS thread and its heap alive — invisible to RSS, which is why 55 orphaned `MessagePort`s took 16 h to surface. Removing the parameter rather than keeping it is the correct call: the pool was its only owner.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:45 -03:00
anhtahaylove
b1733d3c83 fix(plugins): make SIGKILL escalation idempotent per child (#13092)
`once` only detaches when exit actually fires, so a plugin trapping SIGTERM accumulated one listener and one timer per hook timeout. Keying idempotence on the child via a `WeakSet` is right — a second SIGKILL timer would only re-signal a corpse.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:42 -03:00
anhtahaylove
66330cc724 fix(compression): pass a URL object when spawning the LLMLingua worker (#13093)
`node:worker_threads` only treats a `URL` instance as a `file:` URL — a string must be a relative path. The silent `catch {}` in `pump()` made this degrade compression to a passthrough while still reporting success, which is the worst shape for it.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:38 -03:00
anhtahaylove
a3fa6cf524 fix(traffic-inspector): release WS subscriber and ping timer on a dead socket (#13155)
Attaching `close`/`error` before subscribing closes the window where a resource is held with no live cleanup path, and the destroyed-socket re-check after the handshake covers the in-flight case. `write()` not throwing synchronously is exactly why the old `try/catch` never fired.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:34 -03:00
anhtahaylove
30c96d43a5 fix(telegram): bound the per-user API key cache (#13166)
`resolveUserApiKey()` keyed an uncapped Map on an id taken straight from the webhook body. The LRU's recency test is what keeps this from regressing into a clear-when-full cache.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:30 -03:00
anhtahaylove
658153c7b0 fix(stream): release the upstream body when the JSON-to-SSE sniff times out (#13171)
The sniff loop abandoned the upstream reader when `withBodyTimeout()` rejected. The `handedOff` flag correctly spares the two success paths from cancellation; the second test guards that direction.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:26 -03:00
anhtahaylove
a1b9b02d5d fix(telegram): authenticate webhook deliveries with the Telegram secret token (#13175)
Real abuse vector: the bot-webhook branch reached `proxyChat()` — which mints an API key and spends upstream quota — with nothing proving the caller was Telegram. Fail-closed 503 when the secret is unset is the right default.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:23 -03:00
anhtahaylove
178d25250a fix(test): cap local unit-test concurrency at 4 to avoid exhausting commit charge (#13187)
Aligns the two hand-typed local scripts with `test:unit:ci`, which already ran at concurrency 4; `--test-force-exit` was likewise the one flag `test` was missing. CI scripts are untouched.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:18 -03:00
anhtahaylove
edfcb8be17 fix(test): resolve the WebDAV handler path with fileURLToPath (#13196)
Correct: `URL.pathname` is a URL path, so on Windows it yields `/C:/...` and `path.resolve` produces the doubled `C:\C:\` prefix. `fileURLToPath` is the right decoder and also un-escapes `%20`. All 37 WebDAV tests green here.

---

Validated in one consolidated worktree cut from `release/v3.8.51`, boarded together with the other 13 PRs of this batch — zero merge conflicts between them.

- `typecheck:core` clean
- complexity 2799 / baseline 3218 and cognitive-complexity 1265 / baseline 1437 — both under baseline
- 71 focused assertions green across the 13 test files this batch adds or touches

⚠️ base-red inherited: #12732 — `Docs Gates (fast-path)`, `Merge integrity`, `No new ESLint warnings`, `Unit Tests fast-path` and `Fast Quality Gates` all reproduce on the pure `release/v3.8.51` tip (provider count 356 vs the 358 the modules define, SKILL.md drift, and `open-sse/utils/stream.ts` at 3115 > frozen 3098). None of them touch this diff.

Thanks @anhtahaylove — the root-cause write-up, the measured before/after numbers and the red-before-green proof on every one of these made the batch reviewable as a unit.
2026-09-11 13:27:14 -03:00
Nguyen Thanh Dat
af49d4972e fix(stream): accept the buffer size glm.ts has been passing since #12179 (#12925)
Rebased onto the tip and completed, per the maintainer's call to finish the wiring rather than merge the capability alone.

What changed since your version:

The tip had already cleared the TS2554 by deleting the 16th argument, leaving a comment that the highWaterMark stays at the helper default. So the base-red you found is gone, but the 64 KB #12179 asked for was still not applied and your new parameter had no caller. glm.ts now passes it, which is what turns the capability into the fix.

Your test file also hung the runner: every stream createSSEStream builds arms a 10s idle watchdog via setInterval in start, and nothing cancelled them, so node:test waited on a non-empty event loop long after the assertions passed. Cancelling each readable in an after hook runs the cancel handler that clears the timer — the file now reports in about 7 seconds. Worth knowing for future stream tests.

Your five assertions are unchanged and all pass. Reading the writable's desiredSize to measure the queue budget the stream was actually built with, rather than standing in for it, is the detail that makes this testable at all — and the 0-budget case pinning `??` against `||` is the kind of thing that silently rots otherwise.

Thank you also for separating your own red checks from the base's and reporting what you found there. That is how #12919's identical failures got explained instead of chased.
2026-09-10 18:25:31 -03:00
Nguyen Thanh Dat
22473dee50 feat(providers): add EURouter as an OpenAI-compatible gateway (#12985) (#13025)
Rebased onto the release tip after #13024 landed: both PRs extend the same three registration files, so the sibling merge turned this into a conflict. The resolution is additive — both catalog entries kept, both registry imports kept, both base URLs kept — and EURouter stays in AGGREGATOR_PROVIDER_IDS while GreenPT stays out, exactly as each PR argued. 14 provider tests pass on the rebased branch and the file-size gate is green under the annotated rebaseline.

Thank you for re-checking the endpoint live instead of trusting the report, and for the sovereignty caveat. Naming the upstreams from EURouter's own catalog — Claude Sonnet served by AWS Bedrock, 19 models owned by openai — and then writing an apiHint that says routing rather than residency is the kind of care that keeps a provider entry honest. The test asserting the copy contains none of "residency", "stays in the EU", "EU-hosted" or "sovereign" is a good guard against that drifting later.
2026-09-10 18:16:32 -03:00
Nguyen Thanh Dat
2b9e7fb3ec feat(providers): add GreenPT as an OpenAI-compatible provider (#13024)
Merged with a rebaseline commit added on top of your branch: check:file-size freezes the gateways catalog at 1462 lines, so any new entry fails the gate on arrival. The annotation covers this entry and EURouter's (#13025) together, following the route every previous gateway entry took (#11786 seekai, #10987 logfare, #10668 tabitoken, #10531 freebuff, #11631 1min.ai) — the file is declarative data already split into six family files, so splitting it for two entries would break the semantic-families rule.

Validated in a combined worktree with 13 sibling PRs: 132 focused tests pass, typecheck:core clean, file-size green after the rebaseline.

Thank you for stating plainly what you did not verify. "The endpoint exists and is key-gated; catalog, streaming and tool calls not exercised" is worth more than a confident entry that turns out to be guesswork, and the conservative entry that follows from it — empty models, no capability declared, hasFree false with the billing shape spelled out — is exactly right.
2026-09-10 18:14:04 -03:00
Nguyen Thanh Dat
9a56147019 fix(skills): read positionals declared with .addArgument() (#13009)
Approved by the maintainer for the agent-instruction surface it touches: the SKILL.md change is regenerated output from the corrected parser (`resilience set` -> `resilience set <name>`), restoring the required argument the published page had been hiding. No hand-written directive was added.

Boarded with 13 sibling PRs and validated as a set: 132 focused tests pass, typecheck:core clean, changelog integrity and file-size gates green.

Thank you — the table contrasting the declared argument against the published page is what made the second case (an agent told to run `resilience set` with no argument) visible as more than cosmetic.
2026-09-10 18:13:37 -03:00
Nguyen Thanh Dat
751247a143 fix(security): scan both ends of an oversized body, not just the front (#13104)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:20 -03:00
Nguyen Thanh Dat
567abb5d68 fix(security): scan the text a tool_result carries (#13101)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:16 -03:00
Nguyen Thanh Dat
403a1a697d fix(guardrails): mask PII inside a tool_result's nested content (#12930)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:12 -03:00
Nguyen Thanh Dat
f2d5728cfd fix(dashboard): test Responses nodes on /v1/responses, not chat completions (#13070) (#13087)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:08 -03:00
Nguyen Thanh Dat
0a314c84de fix(translator): treat contentSchema and unevaluatedItems as schema slots (#13110)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:05 -03:00
Nguyen Thanh Dat
1929aa656a fix(validation): accept a null dailyQuotaResetTimezone (#13066) (#13083)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:13:01 -03:00
Nguyen Thanh Dat
4edc3d57d0 fix(azure): match the generation, not one release, for max_completion_tokens (#13007)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:57 -03:00
Nguyen Thanh Dat
a6f28210de fix(logs): match the in-memory call-log filter to the SQL one it re-applies (#12896)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:54 -03:00
Nguyen Thanh Dat
5df94f8b05 fix(bedrock): resolve context limits for every vendor prefix, not just anthropic (#12921)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:49 -03:00
Nguyen Thanh Dat
e1a1290fde fix(compression): keep tool_result blocks first when aging annotates a turn (#12920)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:45 -03:00
Nguyen Thanh Dat
ee21e7d2c9 fix(a2a): build the status agent card from the request that asked for it (#12918)
Boarded with 13 sibling PRs into one worktree off release/v3.8.51 and validated as a set: 132 focused tests pass across all 15 test files in the batch, typecheck:core is clean, check-changelog-integrity reports no lost base bullets, and check-file-size is green. Your PR merged without conflict against its siblings.

Thank you — the write-up made this reviewable: measuring the behaviour on the release tip and showing the before/after table meant the defect could be confirmed rather than taken on faith.
2026-09-10 18:12:41 -03:00
Diego Rodrigues de Sa e Souza
fd27ff08c7 chore(deps): drain the Dependabot queue — 10 of 13 alerts (#13213)
* chore(deps): drain the Dependabot queue — 7 of 10 alerts

Lockfile-only bumps; no manifest touched, so nothing changes for consumers.

Root package-lock.json:
  hono      4.13.0 -> 4.13.7  (#215 #216 #217, medium, patched 4.13.5)
  csv-parse 7.0.1  -> 7.0.2   (#213, medium)
  joi       18.2.3 -> 18.2.8  (#211 #212, low, patched 18.2.4/18.2.5)

@omniroute/opencode-plugin:
  toml      4.1.1  -> 4.3.0   (#209, HIGH, patched 4.1.2)

@omniroute/opencode-plugin-v2:
  esbuild   0.28.1 -> 0.28.2  (#210, low) — the direct copy only; see below.

The plugin-v2 diff looks large but is one package: esbuild ships 27 platform
binaries, each carrying version + resolved + integrity.

Three alerts stay open, deliberately:

  #218 extract-zip (HIGH) and #214 adm-zip (medium) have NO published patch.
  Both are dev-scope. Closing them needs an upstream release or a decision to
  replace the dependency — neither belongs in a lockfile bump.

  #210 esbuild is only half-closed. `node_modules/esbuild` is on 0.28.2, but
  `tsup` pins `esbuild: ^0.27.0`, so its nested copy stays at 0.27.7 — inside the
  vulnerable range (>= 0.27.3, < 0.28.1). Updating tsup does not move it (8.5.1
  is already current). Forcing it would take an `overrides` entry pushing a major
  of esbuild inside the bundler, which is exactly the change that breaks a build
  silently, for a LOW dev-only alert. Left for an upstream tsup release.

check:lockfile passes on all three, including the workspace lock/manifest
consistency check. check:tracked-artifacts OK.

* chore(deps): bump js-yaml to 4.3.2 (root + electron)

Two more HIGH alerts arrived after the first sweep:

  #220 js-yaml (root package-lock.json)     >= 4.0.0, < 4.3.2
  #219 js-yaml (electron/package-lock.json) >= 4.0.0, < 4.3.2

The root's own js-yaml was already on 5.4.1; the vulnerable copies were the ones
nested under @yarnpkg/parsers, lockfile-lint, xmlbuilder2 (root) and the direct
dependency in electron. All now 4.3.2. Four version lines, nothing else.

#221 smol-toml (HIGH, <= 1.7.0) is NOT closed here. The root is on 1.8.0; the
vulnerable 1.6.1 sits under @openai/codex-security, which pins it as an EXACT
version rather than a range, so `npm update` cannot move it. Bumping
codex-security itself (0.1.24 -> 0.1.26) does not help — 0.1.26 pins the same
1.6.1 — so that bump was reverted rather than carried along for no benefit.

Closing #221 needs an upstream codex-security release or an `overrides` entry,
the same trade already declined for #210/tsup: forcing a transitive pin from
outside is how a build breaks silently. Note that @openai/codex-security is also
the package carrying the unpatched extract-zip (#218), so one upstream release
would likely clear both.

* chore(deps): override smol-toml to 1.8.0 and raise the js-yaml floor

Closes #221 (smol-toml, HIGH, DoS via malformed TOML, vulnerable <= 1.7.0).

@openai/codex-security pins smol-toml at 1.6.1 as an EXACT version, so no
`npm update` reaches it. This repo already uses `overrides` as its standard tool
for exactly that situation — the block carries 20+ entries, including the
scoped-by-parent form and the `qs`/`fast-uri`/`ip-address` entries that back
earlier security bumps — so a scoped override is the idiomatic fix here, not a
new mechanism:

    "@openai/codex-security": { "smol-toml": "^1.8.0" }

The nested copy deduplicates to the root's existing 1.8.0, which two other
consumers (the root itself and knip) already run, so the version is proven in
this tree. The whole lockfile diff is the 14 lines of the removed 1.6.1 entry.

Also raised the `@yarnpkg/parsers` js-yaml floor from ^4.3.1 to ^4.3.2, so the
override documents the patched version rather than permitting the vulnerable one
it was written against.

Not fixed, and not fixable by version — verified against the npm registry rather
than trusting the advisory metadata:

  #218 extract-zip — latest published IS 2.0.1, the vulnerable version. Dev
       scope, via @openai/codex-security. No release to move to.
  #214 adm-zip — latest published IS 0.6.0, the top of the vulnerable range
       (>= 0.5.9, <= 0.6.0). RUNTIME scope, via onnxruntime-node's ^0.5.16, and
       the repo already overrides adm-zip to ^0.6.0. No release to move to.

Both need an upstream fix or a decision to replace the dependency; neither is a
lockfile change. adm-zip being runtime rather than dev makes it the one worth
tracking.

#210 esbuild stays open too. A flat `overrides: { esbuild: ^0.28.2 }` in
opencode-plugin-v2 does close it — npm then reports 0 vulnerabilities — but it
requires regenerating that lockfile from scratch: 823 lines, 96 packages moved,
for a LOW dev-only alert, and a major esbuild bump inside tsup cannot be
validated here without a real install of that package. Tried, measured,
reverted. Left for an upstream tsup release.

check:lockfile OK on all lockfiles including the workspace consistency check;
check:tracked-artifacts OK; prettier clean.
2026-09-10 13:37:55 -03:00
Diego Rodrigues de Sa e Souza
0549dcfc36 fix(api): scope batch bulk-delete to the calling API key (#13211)
GHSA-wvxc-jp3v-5mg5: `DELETE /api/v1/batches/delete-completed` deleted the
completed batches of EVERY api key on the instance and nulled the contents of
every file those batches referenced. Any ordinary inference key reached it —
including one with `scopes: []` — and no victim key, batch id or file id was
needed.

Two defects stacked in one endpoint:

  - `deleteCompletedBatches()` carried no `api_key_id` predicate. The file
    SELECT, the checkpoint DELETE and the batch DELETE were all instance-wide.
  - The route only checked that SOME key was present (`!scope.apiKeyId` → 401),
    never that the caller owned anything, and called the helper bare.

The helper now takes `apiKeyId` and scopes all three statements to it; the route
passes the caller's key and omits it only for session auth, so the operator's own
dashboard keeps its instance-wide cleanup and an API key clears only its own
completed batches.

None of this is a new pattern. `listBatches(apiKeyId?)` and
`countBatches(apiKeyId?)` in the same module already scope by `api_key_id`, and
`batches/[id]/route.ts` already gates per-record access with `scopeCheck` —
session auth sees everything, a key sees only its own. This one helper was the
one that never got it, which is why the fix reuses the shape instead of
inventing a second convention.

tests/unit/batch-delete-completed-ownership-wvxc.test.ts — 5 tests, 4 red before
the fix, including the two that prove the cross-tenant destruction (another
key's batch survives; another key's file content survives). It also pins the
instance-wide dashboard sweep so the fix cannot be "tightened" into breaking the
operator's own cleanup, and a source guard that the route never calls the helper
bare again.

Reported privately via GHSA-wvxc-jp3v-5mg5.

Closes GHSA-wvxc-jp3v-5mg5
2026-09-10 13:27:10 -03:00
Diego Rodrigues de Sa e Souza
393cfdd660 fix(test): make the ToS heading guard actually require the parentheses (#13228)
CodeQL js/useless-regexp-character-escape (#994-#997) on one line, and it is a
real defect rather than the usual query noise.

The assertion built its pattern in a TEMPLATE literal:

    new RegExp(`\(\s*${String(tos?.actual)}\s*\)`)

JavaScript resolves the escapes before RegExp ever sees the string: `\(` becomes
"(" and `\s` becomes the LETTER "s". The compiled pattern was `(s*16s*)` — a
capture group around optional "s" characters — so it matched any heading merely
CONTAINING the number. The literal parentheses this guard exists to require were
never checked, and it passed on exactly the headings it was written to reject:

    /(s*16s*)/.test("### Caution — clauses worth checking 16")   // true

Doubled the backslashes so they survive the template literal, and routed the
interpolated value through an `escapeRegExp` helper — the count is a number
today, but interpolating an unescaped value into a regex source is the same
class of bug one refactor away.

Added a second test that pins the behaviour rather than the spelling: the
pattern must REJECT a heading carrying the count without parentheses, and accept
it with them (including inner whitespace). Before this fix that test fails.

4/4 green against the real docs/reference/FREE_TIERS.md heading.
2026-09-10 13:27:02 -03:00
Diego Rodrigues de Sa e Souza
d86cf75aef fix(quality): register 4 drifted covering tests in stryker tap.testFiles (#13229)
`check:mutation-test-coverage --strict` has been failing Fast Quality Gates on
every open PR against release/v3.8.51. It grew from 2 missing entries to 5 in
roughly an hour, so it is drifting faster than PRs land.

Four test files cover a mutated module without being listed, so their mutant
kills do not count:

  open-sse/services/accountFallback.ts      <- openai-compatible-per-upstream-402-health
  src/sse/services/auth.ts                  <- openai-compatible-per-upstream-402-health
                                            <- quota-window-label
  src/shared/utils/circuitBreaker.ts        <- combo/execute-target-gates
  open-sse/services/combo/comboStructure.ts <- combo-pin-implicit-allowlist

Registration only — no test or module is touched, and no gate is weakened; the
listing is what makes those kills count in the first place.

Inserted in place, never through a JSON round-trip: re-serializing this file
reorders the ~10 curated entries that are already out of alphabetical order
(learned the hard way in #11438).

check:mutation-test-coverage now reports no drift. check:tracked-artifacts OK,
prettier clean.

Worth noting for whoever adds the next test: this gate fires whenever a NEW test
happens to cover one of the 31 mutated modules, which is easy to do without
realising. Registering it in the same commit is cheaper than a CI round-trip.
2026-09-10 13:26:53 -03:00
Dizzle
81bf3cc36e docs(checks): keep doc counts honest — headings, rankings, catalog, weights, quality gate and scoring diagram now covered (#12507)
Estender o gate de contagens para headings, rankings, catálogo, pesos, quality gate e o diagrama de scoring é exatamente o tipo de trabalho que evita a classe inteira em vez de um caso.

Falo por experiência desta campanha: o `check:docs-counts` caiu **duas vezes** hoje pela mesma causa — contagem de migration escrita à mão em três arquivos mais 41 mirrors, desatualizando a cada migration nova (#12970 e #13209). Cada superfície que este PR passa a cobrir é uma que deixa de virar base-red na mão de quem vier depois.

Revalidei sobre o tip: **19/19**, `check:docs-counts-sync` com 0 drifts, `check:docs-all` PASS, `check:doc-links` PASS.

**Integração:** dois conflitos.

1. `scripts/check/check-docs-counts-sync.mjs` — o bloco de leitura de fatos conflitou com os imports de free-tier que entraram pelo #12786/#12744 nesta campanha. Aditivo, os dois conjuntos ficaram.
2. `docs/diagrams/auto-combo-scoring.mmd` — o seu rótulo dizia `reliability (0.0000)`, mas o #12731 mergeou horas antes e passou a dar peso de reliability a todo mode pack. Ficou o rótulo do tip, `reliability (0.0000 DEFAULT, 0.03 packs, 0.04 reliable)`, que é o número real agora.
2026-09-10 10:54:38 -03:00
Markus Hartung
955b28ef5c feat(dashboard): badge a conversation that never reached a clean stop (#12717)
Uma conversa que nunca chegou a parada limpa e não sinaliza nada é o pior estado possível de UI: indistinguível de uma que terminou. O incidente que você cita no comentário do teste — stream pesado em reasoning estourando o cap do coletor no meio, deixando a conversa presa sem sinal — é exatamente o caso que justifica o badge.

Separar `resolveTurnCompletionState` de `resolveConversationStalledState` também está certo: `tool_call_pending` é um estado legítimo em voo, não uma conversa travada.

Revalidei sobre o tip: **29/29**, typecheck:core limpo.

**Nota de integração.** O `tests/unit/responses-continuation-store.test.ts` conflitou com o #12854, que anexa a própria bateria ao mesmo arquivo. Reconstruí o arquivo como append limpo — versão do tip mais o seu bloco de 184 linhas, verificado por `esbuild` antes de rodar. Registro por que importa: na primeira tentativa eu apenas retirei os marcadores de conflito, e isso enfiou os seus testes **dentro** de um objeto literal não terminado do #12854. Compilava como erro de transform, não como conflito — só apareceu ao rodar. Resolver JSON e teste "aditivamente" sem verificar a sintaxe depois é armadilha; ficou a lição.
2026-09-10 10:52:03 -03:00
Dizzle
a152eb92db fix(resilience): stop unbounded queue that hangs 6min until Aborted (#12715)
Fila sem teto que segura a request seis minutos até o cliente abortar é pior que 503 imediato: consome slot, mascara a saturação e ainda entrega erro no fim. Um orçamento `maxWaitMs` por conexão compartilhado entre gate, slot padrão do provider e fila do Bottleneck é a forma certa — o teto tem que ser um só, senão cada camada espera o seu.

O `max(perConn, upstream)` no `executionMaxWaitMs` é o detalhe que evita a correção matar request em voo, que seria trocar um defeito por outro.

Registro a atribuição: você manteve o #12635 aberto para o @Tushar49 e creditou a percepção dele (providers lentos precisam de 2min→10min por conexão) enquanto adiciona o encanamento que faltava. É o jeito certo de construir sobre PR de outra pessoa sem tomar o crédito.

Sobre o `npm run lint` desmarcado com a nota do eslint quebrado no ambiente: deixar em branco e explicar vale mais que marcar sem ter rodado. Rodei aqui: limpo.

Revalidei sobre o tip: **13/13**, typecheck:core limpo, check-file-size OK. O `file-size-baseline.json` conflitou com os rebaselines desta campanha — resolvido aditivamente, JSON revalidado com `json.load`.
2026-09-10 10:49:34 -03:00
Ravi Tharuma
d6a61074dc fix(quota): align AUTH window labels with usage API and clarify 503 (#12884)
Um `ALL_TARGETS_SKIPPED` 503 que não diz qual janela esgotou é opaco justamente no momento em que o operador mais precisa saber. Alinhar os rótulos de janela AUTH com os da API de uso fecha a outra metade: dois nomes para a mesma coisa fazem o dashboard e o erro parecerem discordar.

Revalidei sobre o tip: **6/6**, typecheck:core limpo, check-file-size OK.

**Dois consertos meus na sua branch.**

1. `typecheck:core` falhava com `TS2345` em `comboAttemptLoop.ts` (linhas 130 e 416): o `QuotaSkipTarget` declarava `connectionId?: string`, mas o `ResolvedComboTarget` carrega `string | null` para alvo não-pinado. Alarguei para `string | null` no tipo de diagnóstico em vez de estreitar o call site — o módulo só **lê** o campo e a linha 29 já narrowa com `typeof === "string"`, então null não custa nada ali. Isso apareceu porque o `comboAttemptLoop` mudou de forma no #12746/#12811, mergeados nesta mesma campanha depois que você cortou a branch.

2. O `roundRobinCombo.ts` foi de 1198 para 1205 e cruzou o teto de 1200 para arquivo novo. Congelei com justificativa: o arquivo já nasceu em 1198 quando o #12811 o levantou de dentro do `combo.ts`, e os diagnósticos em si vivem no `quotaSkipDiagnostics.ts`, sob o cap. Registrei que a próxima extração natural é o corpo do attempt loop, mas que ele acabou de ser movido e deve assentar antes de ser cortado de novo.
2026-09-10 10:47:11 -03:00
Dizzle
4c10baa644 fix(dashboard): explain silent Radar cells on hover and gate them (#12937)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Sentinela que não se explica (`—`, `?`) faz o leitor inventar a razão. Explicar no hover é metade; o `check:radar-sentinels` é a outra — sem o gate, a explicação apodrece na primeira coluna nova.
2026-09-10 10:42:48 -03:00
Markus Hartung
b516e95262 fix(conversations): show a pending spinner for unresolved tool nodes instead of "(empty)" (#12727)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

"(empty)" para um nó de ferramenta ainda não resolvido é informação errada, não ausência de informação — o usuário lê como "não retornou nada". Spinner de pendente diz a verdade.
2026-09-10 10:42:38 -03:00
Ravi Tharuma
3e2a6d8f35 fix(credentialHealth): do not poison multi-upstream openai-compat conn on one model 402 (#12875)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Envenenar a conexão inteira por um 402 de **um** modelo é o erro clássico de granularidade em provider openai-compatible com múltiplos upstreams — derruba modelos que estavam saudáveis. Restringir ao modelo afetado é o comportamento correto, e é a mesma distinção que o guia de resiliência faz entre cooldown de conexão e lockout de modelo.
2026-09-10 10:42:34 -03:00
Ravi Tharuma
6029515402 fix(api): page and stream-complete GET /v1/models for large catalogs (#12882)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Paginar e completar o stream em `GET /v1/models` é a correção certa para catálogo grande: um payload único que cresce com o número de providers vira timeout silencioso no cliente, não erro.
2026-09-10 10:42:30 -03:00
Ravi Tharuma
0ddb47228b fix(monitoring): expose failed connection ids on credentialHealth (#12876)
Validado numa worktree combinada com a onda de dashboard/monitoring desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 130/131 nos testes focados — a falha restante é asserção de tempo de parede sob carga, verde 6/6 isolada.

Um health que diz "falhou" sem dizer **qual** conexão obriga o operador a cruzar logs para achar o óbvio. Expor os ids das que falharam é o que transforma o endpoint em ferramenta de diagnóstico.
2026-09-10 10:42:26 -03:00
Diego Rodrigues de Sa e Souza
a0c69ca25e refactor(ui): orchestration canvas fase 3 — theme-aware status tokens across flow surfaces (#12378) (#13203)
* refactor(ui): move shared flow colors to the orchestration status tokens

FLOW_EDGE_COLORS and TokenHealthBadge were pinned to the fixed dark-mode
hexes in STATUS_HEX, so both rendered dark-theme green/amber/red on a light
background. They now read the theme-aware --orch-status-{success,warning,
error,muted} custom properties introduced in Fase 2. The dark values of
those tokens are exactly the old hexes, so dark mode is unchanged and only
light mode gains contrast. `idle` was already a CSS var, which is the
precedent proving a var() resolves in a ReactFlow edge stroke.

Five call-sites built translucent variants by concatenating an 8-bit alpha
suffix onto the palette hex (`${FLOW_EDGE_COLORS.error}40`), which cannot
work with a var(). They move to a new documented helper, flowColorAlpha(),
that wraps color-mix() — the same approach orchStateBadgeBg() already uses
in the orchestration model. Percentages mirror the old suffixes
(20 -> 13%, 30 -> 19%, 40 -> 25%).

STATUS_HEX stays exported as the dark-mode mirror; it now has no production
consumer. globals.css needed no change — all five tokens already existed in
both themes.

The colour assertions in the topology, combo-live and design-grid suites
were aligned to the tokens, never weakened: every hex equality became an
equality against the corresponding var(). design-grid additionally now
asserts each token is defined in BOTH themes.

Refs #12378

* refactor(ui): finish the status-token migration across flow surfaces

Sweeps the five state hexes across the remaining flow surfaces, following D1:

- ComboLiveStudio: active/error provider pills and the run-outcome tri-state.
- CompressionCockpit / WaterfallInspector / IoNode: the savings readouts and the
  savings quality ramp (>=30 success, >=15 warning, else muted).
- EngineNode: the same ramp, plus the running state, whose glow moved to
  flowColorAlpha — the literal #f59e0b40 suffix is invalid once the value is a var().
- WaterfallInspector: a skipped step now reads as muted rather than a bare grey hex.

Deliberately NOT migrated, because they are categorical or brand palettes rather
than state: STRATEGY_COLORS (routing-strategy hues), LAYER_COLORS (compression
layer pills), the provider brand color in ProviderTopology, and IoNode's
indigo/green input-output identity pair. The new test asserts both halves — what
became a token AND what stays hex — so a later sweep cannot silently swallow a
categorical palette.

Refs #12378
2026-09-10 10:29:55 -03:00
Diego Rodrigues de Sa e Souza
590582711c fix(dashboard): orchestration canvas fase 3 — follow-ups da review final (#12639) (#12988)
* feat(api): hydrate memoryHits from the persisted history event

`GET /api/a2a/tasks/[id]` falls back to the persisted history row once a task
leaves the in-memory TTL window, and `reconstituteHistoricalTask` hard-coded
`metadata: {}` — so the drawer's "Memory used" section vanished for any
historical task, even though `executeA2ATaskWithState` had already written a
`memory_hits` event with the hits.

The fallback now reads that event: `data_json` is parsed and, when it yields at
least one well-formed hit, exposed as `metadata.memoryHits`. The event itself is
filtered out of `events` — it is observability, not a state transition, and
without the filter it leaked into the timeline as a duplicate of the row's
current state.

Reading is defensive throughout, mirroring `DrawerMemory`'s own validation: the
payload is caller-influenced and unvalidated end to end, so `JSON.parse` runs
inside `safeJsonParse`, non-arrays are rejected, and each entry must carry `id`,
`key`, `type` and `snippet` as strings (a non-string field would be rendered as
a React child and take the drawer down). Malformed input degrades to
`metadata: {}` and a 200 — never a 500.

Refs #12639

* fix(a2a): bound the memory recall with its own deadline

collectMemoryHits() runs BEFORE the skill handler and had no deadline at all,
so a slow memory backend delayed the start of every A2A task — the HTTP
genericBackend alone defaults to a 30s timeout.

The search now races a MEMORY_RECALL_TIMEOUT_MS (1500ms) deadline. Overshooting
degrades exactly like any other recall failure: empty hits, a warn log, and the
task proceeds normally (best-effort contract unchanged, nothing propagates).
The deadline timer is cleared in a finally on BOTH paths so no handle is left
holding the event loop open, and MemoryHitsDeps.timeoutMs makes it injectable
so the tests cost milliseconds instead of 1.5s of wall clock.

Refs #12639

* fix(dashboard): carry conductor requirements and focus the repeated task

The drawer's "Repeat" for a Conductor task dropped the runner/model pinning and
left the operator staring at the finished run:

- `hubTaskSchema` now parses the hub's `requirements` (`.catch(null)` so an odd
  shape never fails the whole task parse), and `ConductorTaskDetail` exposes
  `cli`/`model` (`null` when the hub sends none).
- `repeatReqForConductor` carries `cli`/`model` when present and OMITS them
  otherwise — the route's Zod takes both as optional strings, so a `null` would
  400. The two fields are independent.
- `performAction` reads the response body once and returns it, so the repeat can
  report the CANVAS id of the created task (`task_id` / `data.id` /
  `result.task.id`, each with its node prefix). `OrchestrationPageClient` then
  refetches and focuses it via `?node=`; History keeps its current behavior.
- `conductor-routes-auth.test.ts` covers the creation route through its `ROUTES`
  array; the duplicated source assertion left `conductor-create-route.test.ts`.

Refs #12639

* chore(a2a): follow-ups changelog

Changelog fragment for the five items PR-C delivers from #12639.

The sixth item on the issue — an authenticated panel path for A2A task
creation — stays deliberately out of scope and is recorded as such in a
comment on the issue rather than silently dropped: the JSON-RPC endpoint
accepts API keys only, and widening that endpoint's auth surface to serve a
UI convenience is the operator's call, not the implementation's.

Closes #12639
2026-09-10 10:23:52 -03:00
Markus Hartung
7d4189fd78 fix(responses-continuation): bridge the write-in-flight window with an in-memory pending store (#12854)
Diagnóstico por captura de pacote em tráfego real, com o `400 previous_response_not_found` reassemblado do tcpdump três vezes no mesmo loop de tool-calling — isso é evidência, não hipótese. A causa é limpa: `detail_state` só vira `'ready'` depois de uma escrita fire-and-forget enfileirada num worker único, e o cliente já tem o id de resposta antes disso. Semear a ponte **antes do primeiro await** é o que faz a correção não custar latência.

Revalidei sobre o tip: **19/19**, typecheck:core limpo, check-file-size OK.

**Estava draft e eu marquei como ready.** Não havia gate declarado — nem RFC pendente, nem decisão de produto em aberto — e passou na validação; a diretiva permanente do dono para esta campanha é avaliar draft como qualquer PR e promover quando passa. Se a intenção era segurar por outro motivo, me avise que eu reverto.

**Um conserto meu na sua branch.** O `typecheck:core` falhava com `TS2345` em `callLogs.ts:489` — e falhava **na sua branch sozinha**, não por interação com a onda; confirmei isolando. O call site fazia cast para `{ clientRawRequest?: unknown; clientResponse?: unknown }`, mais frouxo que o `ContinuationPipeline` que o parâmetro exige, e `unknown` não assina para os membros tipados. Exportei o `ContinuationPipeline` do próprio store e usei ele no cast, em vez de alargar o tipo do parâmetro: o contrato passa a ter um nome só, no lugar onde ele já vivia.

**Sobre a sua Reviewer Note do Map sem limite de contagem:** concordo que vale registrar. Entradas pequenas com TTL de 60s auto-expirando não justificam sizing agora, mas se aparecer burst sustentado o sintoma será memória, não erro — e aí a nota está aqui.

Também carreguei o rebaseline de `chatCore.ts` (6021→6026) e `stream.ts` (3080→3098), que a onda de streaming inteira faz crescer.
2026-09-10 10:23:26 -03:00
Dizzle
12c7895bbd fix(sse): rotate opencode accounts on geo-blocked 403 (#12941)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Estender a rotação que já existe para 429 ao 403 de bloqueio geográfico é a generalização certa, e manter a rejeição de fingerprint (Cloudflare 1010) fora dela é o que impede a rotação de queimar todas as contas contra uma recusa que não é de egresso.

Nota: os checkboxes de validação do corpo ficaram em branco, mas o diff traz dois arquivos de teste — vale marcar da próxima para o revisor não precisar conferir.
2026-09-10 10:19:14 -03:00
Markus Hartung
1216cff05b fix(streaming): fail fast when an upstream stream produces only lifecycle/heartbeat events, never real content (#12741)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Um stream que só emite eventos de ciclo de vida e heartbeat, sem conteúdo nenhum, é falha disfarçada de sucesso: o cliente espera até o timeout dele. Falhar rápido devolve o controle.
2026-09-10 10:19:10 -03:00
Markus Hartung
2b1c57485c fix(sse): stop rebuilding a truncated summary from the collector's cap-dropped event array (#12718)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

Reconstruir o resumo a partir de um array que o próprio coletor já truncou por cap produz um resumo que parece completo e não é — pior que resumo ausente, porque não se distingue. Parar de reconstruir dali é a correção.
2026-09-10 10:19:06 -03:00
Dizzle
91990d606b fix(sse): emit trailing usage estimate in translate streams when upstream stays silent (#12828)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

O #12151 cobriu só metade: passthrough emitia o chunk final de usage, translate calculava a estimativa **depois** de fechar o stream, então o número só chegava ao log do servidor e nunca ao cliente. Fechar essa metade é o que faz a feature existir de fato.

Não emitir segundo chunk quando o upstream já mandou usage real é o detalhe que impede a correção de virar contagem dobrada.
2026-09-10 10:19:02 -03:00
Ravi Tharuma
3272bedd1b fix(translator): drop or map agent_message on Chat Completions fallback (#12880)
Validado numa worktree combinada com a onda de streaming desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (289), check-file-size OK após rebaseline, 88/88 nos testes focados.

`agent_message` chegando num fallback de Chat Completions é um item que o cliente não sabe interpretar; mapear ou descartar é a escolha certa, e escolher por item em vez de derrubar a resposta inteira mantém o fallback útil.
2026-09-10 10:18:58 -03:00
Diego Rodrigues de Sa e Souza
a1b260146d fix(dashboard): orchestration canvas fase 3 — canvas polish (#12392) (#12983)
* fix(dashboard): keep the first failure timestamp in sourceStale

buildSourceStatuses stamped nowIso on every failing source at every poll, so
the stale indicator reported "since the last poll" instead of the first
failure — and, because snapshotContentKey serializes sources, the snapshot
identity churned on every tick while any source was down.

The failing branches now reuse the staleSince already held by that source in
the previous status list, via the functional setStatuses updater (no ref read
during render, no setState inside an effect body).

Refs #12392

* fix(dashboard): flag a source that starts failing after it had data

buildRootAndSourceEdges only materialized a placeholder SourceNode when the
failing source had no node at all. A source that already had work nodes and
then started failing (or went offline) kept its healthy-looking SourceNode
forever: no ⚠, no stale styling, no `sourceStale` line — the operator saw a
normal source while it was actually broken.

Now every non-ok/offline source is flagged: when its SourceNode is missing the
placeholder is created as before; when it exists, the node is replaced by a
copy carrying `sourceIssue` and `staleSince`. The copy (never a mutation) keeps
the function pure — the original object is still referenced by the caller's
`parts`, the same trap the droppedByState aliasing fix covered.

Tests: three cases in tests/unit/ui/orchestrationModel.test.ts — existing node
starting to fail (flags set, work nodes kept, no duplicate node, input object
untouched), existing node going offline (no invented staleSince), and a healthy
source staying free of both fields.

Refs #12392

* fix(dashboard): canvas polish batch (#12392)

Seven pointwise fixes on the Orchestration Canvas, each covered by a test:

1. Debounce x chip race: every chip/clear write in OrchestrationToolbar now cancels
   the pending search timer first. Left armed, it fired ~300ms later with a setParams
   closed over the pre-chip query string and silently reverted the chip.
2. The search input carries an aria-label (searchPlaceholder) — the placeholder alone
   is not an accessible name.
3. parseCsvSet trims each token, so `?state=running, failed` parses like the unpadded
   form instead of dropping the padded value.
4. toggleCsv was duplicated in the toolbar and the page client; both now import the
   single definition from the new model/urlParams.ts (pure, never mutates its inputs).
5. AgentsTab tells "nothing running" apart from "the filter matched nothing": with an
   active filter and no work node it renders noMatches + a clear-filters button instead
   of the setup CTAs, which would be wrong advice there.
6. Particle cap: orchestrationToFlow stamps `particles` on every edge and turns it off
   above PARTICLE_EDGE_CAP (40) simultaneously active edges — StatusEdge then renders
   the colored stroke without its 3 SMIL particles per edge.
7. The drawer's error banner clears when an action succeeds, so a recovered failure
   does not stay on screen.

Only `noMatches` is added to en.json here; the other locales are task B4.

Refs #12392

* chore(dashboard): canvas polish i18n + changelog

Real translations for orchestration.noMatches in the 41 non-English locales,
each one written against that file's own neighbouring keys (emptyTitle,
stateRunning, searchPlaceholder) so the wording for "task" and "filter"
matches what the locale already uses. No i18n:sync-ui, no __MISSING__ left.

Adds the changelog fragment for the nine PR-B fixes.

Closes #12392
2026-09-10 10:17:04 -03:00
Diego Rodrigues de Sa e Souza
13d792c0bb feat(dashboard): orchestration canvas fase 3 — compare two runs in the History tab (2.9) (#12677)
* feat(dashboard): pure model to compare two orchestration runs

* feat(dashboard): compare-mode selection in the History grid

* feat(dashboard): side-by-side comparison panel in the History tab (2.9)

* chore(dashboard): compare-runs i18n + changelog

* fix(dashboard): compare-panel loading state, height bound, delta legend, ARIA level

Final-review fix wave for PR-A (Orchestration Canvas Fase 3):

- Give each compare-panel side an explicit fetch status (loading/ok/error) so the
  Events metrics row shows "—" instead of a misleading real "0"/delta while a side
  is still loading or after its fetch failed.
- Bound the compare panel's height (max-h-[45vh], overflow-y-auto, shrink-0) so a
  run with many activities can no longer collapse the History grid to zero height.
- Clear the compare selection when the History preset changes, since a stale pick
  can fall outside the new range.
- Add a delta-column legend (new compareDeltaLegend i18n key, translated into all
  41 non-English locales) so operators know which side a positive delta favors.
- Use role="status" (not role="alert") for the informational compareDifferentIdentity
  banner, reserving role="alert" for actual per-side fetch failures.
- Restore ro.json's compareCost to the true cognate "Cost" (was distorted to
  "Cheltuieli" to dodge a byte-identical-to-English heuristic); audited the other
  40 locales for the same pattern across the 9 compare* keys, no other instance found.

* refactor(dashboard): split compare-runs/history-tab functions to clear complexity ratchets

CompareRunsPanel (92 lines, max-lines-per-function) and HistoryTab (85 lines,
same rule) exceeded the 80-line function cap; compareRuns.ts's a2aEventsFrom
exceeded the cognitive-complexity cap (16 > 15). Extract pure/presentational
helpers (ComparePanelHeaderBar, SideErrorRow, ComparisonMetrics, a2aEventFrom,
HistoryStatusRows, refreshNowMsOnActionDone) with no behavior, DOM, i18n or
aria change.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-09-10 10:15:52 -03:00
Diego Rodrigues de Sa e Souza
9debec71ec feat(i18n): 9 new locales — all 24 official EU languages (51 locales) (#13044)
Batch 1 of the locale expansion: Greek, Croatian, Serbian, Lithuanian, Estonian, Latvian, Slovenian, Maltese and Irish across the dashboard catalog, docs mirrors, CLI catalog, README, locale index and the site. 42 → 51 locales.

Also fixes the ICU literal escape the translation backend dropped around angle placeholders, four translations that invented or renamed a placeholder, the language bars that linked to mirrors that do not exist, and the migration count drift (171 → 172).

⚠️ base-red inherited: #12732 — the four unit shards and Fast Quality Gates fail identically on unrelated PRs cut from the same base.
2026-09-10 10:13:09 -03:00
Dizzle
8ecdd88c15 fix(pool): diagnose empty pools, align catalog filter, and surface drop reasons (#12795)
Um pool `auto/*` vazio que não diz por que está vazio é a pior forma de falha: o operador vê ausência e não sabe se é config, cota ou catálogo. Registrar qual estágio removeu quantos, e carregar `dropReason` em cada entrada de `quotaHealth.providers`, transforma silêncio em diagnóstico.

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

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

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

**Dívida registrada:** o `virtualFactory.ts` foi de 1187 para 1219 somando esta onda e cruzou o teto de 1200 pela primeira vez. Congelado com os candidatos a extração nomeados na justificativa.
2026-09-10 09:25:59 -03:00
Dizzle
d88fc2bce7 fix(routing): table pricing with catalog fallback for off-table models, pooled latency bootstrap, fresh tier cache (#12792)
Um modelo grátis fora da tabela herdando $5/$15 por milhão e afundando no roteamento cost-aware é o defeito mais caro desta onda: silencioso, e inverte exatamente a decisão que o operador quer.

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

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

**Duas integrações:**

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

**Nota de dívida:** o `virtualFactory.ts` cruzou o teto de 1200 linhas pela primeira vez (1187 → 1207) somando esta onda. Congelei em vez de dividir e registrei os dois candidatos a extração na justificativa — `computeSnapshotWeights` (~85 linhas) e o grupo de elegibilidade de credencial (~70). Qualquer um dos dois volta o arquivo para baixo do cap.
2026-09-10 09:20:18 -03:00
Dizzle
d68c8c869e fix(providers): single source for provider order, resolve xao rank (#12790)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

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

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

**Integração:** `scripts/quality/run-all-gates.mjs` conflitou com o `check:pricing-freshness` que entrou pelo #12792 na mesma onda. Aditivo — os dois gates coexistem.
2026-09-10 09:13:10 -03:00
Dizzle
9795372ef1 fix(auto-combo): give every mode pack quality and reliability weights (#12731)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

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

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

**Integração:** conflitou com o #12794 no `computeSnapshotWeights`; os dois compõem e ambos ficaram.
2026-09-10 09:13:04 -03:00
Dizzle
2b276e2043 fix(auto-combo): stop neutral snapshot scoring on observed tier/quality/breaker (#12794)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados.

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

**Integração:** o `computeSnapshotWeights` conflitou com o #12731, que adiciona peso de `reliability` a partir de `failureRate`/`errorRate`. Os dois cobrem chaves diferentes e compõem — ficaram ambos: reliability do #12731, health via breaker e quality desta PR, quota neutro nos dois. Nenhum dos dois lados foi descartado.
2026-09-10 09:12:58 -03:00
Dizzle
0048be75c8 feat(providers): advertise free-tier capability in provider plugin manifest (#12786)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

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

Registrar que 77 entradas de catálogo viram 76 no manifesto porque o `arcee-ai` ainda não tem entrada no registry é exatamente o tipo de discrepância que costuma virar bug fantasma.
2026-09-10 09:12:10 -03:00
Dizzle
39f9fc06c2 fix(models): stop treating provider-supplied free flags as trusted without a documented free tier (#12744)
Validado numa worktree combinada com a onda de roteamento/free-tier desta leva sobre `release/v3.8.51`: typecheck:core limpo, check-api-typecheck OK (288), check-file-size OK após rebaseline, 84/85 nos testes focados no runner Node.

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

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

**Integração:** o `decideHidePaid` que o #12795 extraiu passou a usar o seu `isFreeForProvider` por id, em vez de OR-ear os dois aliases num único `freeProvider`. A forma por id é a garantia que esta PR estabelece, então ela prevaleceu.
2026-09-10 09:12:07 -03:00
Diego Rodrigues de Sa e Souza
1db1bf3a9d docs: bump the documented migration count to 172 (#13209)
Contagem de migrations 171 → 172 após a `175_call_logs_provider_stats_indexes.sql` do #12832. Medido com `ls src/lib/db/migrations/*.sql | wc -l`.

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

41 mirrors de `llm.txt` regenerados pelo script do projeto. Aprovado por você para tocar `AGENTS.md`, mesma classe do #12970.
2026-09-10 09:11:23 -03:00
Diego Rodrigues de Sa e Souza
5a777a243b docs(changelog): add the missing bullet marker to the DeepSeek PoW fragment (#13200)
Um caractere. O fragmento do #13097 subiu sem o `- ` inicial e derrubou o `Merge integrity` para todo mundo que veio depois.

Terceira ocorrência da mesma causa nesta release; a anterior foi o `reset-aware-model-family.md`, que o #12711 consertou de carona.
2026-09-10 08:21:02 -03:00
Dizzle
85d29b253f fix(health): read empty quota as unknown instead of 0% (#12857)
Um 0% vermelho num install novo não é só feio: é um número contraditório, porque afirma medição onde não houve nenhuma. Ler ausência como "n/a" é a correção certa, e mantê-la display-only no caminho de combo health mantém o escopo honesto.

Registro que gostei: a PR **não** mexe no `/api/health`, e diz por quê — qualquer coisa que aquela rota devolva é pública numa instância exposta. Recusar o escopo adjacente com a razão escrita vale mais que a mudança em si.

Revalidei após mergear a base na branch: `api-health-version-source` + `combo-health-empty-snapshot` 5/5 no runner Node, `combo-health-null-quota` 1/1 no vitest, typecheck:core limpo.

**Integração:** `src/app/api/system/version/route.ts` conflitou com o `restartRunningServer` que entrou na release depois que você cortou a branch. Aditivo — ficaram os dois imports, a sua troca por `APP_CONFIG.version` e o passo de restart do outro PR.
2026-09-10 08:17:58 -03:00
Bob.Hou
bfbd090a96 fix(codex): lift nested child cooldowns on parent clear and on snapshot headroom (#12951)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289). Revalidei o head atual mergeado com o tip: typecheck:core limpo e **36/36** entre `db-rate-limit-guard` e as suítes desta PR.

Levantar o cooldown do escopo pai sem deixar os filhos aninhados presos é o miolo — um cooldown órfão em filho é invisível no dashboard e mantém a conexão fora de rota sem explicação.

**Nota de coordenação:** o `setConnectionRateLimitUntil` colidiu com o #12788 (guard contra timestamp não-finito ou já expirado), que mergeei nesta mesma onda. Eu tinha resolvido a integração na minha worktree, mas ao empurrar o push foi rejeitado — você já tinha empurrado `441fd44`, `f853ba5` e `af2ed01` com a integração feita, e a sua ordenação é equivalente à minha. Descartei a minha e mantive a sua; o crédito é seu inteiro. Fica o registro de que push rejeitado não é erro leve: se eu tivesse mergeado sem reler, teria levado a branch errada.
2026-09-10 08:15:57 -03:00
anhtahaylove
9492357360 fix(deepseek): release the PoW worker slot when spawning fails (#13097)
`solveInWorker` increments `activeWorkerCount` before constructing the Worker,
but the `cleanup()` that decrements it lives inside the promise executor and only
runs once the worker exists. Anything that throws first -- most obviously
`resolveWorkerPath()` when the worker script is missing, since it resolves
against `process.cwd()` -- leaves the counter permanently incremented.

With `MAX_CONCURRENT_WORKERS = 2`, two such failures wedge the solver for the
lifetime of the process: every later call rejects with "capacity reached (2)"
while no worker is actually running, and the real cause is hidden.

Construct the Worker inside a try/catch and release the slot before rejecting.

Fixes #13094
2026-09-09 20:58:23 -03:00
Dizzle
ba597b631d fix(db): call_logs provider stats read true on empty and legacy data (#12832)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289), 70 testes focados no runner Node e 1 no vitest, todos verdes.

"Zeros continuam zeros, latências ausentes continuam ausentes, falhas pré-coluna ganham o próprio balde" — a distinção entre ausência e zero é o miolo aqui. Um install novo mostrando 0ms como se tivesse medido é pior que mostrar nada, porque parece dado.

**Uma mudança minha na sua branch: a migration foi renumerada de 174 para 175.** A `174_server_tool_executions.sql` entrou no #12867, mergeado horas antes desta leva, então `174_call_logs_provider_stats_indexes.sql` colidia. Renomeei o arquivo e ajustei o rótulo do teste ("migration 174 creates..." → 175). Confirmei que não sobrou prefixo duplicado em `src/lib/db/migrations/` e revalidei o `call-logs-provider-stats`: 4/4.

Os dois índices compostos são a parte que paga a longo prazo — rollup por provider parando de varrer a tabela.
2026-09-08 09:31:32 -03:00
Diego Rodrigues de Sa e Souza
f7349dd776 chore(quality): rebaseline chatCore.ts after the non-streaming regression fixes (#13045)
Cap de arquivo estourado por mim: mergeei o #12963 e o #12990 sem rebaselinar o `chatCore.ts`, que ambos fazem crescer. Cada um passou no próprio gate porque mediu contra o tip de onde saiu — o cap só estoura no conjunto, que é o padrão registrado sete vezes no handoff da campanha anterior.

5984 → 6021, +37 linhas, irredutíveis nos chokepoints existentes: cada edição fica onde o `chatCore` já toma a decisão, e os helpers estão sob o cap. Coberto por `chatcore-translation-paths` (72/74; as 2 abertas são a issue #13043).
2026-09-08 09:30:02 -03:00
Bob.Hou
0f81e7557c feat(volcengine): canonical quota window mapping and safe multi-connection plan binding (#12950)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.

Mapeamento canônico de janela de quota mais binding multi-conexão seguro, com 4 arquivos de teste para 4 de produção — proporção que dá para revisar.
2026-09-08 09:29:45 -03:00
Dizzle
29593377cc fix(db): extract WAL maintenance, surface TRUNCATE no-op (#12853)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.

"Um checkpoint que nunca olhou o próprio resultado" é o tipo de defeito que só aparece quando o WAL fica maior que o banco. Ler a linha de retorno do pragma e diferenciar busy de sucesso é a correção; o retry `PASSIVE` um minuto depois é o que evita esperar as 6 horas do próximo tick.

Gostei da decisão de não persistir contador: streak em memória que zera no stop é o comportamento honesto para uma métrica de contenção.

Os 11 casos cobrem as formas de retorno que o pragma pode assumir — sentinela `-1`, objeto pelado, `undefined`/`null`/`[]` — que é onde esse tipo de parsing costuma quebrar em silêncio.
2026-09-08 09:29:41 -03:00
Dizzle
3abd855095 fix(quota): dedup concurrent getSaturation misses with singleflight (#12787)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.

Singleflight no ponto certo: o cache de 30s já existia, mas não cobria a janela entre o miss e a resolução — que é exatamente quando a rajada acontece. Manter o `finally` para limpar o pending é o detalhe que impede o dedup de virar um cache permanente em caso de erro.

O teste que vale é o quinto: chamadores concorrentes na mesma chave disparam **um** fetch upstream. Os outros quatro protegem o que não pode mudar — fail-open em 0, veneno de 30s, e nenhum vazamento entre chaves.
2026-09-08 09:29:38 -03:00
Dizzle
678af2ea38 fix(db): ignore non-finite rate_limited_until writes, preserve null clear (#12788)
Validado numa worktree combinada com a onda de persistência desta leva sobre `release/v3.8.51`: check-file-size e check-changelog-integrity OK, typecheck:core limpo, check-api-typecheck OK (289, dentro da baseline), 70 testes focados no runner Node e 1 no vitest, todos verdes.

Persistir `NaN`/`Infinity` numa coluna TEXT envenena toda leitura futura, e um timestamp já expirado sobrescrevendo uma linha viva é pior que não escrever nada. O guard fundido na cabeça da função cobre os dois sem tocar leitores nem o caminho de clear.

Os 6 casos do teste incluem o que mais importa: `null` continua limpando, e uma escrita expirada não derruba um cooldown ativo. Nota: 3 deles falham antes do guard, como você registrou.

**Integração:** este arquivo colidiu com o #12951, que também guarda a cabeça de `setConnectionRateLimitUntil` — lá o `null` vira caminho de clear que também remove os cooldowns filhos do Codex. Os dois compõem: trata-se o `null` primeiro (clear + return), e o seu guard de finitude/expiração passa a valer para os não-nulos. Ambos preservados.
2026-09-08 09:29:33 -03:00
Diego Rodrigues de Sa e Souza
86b1cb84fe feat(release): reconcile-changelog tool + version-anchored fragment aggregation (#12987)
* feat(release): reconcile-changelog tool + version-anchored fragment aggregation

`npm run release:reconcile` (scripts/release/reconcile-changelog.mjs) turns the
v3.8.51 reconciliation pass (#12971) into a repeatable Phase 0a step:

- folds `changelog.d/` fragments under `## [<version>]` — never under the first
  matching heading — and credits each one with the PR of the commit that ADDED
  it (`git log --diff-filter=A`), because the filename prefix is not reliable
  (issue numbers, closed/recreated PRs, literal `#PR_NUMBER`); `--carrier N`
  marks a PR that only back-filled fragments for other people's PRs
- drops fragments whose text already ships in another version section
  (phantom fragments re-added by branches cut before the previous aggregation)
- covers a commit only when its OWN PR is a primary ref of a bullet (a
  `/pull/N` link, the trailing `(#N)`, or an explicit `(#N …)` group) so an
  incidental mention cannot hide a PR's own bullet; generates
  `**type(scope):** subject (#PR) — thanks @author` for the rest, rolls
  Dependabot bumps into one line, documents direct pushes by hash
- `--credit N=handle` carries the closed-PR / co-author / deleted-PR audit
- keeps pre-existing section bullets verbatim (changelog-integrity compares
  bullet lines), never touches `[Unreleased]` or older sections
- opens the section with "📊 Release by the numbers" + "🏆 Top 25" (mailmap +
  merged-PR login), the v3.8.50 format

`aggregate-changelog.mjs` gains the same anchoring: `insertBullets(text,
bullets, version)` searches the heading inside `## [version]` only (with
`[Unreleased]` still carrying `###  New Features`, every feature fragment was
landing there); `aggregate()` reads the version from package.json.

Tests: tests/unit/reconcile-changelog.test.ts (helpers + an end-to-end
reconcile fixture) and two new cases in tests/unit/changelog-fragments.test.ts.

* fix(release): escape every regex metacharacter before building the mention regex

CodeQL js/incomplete-sanitization on reconcile-changelog.mjs: the handle was only
escaping '-' before being interpolated into a RegExp. Use a full escapeRegExp
helper instead; handles are [A-Za-z0-9_-] in practice, so behaviour is unchanged
for real input and the addCredit tests still pass.
2026-09-08 09:11:13 -03:00
Diego Rodrigues de Sa e Souza
10fef01d20 docs: sync migration and strategy counts with the code (#12970)
Contadores de docs fora de sincronia com o código, aprovado pelo dono em chat por tocar `AGENTS.md` e `skills/cli-tunnel/SKILL.md` (Hard Rule — superfície de instrução de agente). Nenhuma instrução mudou.

`Docs Gates` acusava 6 drifts STRICT. Dois vieram da minha leva de 16 PRs: migrations 169 → **171** (#12707 trouxe a 173, #12867 a 174) e estratégias de roteamento 19 → **20** (#12789 registrou a `quota-weighted`). Contei os arquivos em vez de confiar na memória: `ls src/lib/db/migrations/*.sql | wc -l` → 171.

Os 41 mirrors de `docs/i18n/*/llm.txt` foram regenerados com `scripts/i18n/sync-llm-mirrors.mjs` — o gate exige cópia exata da raiz.

O outro braço, `check:agent-skills-sync` acusando `GENERATED: + cli-tunnel`, era herdado (o corpo do #12866 já o registrava). O `SKILL.md` commitado documentava `tunnel create [type]`, um argumento que a CLI **não aceita** — conferido em `bin/cli/commands/tunnel.mjs:21`, que declara `.command("create")` puro. Saída do gerador, não escrita à mão.

| gate | antes | depois |
|---|---|---|
| `check:docs-counts` | 6 drifts STRICT | **0** |
| `check:docs-sync` | FAIL — 41 mirrors divergentes | **PASS** |
| `check:agent-skills-sync` | `+ cli-tunnel` | **UNCHANGED: 46 skills** |
2026-09-08 09:10:40 -03:00
Diego Rodrigues de Sa e Souza
89c42d36df fix(security): redact AIza credentials of any length in error bodies (#12964)
Vazamento de credencial em corpo de erro. `tests/unit/error-sanitizer-sk-key-qv45.test.ts` falhava no tip em 8ms:

```
AssertionError: Google key survived: Bad credentials for AIzaSyA1B2C3D4E5F6G7H8I9J0KaLbMcNdOeP
```

O padrão era `/AIza[0-9A-Za-z_-]{35}/` — comprimento **exato**. Uma chave Google padrão tem 39 caracteres e casa; qualquer credencial `AIza…` mais curta ou mais longa passava direto para o corpo do erro.

Os dois lados divergiram na reconciliação de dois PRs do mesmo GHSA: o padrão com `{35}` veio do #12506, o teste anti-drift que cobra `/\\bAIza[A-Za-z0-9_-]{20,}/` veio do #12620. Está vermelho desde que os dois entraram em sequência.

`{20,}` no lugar de `{35}`. Numa mensagem de erro, redigir demais uma string que apenas começa com `AIza` não custa nada; redigir de menos vaza credencial — o lado errado para errar é claro.

Evidência: o arquivo vai de 7/9 para **9/9**. Bateria de sanitização com 538 testes: 533 passam, e as 5 restantes são pré-existentes no tip, não desta mudança (4 levam 21–25s por spawn de processo isolado sob carga; `tunnel-routes-error-sanitization` falha igual no tip puro, verificado). Nenhum teste foi enfraquecido — o padrão foi ampliado para satisfazer uma asserção que já existia.
2026-09-08 09:10:36 -03:00
Diego Rodrigues de Sa e Souza
3ab53d188c fix(sse): restore abort mapping, cache telemetry and fence safety on the non-streaming leg (#12990)
Consertadas 5 das 7 regressões que o #12867 introduziu em `tests/unit/chatcore-translation-paths.test.ts` — arquivo que ele não toca, e por isso fora da minha validação focada quando o mergeei. Medido: **74/74** em `ce49d96` (antes), **67/74** em `d6f3150` (depois), **72/74** agora.

**Abort de cliente perdeu o mapeamento (3 testes).** O leg classificava por `error.name === "AbortError"`, mas `abort(reason)` pode rejeitar com string crua sem `name` — essa forma caía em 502 em vez de 499, o que o #7907 fixou. E a mensagem passava por `formatProviderError`, entregando `[499]: request aborted by client` ao cliente. O `chatCore` sempre usou `isLocalStreamLifecycleError` e o literal `"Request aborted"`; espelhado.

**`clientResponse` sintético em abort (1 teste).** O caminho antigo omitia o campo porque o cliente já tinha desconectado — esse corpo é o que teríamos enviado, e o dashboard lê o campo como "o que o cliente recebeu". O caminho novo gravava sempre.

**Telemetria de prompt cache sumiu do call log (1 teste).** `claudePromptCacheLogMeta` só era construído dentro do `executeProviderRequest`; o leg virou dono do primeiro send e a variável ficou `null`, então `_omniroute.claudePromptCache` desapareceu **em silêncio** de todo call log desse caminho. Não é teste chato: é observabilidade perdida em produção.

**Corpo não canonicalizável derrubava a request (1 teste).** `derivePostInjectionRequestIdentity` era chamado antes de qualquer checagem de flag; ele canonicaliza o corpo e o `canonicalStringify` rejeita `Date`, `Map` e instâncias de classe por desenho. Um corpo com essas formas lançava `TypeError` em **toda** request não-streaming, inclusive com `SERVER_OWNED_TOOL_LOOP_ENABLED` desligada, que é o default. Agora deriva só quando o loop pode rodar e falha fechada.

Evidência: 74 testes do arquivo 72/74; 138 nas 5 suítes vizinhas com 136 passando; `typecheck:core` limpo; `check-api-typecheck` OK 289; ESLint 0.

**As 2 restantes ficam abertas de propósito** — `refreshes GitHub credentials after 401` e `locks per-model quota failures`. Mesma causa: o leg encerra num não-2xx sem passar pela classificação de falha do `chatCore`. `nonStreamingProviderLeg.ts` não tem uma ocorrência de `lockModel`, `refreshCredentials` ou `markAccountUnavailable`; o `chatCore` tem ~170 linhas disso mais o bloco de refresh 401. Em produção: token Copilot não renova no 401, e 402/429 por quota não trava o modelo naquela conexão. Não consertei porque devolver a `Response` ao `chatCore` é impossível (já consumida por `.text()`) e reimplementar a classificação no leg é decisão de desenho do refactor — @HouMinXi tem o contexto.
2026-09-08 09:10:03 -03:00
Diego Rodrigues de Sa e Souza
99282e1054 fix(sse): pin the ok variant of the non-streaming leg result in chatCore (#12963)
Base-red: `API Route Typecheck` falhava no tip com 13 TS2339 novos em `chatCore.ts`, vindos do #12867 — que eu mergeei validando só com `typecheck:core`, que não cobre esse arquivo.

Causa: `legResult` é a união `NonStreamingProviderLegResult`; o guard de erro estreita para a variante `ok`, mas a reatribuição condicional do tool loop devolve o tipo declarado e as 13 leituras seguintes perdem a narrowing. Corrigido fixando a variante num binding próprio — `loopApply.leg` já é `& { kind: "ok" }`, então sem cast.

Gate: 302 erros com 13 novos → **289, todos dentro da baseline congelada**. `typecheck:core` limpo, ESLint 0 no arquivo.

Dois commits: o primeiro é Prettier puro sobre o arquivo do tip (que chegou fora do padrão pelo #12867), verificado byte a byte contra `prettier(tip)`; o segundo é a mudança semântica, 39 linhas.

Os demais vermelhos deste PR são herdados e cobertos por #12990, #12964 e #12970.
2026-09-08 09:05:16 -03:00
Diego Rodrigues de Sa e Souza
fcc2dcd1a6 docs(changelog): reconcile the v3.8.51 living section — fold 366 fragments, cover every cycle commit, credit every contributor (#12971)
Mid-cycle reconciliation of `## [3.8.51]` against the full cycle range
`release/v3.8.50..release/v3.8.51` (091589089c..d6f315018a, 696 non-merge commits):

- fold the 366 `changelog.d/` fragments into the section (features had been
  landing under `[Unreleased]` because the aggregator appends at the FIRST
  matching heading) and delete them
- drop 25 fragments that duplicate bullets already shipped in `[3.8.50]` /
  `[Unreleased]` (six phantom fragments re-added by branches cut before the
  v3.8.50 aggregation; 19 pre-cycle PR fragments)
- generate one bullet per cycle commit that had no fragment (350 commits:
  44 features / 196 fixes / 91 maintenance, 17 Dependabot bumps rolled up),
  carrying the merged PR link and `— thanks @author`
- credit audit: link every fragment to the PR that actually landed it
  (`git log --diff-filter=A`), fix three `#PR_NUMBER` placeholders and two
  misnumbered fragments (#11845, #11864), credit the recreated PRs to their
  original authors (#11887–#11892 → @MumuTW, #12255 → @backryun,
  #11771 → @Rahulsharma0810), the nine hartmark co-authored fixes, and the
  deleted PR #11370 to @kriptoburak
- add the "Release by the numbers" block, the Top-25 ranking (mailmap +
  GitHub login) and the mandatory `### 🙌 Contributors` hall
  (111 external contributors + maintainer, generated by
  `release:contributors --inject`)
- resync the 41 i18n CHANGELOG mirrors

Gates: check:changelog-integrity OK, check:docs-sync PASS.
2026-09-07 16:20:57 -03:00
3624 changed files with 1316012 additions and 147432 deletions

View File

@@ -73,6 +73,11 @@ docs/i18n/**
# so without this rule these land in /app/docs and become readable through the
# dashboard's Docs viewer at runtime.
docs/superpowers/**
# Operator-internal security writeups: git only, not the image or /docs catalog.
docs/security/STEALTH_GUIDE.md
docs/security/SOCKET_DEV_FINDINGS.md
docs/security/MITM-TPROXY-DECRYPT.md
docs/security/PUBLIC_CREDS.md
docs/diagrams/**/*.png
docs/diagrams/**/*.jpg
docs/diagrams/**/*.jpeg

View File

@@ -124,6 +124,21 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Host port for the compose Redis sidecar. Default: 6379.
# REDIS_PORT=6379
# Host interface docker-compose publishes the app's own ports (dashboard,
# API, live-WS) on for the base/web/cli/host profiles and docker-compose.prod.yml.
# Default: 127.0.0.1 (loopback only). Combined with REQUIRE_API_KEY=false
# (the default below), an unqualified publish spec would expose the anonymous
# /v1 LLM proxy to your whole LAN/WAN. Only set this to 0.0.0.0 once you've
# confirmed REQUIRE_API_KEY=true, or that a reverse proxy in front of this
# instance already enforces its own authentication. (#12568)
# APP_BIND_HOST=127.0.0.1
# Host interface docker-compose publishes the Qdrant memory sidecar on.
# Default: 127.0.0.1 (loopback only). Same LAN-exposure reasoning as Redis.
# QDRANT_BIND_HOST=127.0.0.1
# Host interface docker-compose publishes the Bifrost router sidecar on.
# Default: 127.0.0.1 (loopback only). Same LAN-exposure reasoning as Redis.
# BIFROST_BIND_HOST=127.0.0.1
# ═══════════════════════════════════════════════════════════════════════════════
# 3. NETWORK & PORTS
# ═══════════════════════════════════════════════════════════════════════════════
@@ -373,6 +388,8 @@ AUTH_COOKIE_SECURE=false
# Require an API key for all /v1/* proxy endpoints.
# Used by: API middleware — rejects unauthenticated requests to the proxy API.
# Default: false | Set true for multi-user/public deployments.
# Leaving this false is only safe when the app is reachable on loopback only
# (see APP_BIND_HOST above) or sits behind a reverse proxy doing its own auth.
REQUIRE_API_KEY=false
# Allow revealing full API key values in the Dashboard UI.
@@ -720,6 +737,12 @@ NEXT_PUBLIC_CLOUD_URL=
ENABLE_SOCKS5_PROXY=true
NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
# Opt-in feature flag (default off; a dashboard DB override wins over this value): proxy pools
# and per-account rotation stop re-serving a member that just failed (TCP probe refused, or a
# 429 received through it) for a period that doubles on each repeat, up to a cap. No proxy
# status is written. "true" (or 1, yes) enables it; unset keeps plain selection.
# PROXY_SKIP_RECENTLY_FAILED=false
# Standard proxy variables (lowercase variants also supported).
# HTTP_PROXY=http://127.0.0.1:7890
# HTTPS_PROXY=http://127.0.0.1:7890
@@ -1123,6 +1146,22 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
#OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS=21600000
# Frequent wal_checkpoint(PASSIVE) cadence (ms). Set to 0 to disable. Default: 300000 (5m).
# Used by: src/lib/db/walMaintenance.ts.
#OMNIROUTE_WAL_PASSIVE_INTERVAL_MS=300000
# WAL size (MB) above which a PASSIVE tick escalates to wal_checkpoint(TRUNCATE). Default: 256.
# Used by: src/lib/db/walMaintenance.ts.
#OMNIROUTE_WAL_GUARD_MAX_MB=256
# Minimum rows a cleanup must delete before the post-cleanup VACUUM runs. Default: 1000.
# 0 always vacuums when rows were freed. Used by: src/lib/db/cleanup.ts.
#OMNIROUTE_VACUUM_MIN_DELETED_ROWS=1000
# Explicit path to sql-wasm.wasm for the sql.js fallback adapter. Default: auto-detect.
# Used by: src/lib/db/adapters/sqljsAdapter.ts.
#OMNIROUTE_SQLJS_WASM_PATH=
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
@@ -1175,6 +1214,11 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
# Trae OAuth token override. Used by: open-sse/executors/trae.ts.
# TRAE_TOKEN=
# Trae web client Origin/Referer override (fleet-wide bump if Trae moves hosts
# again without a code change). Default: https://work.trae.ai.
# Used by: open-sse/executors/trae.ts.
# TRAE_WEB_ORIGIN=https://work.trae.ai
# ── Gemini / Antigravity (Google-based) ──
# These providers ship public OAuth client_id/secret values embedded in their
# public CLIs. Defaults are baked into the code via
@@ -1311,7 +1355,8 @@ CLAUDE_USER_AGENT="claude-cli/2.1.258 (external, cli)"
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
# forward the original names verbatim (debugging only).
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)"
# Optional override; leave unset to follow the shared Codex client version.
# CODEX_USER_AGENT="codex-cli/0.153.4 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
@@ -1331,7 +1376,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Override Codex client version sent in headers independently of the
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.144.1
# CODEX_CLIENT_VERSION=0.153.4
#
# Override the advertised Claude Code client version independently of
# CLAUDE_USER_AGENT. Anthropic gates some models (Fable 5.1) on this
@@ -1646,6 +1691,9 @@ CURSOR_USER_AGENT="Cursor/3.4"
# ── TLS client (wreq-js fingerprint proxy) ──
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
# TLS_FIRST_BYTE_WATCHDOG_MS=10000 # #12656: bounds time-to-first-byte on the wreq body (0 disables)
# OPENCODE_RESPONSES_STALL_ROTATION=false # #13484 feature flag (Settings → Feature Flags wins): rotate once when a streamed Responses reply stalls before its first byte
# RESPONSES_FIRST_BYTE_TIMEOUT_MS=15000 # #13484: OpenCode Responses first-byte window, only used when the OPENCODE_RESPONSES_STALL_ROTATION flag is on (0 disables)
# ── API Bridge (/v1 proxy server) ──
# API_BRIDGE_PROXY_TIMEOUT_MS=600000 # Proxy hop timeout (default: 10min)
@@ -1776,6 +1824,13 @@ APP_LOG_TO_FILE=true
# Override only to hand-tune for a known workload.
# HEAP_PRESSURE_THRESHOLD_MB=
# Exit the process after critical resource pressure persists, so a supervisor
# (systemd Restart=always, Docker restart policy) brings back a clean process.
# Accepts 1/true/yes/on. Default: false. Used by: open-sse/utils/resourcePressure.ts.
# OMNIROUTE_PRESSURE_SELF_RESTART=false
# How long (ms) critical pressure must persist before that exit fires. Default: 120000 (2m).
# OMNIROUTE_PRESSURE_SELF_RESTART_AFTER_MS=120000
# ── CLI helpers (bin/cli/) ──
# Override UI language for CLI output. Accepts BCP-47 locale (e.g. en, pt-BR).
# Falls back to LC_ALL / LC_MESSAGES / LANG / en if unset.
@@ -1793,6 +1848,10 @@ APP_LOG_TO_FILE=true
# Per-attempt HTTP timeout for CLI → server calls (milliseconds). Default: 30000.
# OMNIROUTE_HTTP_TIMEOUT_MS=30000
# How long `omniroute serve` waits for the health endpoint before printing the
# readiness-timeout warning (milliseconds). Also --ready-timeout. Default: 60000.
# OMNIROUTE_READY_TIMEOUT_MS=60000
# Set to 1 to print retry/backoff details to stderr during CLI commands.
# OMNIROUTE_VERBOSE=0
@@ -1938,6 +1997,12 @@ APP_LOG_TO_FILE=true
# Default: 8000 (8 seconds). On timeout, a last-good 200 is served when available.
# CATALOG_BUILD_TIMEOUT_MS=8000
# Age after which a connection's synced model list stops being authoritative for routing (#12849).
# A stale (or never-timestamped) synced catalog fails open to the provider registry.
# Used by: src/lib/db/models/activeSyncedCatalog.ts
# Default: 2592000000 (30 days)
# OMNIROUTE_SYNCED_CATALOG_STALE_AFTER_MS=2592000000
# ── NanoBanana (Image Generation) ──
# Polling config for async image generation jobs.
# Used by: open-sse/handlers/imageGeneration.ts
@@ -2077,6 +2142,13 @@ APP_LOG_TO_FILE=true
# Management key for an externally managed instance. Embedded instances use
# OmniRoute's encrypted service key.
# CLIPROXYAPI_MANAGEMENT_KEY=
# Host interface docker-compose publishes the cliproxyapi sidecar on (the
# --profile cliproxyapi Docker service, port 8317). Default: 127.0.0.1
# (loopback only) — its data volume holds provider OAuth/API credentials, and
# the pinned image has no env-based data-plane api-keys override (only a
# mounted config.yaml), so an unqualified publish spec would put a
# credential-bearing service on your whole LAN. (#12578)
# CLIPROXY_BIND_HOST=127.0.0.1
# ── Mux embedded service ──
# Override the port where the embedded Mux (coder/mux) agent-orchestration
@@ -2167,15 +2239,20 @@ APP_LOG_TO_FILE=true
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
# PROXY_HEALTH_AUTO_DEACTIVATE=false
# Opt-in feature flag (default off; a dashboard DB override wins over this value): show,
# under a proxy pool in the dashboard, how many observed egress IPs served its members over
# the last 24 h and how many connections used them (read-only, computed from the proxy log,
# never used for routing). "true" (or 1, yes) enables it.
# PROXY_POOL_EGRESS_OBSERVATION=false
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags.
# OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK=false
# Rate limit maximum wait time before failing a request (ms). Default: 15000 (15s)
# Rate limit maximum wait time before failing a request (ms). Default: 30000 (30s)
# Used by: open-sse/services/rateLimitManager.ts
# RATE_LIMIT_MAX_WAIT_MS=15000
# RATE_LIMIT_MAX_WAIT_MS=30000
# Limiter-managed execution backstop (Bottleneck `expiration`): bounds a job's
# post-dispatch execution, never queue wait. Must stay ABOVE upstream
@@ -2671,6 +2748,13 @@ APP_LOG_TO_FILE=true
# tokens (accessToken / refreshToken / providerSpecificData). Default OFF —
# only non-credential metadata is synced. See docs/security/SOCKET_DEV_FINDINGS.md §5.
# OMNIROUTE_CLOUD_SYNC_SECRETS=false
#
# Set to "true" to reject an UNSIGNED Cloud sync response when no local secret
# is configured (#13679). Default OFF keeps v3.8.x back-compat for peers that
# have not rotated in a shared secret yet; v3.9 flips the default to enforced.
# A signature that IS present is always verified, and always rejected when
# OMNIROUTE_CLOUD_SYNC_SECRET is unset, regardless of this flag.
# OMNIROUTE_CLOUD_SYNC_ENFORCE_SIGNATURE=false
# ─── Zed import legacy compat (v3.8.6) ──────────────────────────────────────
# Set to "true" to fall back to the v3.8.5 one-step "import everything from
@@ -2721,6 +2805,10 @@ PLAYGROUND_COMPARE_MAX_COLUMNS=4
# MEMORY_VEC_TOP_K=20 # default top-K for vector search
# MEMORY_RRF_K=60 # RRF k constant (sqlite-vec hybrid recipe)
# HF_HUB_ENDPOINT=https://huggingface.co # override Hugging Face Hub base URL for static potion downloads
# Test/diagnostic seam (src/lib/memory/vectorStore.ts) — forces getVectorStore() to
# return null (simulates a cloud/WASM environment without sqlite-vec), degrading
# memory retrieval to FTS5 keyword search. Default off; leave unset in production.
# VECTOR_STORE_DISABLE_VEC=false
# TV6 typed memory decay (OPT-IN, default off — the sweep DELETES decayed memories)
# MEMORY_TYPED_DECAY_ENABLED=false # master switch for the destructive sweep (default off)
# MEMORY_TYPED_DECAY_EPISODIC_DAYS=30 # episodic TTL in days; 0 = episodic immune too
@@ -2962,6 +3050,13 @@ QUOTA_STORE_DRIVER=sqlite
# CHATGPT_WEB_CODEX_CHROME_PATH=/usr/bin/chromium
# CHROME_PATH=/usr/bin/chromium
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
# CDP_PROXY_TOKEN required by docker/chatgpt-web-codex-browser/cdp-proxy.mjs (#13679):
# when set, every request to the CDP proxy sidecar must present it as an
# `X-Omni-Cdp-Token` header. Left unset, the proxy keeps forwarding requests
# unauthenticated (network isolation via docker-compose.yml's dedicated
# `chatgpt-web-codex-net` is the default mitigation). Generate with:
# `openssl rand -hex 32`
# CDP_PROXY_TOKEN=
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex v2
@@ -2990,6 +3085,7 @@ QUOTA_STORE_DRIVER=sqlite
# OMNIROUTE_VNC_READY_MS=45000
# OMNIROUTE_VNC_HARVEST_MS=20000
# OMNIROUTE_VNC_CHROMIUM_ARGS=--remote-debugging-port=9222 --no-first-run --no-default-browser-check
# OMNIROUTE_VNC_NETWORK=omniroute-vnc-browser-login
# ─────────────────────────────────────────────────────────────────────────────
# Data-dir alias (optional — open-sse/services/notionThreadSessions.ts)
@@ -3070,6 +3166,11 @@ QUOTA_STORE_DRIVER=sqlite
# Telegram Mini App bridge. The update endpoint remains disabled while the bot
# token is unset. Used by: src/lib/telegram/* and src/app/api/telegram/update/route.ts.
# TELEGRAM_BOT_TOKEN=
# Shared secret registered with setWebhook and echoed back by Telegram as the
# X-Telegram-Bot-Api-Secret-Token header. REQUIRED for the webhook path: without
# it the webhook is rejected with 503, because an unauthenticated update lets any
# caller mint API keys and spend upstream quota. The Mini App path does not use it.
# TELEGRAM_WEBHOOK_SECRET=
# TELEGRAM_DEFAULT_MODEL=auto/chat
# TELEGRAM_BOT_API_BASE=https://api.telegram.org
# TELEGRAM_WEBHOOK_TIMEOUT_MS=60000

View File

@@ -142,7 +142,14 @@ jobs:
- run: npm run check:known-symbols
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:radar-sentinels
- run: npm run check:tracked-artifacts
# A test parked in vitest.config.ts's exclude list does not run, and looks like
# coverage to whoever reads the tree. 62 files accumulated behind a comment pointing
# at #8618 — closed in August while the list grew to 62; 51 of them passed when
# finally measured (#13204). This gate requires every exclusion to name a tracker and
# to appear in config/quality/vitest-exclusions.json, so the debt stays reviewable.
- run: npm run check:vitest-exclusions
# (gap 30) Also lives in quality.yml's PR-only "Merge integrity" job — because the
# CHANGELOG half of that job needs a base to diff against. This half does NOT: the
# generator either reproduces the committed SKILL.md files or it does not.
@@ -504,9 +511,11 @@ jobs:
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
# Real-translation ratchet: a leaf copied verbatim from en.json passes key
# parity above but is still English to the user (es shipped 55% English).
# Advisory in PR-0; flipped to blocking once the backlog is retranslated (PR-4).
- name: i18n real-translation ratio (advisory)
run: node scripts/i18n/check-translation-ratio.mjs --warn
# Blocking since PR-4 retranslated the verbatim-English backlog: the share of
# untranslated leaves per locale may only fall (ratchet baseline in
# config/quality/i18n-translation-baseline.json; `npm run i18n:check-ratio:update`).
- name: i18n real-translation ratio
run: node scripts/i18n/check-translation-ratio.mjs
# #8463: a rewritten English value used to leave its 39 translations behind
# silently (googleOAuthWarning shipped wrong copy in 39 locales for months).
# Key parity above cannot see it — a stale translation counts as covered.
@@ -514,6 +523,30 @@ jobs:
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: node scripts/i18n/check-ui-value-drift.mjs
# Sibling of the drift gate above. That one catches an English value that was
# REWRITTEN; this one catches an English key that was ADDED while some locales never
# got it. The coverage gate at the top of this job cannot: it is a percentage per
# locale, and 11 absent keys out of ~13,000 leaves coverage at 99.9%. Incident: the
# Phase 3 canvas keys were translated across the 42 locales that existed, then the EU
# batch (#13044) took the repo to 51 and the nine newcomers shipped untranslated.
- name: i18n new-key coverage (a new key must reach every locale)
env:
BASE_REF: ${{ github.base_ref && format('origin/{0}', github.base_ref) || '' }}
run: node scripts/i18n/check-new-key-coverage.mjs
# Absolute complement of the two gates above: every locale must carry exactly the key
# set of en.json, whatever the age of the key. A locale batch is generated from the
# en.json of the day the branch is cut and translates for days while the base keeps
# adding keys — the batch PR adds no key itself, so the new-key gate stays silent and
# 43 absent keys out of ~13,000 still read 99.7 % coverage. Incident 2026-09-15:
# batch 1 (#13044) landed 43 keys short in nine locales, batch 2 (#13660) 10 keys short
# in eight. Fix is `sync-ui-keys --locale=<codes> --translate-markers`.
- name: i18n key completeness (every locale carries every en.json key)
run: node scripts/i18n/check-key-completeness.mjs
# Same gate for the CLI catalogs (bin/cli/locales). check:cli-i18n only compares
# pt-BR / zh-CN / zh-TW; 38 locales shipped with 124 of 830 keys for months
# (audit 2026-09-16) and the CLI silently fell back to English for them.
- name: i18n key completeness (CLI catalogs)
run: node scripts/i18n/check-key-completeness.mjs --catalog=cli
# #8038: cheap glossary/protected-terms consistency gate —
# complements i18n-ui-coverage (key parity) and the ICU `i18n` job below

View File

@@ -10,7 +10,11 @@ name: Radar Export
on:
workflow_dispatch: # o operador pode publicar sob demanda (de qualquer ref)
push:
branches: [main] # produção: só o catálogo do main clobra o asset estável
# `main` e a release ativa (default branch) publicam no mesmo asset estável: o
# radar-server só consome o asset, então um merge de catálogo na release que ficasse
# à espera do cron semanal deixava o feed até 7 dias atrás do README (2026-09-14: a
# linha da Together removida em d6e62ae só saiu do feed com dispatch manual).
branches: [main, "release/**"]
paths:
- open-sse/config/freeModelCatalog.data.ts
- open-sse/config/freeModelCatalog.ts
@@ -19,7 +23,9 @@ on:
- scripts/release/radar-export.mjs
- .github/workflows/radar-export.yml
schedule:
- cron: "17 6 * * 1" # semanal (segunda 06:17 UTC): mantém geradoEm/proveniência frescos
# Diário 03:17 UTC — antes do `radar-feed.timer` do servidor (04:23 UTC), para o ciclo
# do dia já enxergar o export do dia; também mantém geradoEm/proveniência frescos.
- cron: "17 3 * * *"
permissions:
contents: read

View File

@@ -97,4 +97,11 @@
# credential; the generic-api-key rule flags the long hyphenated string.
'''omniroute-cheaperinference-sponsor-banner-dismissed-v\d+''',
'''SunbreakWebUI1''',
# Uzbek dashboard catalog (#13727, src/i18n/messages/uz.json `outputTokenDesc`):
# "Yakunlash/javob tokenlari" = "completion/response tokens". The rule reads the
# `...TokenDesc` key as a token assignment and the translated words as its value.
'''Yakunlash/javob''',
# Feature-flag id from #13439 (src/shared/constants/featureFlagDefinitions.ts):
# `key: "PROTECTED_PRIORITY_INFRA_502_ENABLED"` is a flag name, not a credential.
'''PROTECTED_PRIORITY_INFRA_502_ENABLED''',
]

File diff suppressed because it is too large Load Diff

View File

@@ -56,8 +56,14 @@ explicitly:
}
```
The token can also come from the `OMNIROUTE_MANAGEMENT_API_KEY` environment
variable (the option wins when both are set). Resolution order:
`managementReadToken` option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then the
`apiKey` fallback.
Left unset, `managementReadToken` falls back to `apiKey` for backwards
compatibility. When a gateway rejects that fallback, the catalog still
compatibility, and the plugin warns once at startup that the fallback is
active. When a gateway rejects that fallback, the catalog still
publishes — but with raw model ids instead of display names, no canonical
alias dedupe, no pricing and no combos. The plugin warns once per endpoint
when this happens, naming the endpoint and the consequence, so the degraded
@@ -65,25 +71,25 @@ catalog is never a mystery.
## Options
| Key | Default | Notes |
| -------------------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
| `managementReadToken` | falls back to `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
| `displayName` | `"OmniRoute"` | Provider display name |
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
| Key | Default | Notes |
| -------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `providerId` | `"omniroute"` | Provider id and integration id; models publish under `<providerId>/…` |
| `baseURL` | required | OmniRoute gateway root (no `/v1` suffix needed) |
| `apiKey` | connected credential, then `OMNIROUTE_API_KEY` | Chat key for `/v1/*` — see [Credentials](#credentials) |
| `managementReadToken` | option, then `OMNIROUTE_MANAGEMENT_API_KEY`, then `apiKey` | Management key for `/api/*` (combos, providers, enrichment) — usually **not** the same key |
| `displayName` | `"OmniRoute"` | Provider display name |
| `timeoutMs` | `10000` | Per-endpoint fetch timeout (auto-combos use 5s) |
| `modelCacheTtlMs` | `300000` | Catalog cache TTL; disk snapshot warms cold starts |
| `timeouts` | per-endpoint override | `{ models, combos, autoCombos, enrichment }` in ms; falls back to `timeoutMs` |
| `enrichment` | `true` | Fetch names + pricing (`/api/pricing*`, `/api/free-tier/summary`) |
| `providerTag` | `true` | Prefix a display name with the upstream provider it routes to |
| `geminiSanitization` | `true` | Strip `$schema`/`additionalProperties` from tool schemas sent to Gemini models (`$ref` tools are forwarded untouched) |
| `usableOnly` | `false` | Filter to healthy provisioned providers (`/api/providers`) |
| `visibleModels` / `hiddenModels` | `[]` | Exact-or-suffix allowlists, deny wins |
| `apiFormat.allowAnthropic` | `false` | Route allowlisted ids to the Anthropic API block |
| `apiFormat.anthropicModels` | `[]` | Full model ids routed to Anthropic |
| `apiFormat.anthropicPrefixes` | v1 defaults | Deprecated, warns once — prefer `anthropicModels` |
| `logLevel` / `startupDebug` | `warn` / `false` | Logger verbosity |
## Tool calling on Gemini models

View File

@@ -22,7 +22,7 @@
"node": ">=22.22.3"
},
"peerDependencies": {
"@opencode-ai/plugin": "*"
"@opencode-ai/plugin": ">=1.18.29 <2"
}
},
"node_modules/@ai-sdk/provider": {
@@ -39,9 +39,9 @@
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz",
"integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==",
"cpu": [
"ppc64"
],
@@ -56,9 +56,9 @@
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz",
"integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==",
"cpu": [
"arm"
],
@@ -73,9 +73,9 @@
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz",
"integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==",
"cpu": [
"arm64"
],
@@ -90,9 +90,9 @@
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz",
"integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==",
"cpu": [
"x64"
],
@@ -107,9 +107,9 @@
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz",
"integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==",
"cpu": [
"arm64"
],
@@ -124,9 +124,9 @@
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz",
"integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==",
"cpu": [
"x64"
],
@@ -141,9 +141,9 @@
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz",
"integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==",
"cpu": [
"arm64"
],
@@ -158,9 +158,9 @@
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz",
"integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==",
"cpu": [
"x64"
],
@@ -175,9 +175,9 @@
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz",
"integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==",
"cpu": [
"arm"
],
@@ -192,9 +192,9 @@
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz",
"integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==",
"cpu": [
"arm64"
],
@@ -209,9 +209,9 @@
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz",
"integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==",
"cpu": [
"ia32"
],
@@ -226,9 +226,9 @@
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz",
"integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==",
"cpu": [
"loong64"
],
@@ -243,9 +243,9 @@
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz",
"integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==",
"cpu": [
"mips64el"
],
@@ -260,9 +260,9 @@
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz",
"integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==",
"cpu": [
"ppc64"
],
@@ -277,9 +277,9 @@
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz",
"integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==",
"cpu": [
"riscv64"
],
@@ -294,9 +294,9 @@
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz",
"integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==",
"cpu": [
"s390x"
],
@@ -311,7 +311,9 @@
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.28.1",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz",
"integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==",
"cpu": [
"x64"
],
@@ -326,9 +328,9 @@
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz",
"integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==",
"cpu": [
"arm64"
],
@@ -343,9 +345,9 @@
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz",
"integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==",
"cpu": [
"x64"
],
@@ -360,9 +362,9 @@
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz",
"integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==",
"cpu": [
"arm64"
],
@@ -377,9 +379,9 @@
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz",
"integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==",
"cpu": [
"x64"
],
@@ -394,9 +396,9 @@
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz",
"integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==",
"cpu": [
"arm64"
],
@@ -411,9 +413,9 @@
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz",
"integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==",
"cpu": [
"x64"
],
@@ -428,9 +430,9 @@
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz",
"integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==",
"cpu": [
"arm64"
],
@@ -445,9 +447,9 @@
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz",
"integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==",
"cpu": [
"ia32"
],
@@ -462,9 +464,9 @@
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.28.1",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz",
"integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==",
"cpu": [
"x64"
],
@@ -1180,7 +1182,9 @@
}
},
"node_modules/esbuild": {
"version": "0.28.1",
"version": "0.28.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz",
"integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1191,32 +1195,32 @@
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1",
"@esbuild/android-arm": "0.28.1",
"@esbuild/android-arm64": "0.28.1",
"@esbuild/android-x64": "0.28.1",
"@esbuild/darwin-arm64": "0.28.1",
"@esbuild/darwin-x64": "0.28.1",
"@esbuild/freebsd-arm64": "0.28.1",
"@esbuild/freebsd-x64": "0.28.1",
"@esbuild/linux-arm": "0.28.1",
"@esbuild/linux-arm64": "0.28.1",
"@esbuild/linux-ia32": "0.28.1",
"@esbuild/linux-loong64": "0.28.1",
"@esbuild/linux-mips64el": "0.28.1",
"@esbuild/linux-ppc64": "0.28.1",
"@esbuild/linux-riscv64": "0.28.1",
"@esbuild/linux-s390x": "0.28.1",
"@esbuild/linux-x64": "0.28.1",
"@esbuild/netbsd-arm64": "0.28.1",
"@esbuild/netbsd-x64": "0.28.1",
"@esbuild/openbsd-arm64": "0.28.1",
"@esbuild/openbsd-x64": "0.28.1",
"@esbuild/openharmony-arm64": "0.28.1",
"@esbuild/sunos-x64": "0.28.1",
"@esbuild/win32-arm64": "0.28.1",
"@esbuild/win32-ia32": "0.28.1",
"@esbuild/win32-x64": "0.28.1"
"@esbuild/aix-ppc64": "0.28.2",
"@esbuild/android-arm": "0.28.2",
"@esbuild/android-arm64": "0.28.2",
"@esbuild/android-x64": "0.28.2",
"@esbuild/darwin-arm64": "0.28.2",
"@esbuild/darwin-x64": "0.28.2",
"@esbuild/freebsd-arm64": "0.28.2",
"@esbuild/freebsd-x64": "0.28.2",
"@esbuild/linux-arm": "0.28.2",
"@esbuild/linux-arm64": "0.28.2",
"@esbuild/linux-ia32": "0.28.2",
"@esbuild/linux-loong64": "0.28.2",
"@esbuild/linux-mips64el": "0.28.2",
"@esbuild/linux-ppc64": "0.28.2",
"@esbuild/linux-riscv64": "0.28.2",
"@esbuild/linux-s390x": "0.28.2",
"@esbuild/linux-x64": "0.28.2",
"@esbuild/netbsd-arm64": "0.28.2",
"@esbuild/netbsd-x64": "0.28.2",
"@esbuild/openbsd-arm64": "0.28.2",
"@esbuild/openbsd-x64": "0.28.2",
"@esbuild/openharmony-arm64": "0.28.2",
"@esbuild/sunos-x64": "0.28.2",
"@esbuild/win32-arm64": "0.28.2",
"@esbuild/win32-ia32": "0.28.2",
"@esbuild/win32-x64": "0.28.2"
}
},
"node_modules/fast-check": {
@@ -1786,490 +1790,6 @@
}
}
},
"node_modules/tsup/node_modules/@esbuild/aix-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/android-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/android-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/android-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/darwin-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/darwin-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/freebsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/freebsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-arm": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-loong64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-mips64el": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-ppc64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-riscv64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-s390x": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/linux-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/netbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/netbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/openbsd-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/openbsd-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/openharmony-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/sunos-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/win32-arm64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/win32-ia32": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/@esbuild/win32-x64": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/tsup/node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.27.7",
"@esbuild/android-arm": "0.27.7",
"@esbuild/android-arm64": "0.27.7",
"@esbuild/android-x64": "0.27.7",
"@esbuild/darwin-arm64": "0.27.7",
"@esbuild/darwin-x64": "0.27.7",
"@esbuild/freebsd-arm64": "0.27.7",
"@esbuild/freebsd-x64": "0.27.7",
"@esbuild/linux-arm": "0.27.7",
"@esbuild/linux-arm64": "0.27.7",
"@esbuild/linux-ia32": "0.27.7",
"@esbuild/linux-loong64": "0.27.7",
"@esbuild/linux-mips64el": "0.27.7",
"@esbuild/linux-ppc64": "0.27.7",
"@esbuild/linux-riscv64": "0.27.7",
"@esbuild/linux-s390x": "0.27.7",
"@esbuild/linux-x64": "0.27.7",
"@esbuild/netbsd-arm64": "0.27.7",
"@esbuild/netbsd-x64": "0.27.7",
"@esbuild/openbsd-arm64": "0.27.7",
"@esbuild/openbsd-x64": "0.27.7",
"@esbuild/openharmony-arm64": "0.27.7",
"@esbuild/sunos-x64": "0.27.7",
"@esbuild/win32-arm64": "0.27.7",
"@esbuild/win32-ia32": "0.27.7",
"@esbuild/win32-x64": "0.27.7"
}
},
"node_modules/tsx": {
"version": "4.22.3",
"dev": true,

View File

@@ -63,5 +63,8 @@
},
"peerDependencies": {
"@opencode-ai/plugin": ">=1.18.29 <2"
},
"overrides": {
"esbuild": "^0.28.1"
}
}

View File

@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { homedir } from "node:os";
import { mkdir, readFile, unlink, writeFile } from "node:fs/promises";
import { mkdir, readFile, rename, unlink, writeFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import type {
OmniRouteEnrichmentEntry,
@@ -10,6 +10,7 @@ import type {
OmniRouteRawCombo,
OmniRouteRawModelEntry,
} from "./shared/index.js";
import { isHttpUrl } from "./shared/index.js";
export const DEFAULT_MODEL_CACHE_TTL_MS = 300_000 as const;
@@ -34,8 +35,9 @@ export const SNAPSHOT_FORMAT_VERSION = 2 as const;
/**
* A raw snapshot entry is stale when it cannot be mapped to a publishable
* model: no string `id` (unroutable) or a pre-mapped `api` block without a
* valid `npm` package (the runner would reject it as `Unsupported package`).
* model: no string `id` (unroutable), or a pre-mapped `api` block missing a
* valid `npm` package (the runner would reject it as `Unsupported package`)
* or a usable `url` (the host would reach the AI SDK with no baseURL).
* Plain `/v1/models` entries carry no `api` block -- it is synthesized at
* publish time -- so only a present-but-invalid block drops the entry.
*/
@@ -47,7 +49,11 @@ export function isStaleSnapshotModel(entry: unknown): boolean {
if (api === undefined) return false;
if (!api || typeof api !== "object") return true;
const npm = (api as { npm?: unknown }).npm;
return typeof npm !== "string" || npm.length === 0;
if (typeof npm !== "string" || npm.length === 0) return true;
// Same requirement as `npm`, and the same predicate the options schema
// applies to `baseURL`: a pre-mapped block without a callable `url` publishes
// a model the host cannot route -- see `legacyApiToInfoApi`.
return !isHttpUrl((api as { url?: unknown }).url);
}
interface DiskSnapshotV2 {
@@ -75,6 +81,12 @@ interface DiskSnapshotV2 {
*/
const MAX_SNAPSHOT_BYTES = 32 * 1024 * 1024;
// Suffix for the temp file each write publishes via rename. Monotone per
// process: two writes for one provider (for example across a credential
// rotation) must not share a temp name. Built after the empty-models and
// size-cap guards, so only real attempts consume a value.
let snapshotWriteCounter = 0;
function trimTrailingSlashes(value: string): string {
let i = value.length;
while (i > 0 && value.charCodeAt(i - 1) === 0x2f) i -= 1;
@@ -127,7 +139,7 @@ export async function readDiskSnapshot(
if (
!parsed ||
typeof parsed.v !== "number" ||
parsed.v < SNAPSHOT_FORMAT_VERSION ||
parsed.v !== SNAPSHOT_FORMAT_VERSION ||
typeof parsed.identityFingerprint !== "string" ||
parsed.identityFingerprint !== identityFingerprint
) {
@@ -145,7 +157,7 @@ export async function readDiskSnapshot(
(entry) => !isStaleSnapshotModel(entry)
);
if (stale > 0) {
logger?.warn(`[omniroute-v2] dropping ${stale} stale snapshot entries without api block`);
logger?.warn(`[omniroute-v2] dropping ${stale} stale snapshot entries with an unusable api block`);
}
if (models.length === 0) return undefined;
return {
@@ -173,8 +185,14 @@ export async function readDiskSnapshot(
export async function writeDiskSnapshot(
providerId: string,
snapshot: CatalogSnapshot,
identityFingerprint: string
identityFingerprint: string,
logger?: { warn: (message: string) => void }
): Promise<void> {
// Monotone per-process suffix: two writes for one provider (for example
// across a credential rotation) must not share a temp name. Declared here
// so the catch below can clean it up; assigned after the guards so only
// real attempts consume a counter value.
let tmp = "";
try {
if (snapshot.models.length === 0) return;
const file = diskSnapshotPath(providerId);
@@ -190,14 +208,33 @@ export async function writeDiskSnapshot(
writtenAt: Date.now(),
};
let payload = JSON.stringify(envelope);
if (payload.length > MAX_SNAPSHOT_BYTES && envelope.enrichment !== undefined) {
if (
Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES &&
envelope.enrichment !== undefined
) {
delete envelope.enrichment;
payload = JSON.stringify(envelope);
}
if (payload.length > MAX_SNAPSHOT_BYTES) return;
await writeFile(file, payload, { encoding: "utf8", mode: 0o600 });
} catch {
if (Buffer.byteLength(payload, "utf8") > MAX_SNAPSHOT_BYTES) {
logger?.warn(
`[omniroute-v2] snapshot for ${providerId} exceeds the size cap, skipping disk write`
);
return;
}
tmp = `${file}.${process.pid}.${snapshotWriteCounter++}`;
await writeFile(tmp, payload, { encoding: "utf8", mode: 0o600 });
await rename(tmp, file);
} catch (err) {
// Best-effort: callers already hold the in-memory entry.
logger?.warn(
`[omniroute-v2] snapshot write failed for ${providerId}: ` +
`${err instanceof Error ? err.message : String(err)}, keeping the in-memory entry`
);
try {
await unlink(tmp);
} catch {
// Ignore: the temp file may not exist (mkdir failed first).
}
}
}

View File

@@ -3,6 +3,7 @@ import { type HostContract, detectHostContract, emitsLegacyFields } from "./comp
import type { Model as LegacyModelV2 } from "@opencode-ai/sdk/v2";
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types";
import {
isHttpUrl,
type ApiFormatV2,
type LogLevel,
type Logger,
@@ -142,6 +143,15 @@ export function legacyApiToInfoApi(api: LegacyModelV2["api"]): ModelV2Info["api"
"[omniroute-v2] refusing to publish a model without an api block (missing api.npm)"
);
}
// The host reads `api.url` in `prepareOptions` and never falls back to the
// provider's own, so a model published without one reaches the AI SDK with no
// baseURL and fails at call time with a bare `Invalid URL` — no request on the
// wire, nothing in the gateway logs, no model named.
if (!isHttpUrl(api.url)) {
throw new Error(
"[omniroute-v2] refusing to publish a model whose api block carries no http(s) url"
);
}
return { id: api.id, type: "aisdk", package: api.npm, url: api.url };
}

View File

@@ -31,7 +31,14 @@ import { assertContext } from "./compat.js";
import { type ApiKeyOrigin, resolveApiKey, warnIfMissing } from "./credentials.js";
import { createSourceErrorReporter } from "./enrichment-report.js";
import { sanitizeToolSchemasFor } from "./gemini-language.js";
import { PLUGIN_ID, parsePluginOptions, resolveTimeouts, type PluginOptions } from "./options.js";
import {
MANAGEMENT_TOKEN_ENV_VAR,
PLUGIN_ID,
parsePluginOptions,
resolveManagementReadToken,
resolveTimeouts,
type PluginOptions,
} from "./options.js";
/**
* A fetch result that says whether it succeeded. Returning a bare `[]` on
@@ -61,7 +68,7 @@ function toResolvedOptions(parsed: PluginOptions): ResolvedOptions {
providerId: parsed.providerId,
baseURL: parsed.baseURL,
apiKey: parsed.apiKey ?? process.env.OMNIROUTE_API_KEY ?? "",
managementReadToken: parsed.managementReadToken,
managementReadToken: resolveManagementReadToken(parsed.managementReadToken),
timeoutMs: parsed.timeoutMs,
timeouts: parsed.timeouts,
logLevel: parsed.logLevel,
@@ -93,6 +100,16 @@ export default define({
resolved.logLevel = parsed.logLevel;
resolved.startupDebug = parsed.startupDebug;
log.info(`[omniroute-v2] init providerId=${X}`);
// The inference key stands in below when no management token is set, and
// gateways usually reject that stand-in with 401/403. Say so once here,
// before any fetch, instead of letting the refusal surface per endpoint.
if (resolved.managementReadToken === undefined) {
log.warn(
`[omniroute-v2] no management token configured: management endpoints (/api/*) will reuse the inference key, ` +
`which gateways usually reject with 401/403. Set "managementReadToken" in the plugin options ` +
`or export ${MANAGEMENT_TOKEN_ENV_VAR}.`
);
}
// v1 parity port: in-memory TTL + disk snapshot. The memory key
// `baseURL::sha256(creds)` isolates credential tuples (prod vs
@@ -297,7 +314,7 @@ export default define({
};
if (models.length > 0) {
state.entries.set(cacheKey, snapshot);
await writeDiskSnapshot(X, snapshot, identityFingerprint);
await writeDiskSnapshot(X, snapshot, identityFingerprint, log);
}
void optional.then(
(parts) => upgradeWithOptional(snapshot, parts),
@@ -344,7 +361,7 @@ export default define({
if (unchanged) return;
state.entries.set(cacheKey, upgraded);
if (upgraded.models.length > 0) {
await writeDiskSnapshot(X, upgraded, identityFingerprint);
await writeDiskSnapshot(X, upgraded, identityFingerprint, log);
}
// Reload only when the optional tier actually moved: the catalog
// fingerprint covers ids alone, so without this the host would rebuild

View File

@@ -1,5 +1,7 @@
import { z } from "zod";
import { isHttpUrl } from "./shared/models-map.js";
const apiFormatSchema = z
.object({
allowAnthropic: z.boolean().optional(),
@@ -28,7 +30,10 @@ const pluginOptionsSchema = z
.regex(/^[A-Za-z0-9._-]+$/, "providerId may only contain letters, digits, '.', '_' and '-'")
.refine((v) => v !== "." && v !== "..", "providerId cannot be a path segment")
.default("omniroute"),
baseURL: z.string().url(),
baseURL: z
.string()
.trim()
.refine(isHttpUrl, "baseURL must be an http(s) URL, for example http://localhost:20128"),
apiKey: z.string().optional(),
displayName: z.string().optional(),
managementReadToken: z.string().optional(),
@@ -56,6 +61,21 @@ const pluginOptionsSchema = z
export type PluginOptions = z.infer<typeof pluginOptionsSchema>;
/** Environment source for the management token (option wins over this). */
export const MANAGEMENT_TOKEN_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
/**
* Resolve the management token: a non-empty option wins, then a non-empty
* environment value, else absent. Empty counts as absent on both inputs, the
* same rule the inference key follows; no trimming, the token is opaque.
*/
export function resolveManagementReadToken(optionValue: string | undefined): string | undefined {
if (optionValue !== undefined && optionValue.length > 0) return optionValue;
const fromEnv = process.env[MANAGEMENT_TOKEN_ENV_VAR];
if (fromEnv !== undefined && fromEnv.length > 0) return fromEnv;
return undefined;
}
/** Per-endpoint timeout defaults (v1 parity). `timeoutMs` is the global fallback. */
export const DEFAULT_TIMEOUT_MS = 10_000 as const;
/** Auto-combos keep the v1 5s budget; the field is resolved now for the P3 port. */

View File

@@ -111,6 +111,22 @@ function trimTrailingSlashes(value: string): string {
* (it appends `/v1/messages` automatically), so callers should branch on
* format first.
*/
/**
* A url the AI SDK can actually call. `new URL()` alone is not enough: it
* parses `localhost:20128` as the scheme `localhost:` and `ftp://host` as ftp,
* both of which reach `fetch` and fail there. Mirrors the `isHttpUrl` guard the
* settings schema applies to `headroomUrl`.
*/
export function isHttpUrl(value: unknown): boolean {
if (typeof value !== "string") return false;
try {
const { protocol } = new URL(value);
return protocol === "http:" || protocol === "https:";
} catch {
return false;
}
}
export function ensureV1Suffix(url: string): string {
const trimmed = trimTrailingSlashes(url);
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;

View File

@@ -0,0 +1,251 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
existsSync,
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import {
diskSnapshotPath,
readDiskSnapshot,
writeDiskSnapshot,
type CatalogSnapshot,
} from "../src/cache.js";
function isolateDisk(): { dir: string; restore: () => void } {
const dir = mkdtempSync(join(tmpdir(), "omniroute-disk-atomic-"));
const prev = process.env.OPENCODE_DATA_DIR;
process.env.OPENCODE_DATA_DIR = dir;
return {
dir,
restore: () => {
if (prev === undefined) delete process.env.OPENCODE_DATA_DIR;
else process.env.OPENCODE_DATA_DIR = prev;
},
};
}
function makeSnapshot(models: string[] = ["m-a"]): CatalogSnapshot {
return {
models: models.map((id) => ({ id })),
combos: [],
autoCombos: [],
providers: [],
fetchedAt: Date.now(),
} as unknown as CatalogSnapshot;
}
function makeLogger() {
const messages: string[] = [];
return {
messages,
logger: { warn: (message: string) => void messages.push(message) },
};
}
// Entries next to the destination other than the destination itself: any
// leftover temp file after a successful write shows up here.
function strayEntries(file: string): string[] {
let entries: string[];
try {
entries = readdirSync(dirname(file));
} catch {
return [];
}
return entries.filter((entry) => entry !== file.split("/").pop());
}
// The writer names its temp file `${file}.${pid}.${counter}` with a
// module-monotone counter starting at 0, built after the empty-models and
// size-cap guards (an over-cap call consumes no counter value). Tests in this
// file run sequentially in one process, so the attempt table below predicts
// every temp path exactly:
// over-cap: no counter use | failed write A: 0, failed write B: 1 |
// interrupted overwrite A: 2, interrupted overwrite B: 3 | mkdir failure: 4 |
// truncated read: 5 | success: 6 | permissions: 7 | round-trip: 8, 9.
function predictedTmp(file: string, counter: number): string {
return `${file}.${process.pid}.${counter}`;
}
describe("disk snapshot atomic write, strict version, traced give-ups", () => {
it("ignores a newer snapshot version without throwing", async () => {
const disk = isolateDisk();
try {
const file = diskSnapshotPath("t1-future");
mkdirSync(dirname(file), { recursive: true });
// A writer from the future persists version 3; this reader must
// treat it as "no snapshot" instead of trusting unknown data.
writeFileSync(
file,
JSON.stringify({
v: 3,
identityFingerprint: "fp-1",
models: [{ id: "m-future" }],
combos: [],
writtenAt: Date.now(),
})
);
const back = await readDiskSnapshot("t1-future", "fp-1");
assert.equal(back, undefined);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("traces an over-cap write and leaves no destination behind", async () => {
const disk = isolateDisk();
try {
const { messages, logger } = makeLogger();
const bigId = `huge-${"x".repeat(33 * 1024 * 1024)}`;
await writeDiskSnapshot("t2-cap", makeSnapshot([bigId]), "fp-1", logger);
const file = diskSnapshotPath("t2-cap");
assert.equal(existsSync(file), false);
assert.deepEqual(strayEntries(file), []);
assert.match(messages.join("\n"), /exceeds|too large|size cap/i);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a failed write leaves no destination behind and is traced", async () => {
const disk = isolateDisk();
const file = diskSnapshotPath("t3a-fail");
const blocker = predictedTmp(file, 1);
try {
const { messages, logger } = makeLogger();
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-before"]), "fp-1", logger);
// Plant a directory at the next temp path: the write fails with
// EISDIR before any rename, deterministically, on every platform.
mkdirSync(dirname(file), { recursive: true });
mkdirSync(blocker, { recursive: true });
await writeDiskSnapshot("t3a-fail", makeSnapshot(["m-after"]), "fp-1", logger);
assert.equal(existsSync(file), true);
const back = await readDiskSnapshot("t3a-fail", "fp-1");
assert.deepEqual(
(back?.models ?? []).map((entry) => entry.id),
["m-before"]
);
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
} finally {
rmSync(blocker, { recursive: true, force: true });
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("an interrupted overwrite keeps the previous snapshot", async () => {
const disk = isolateDisk();
const file = diskSnapshotPath("t3b-keep");
const blocker = predictedTmp(file, 3);
try {
const { messages, logger } = makeLogger();
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-before"]), "fp-1", logger);
const before = readFileSync(file, "utf8");
mkdirSync(blocker, { recursive: true });
await writeDiskSnapshot("t3b-keep", makeSnapshot(["m-after"]), "fp-1", logger);
assert.equal(readFileSync(file, "utf8"), before);
const back = await readDiskSnapshot("t3b-keep", "fp-1");
assert.deepEqual(
(back?.models ?? []).map((entry) => entry.id),
["m-before"]
);
assert.match(messages.join("\n"), /failed|EISDIR|error/i);
} finally {
rmSync(blocker, { recursive: true, force: true });
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a mkdir failure is traced and writes nothing", async () => {
const disk = isolateDisk();
try {
const { messages, logger } = makeLogger();
// A file planted at the plugins path makes mkdir fail
// deterministically (EEXIST on mkdir, ENOTDIR on direct writeFile).
writeFileSync(join(disk.dir, "plugins"), "blocker");
await writeDiskSnapshot("t3b-bis", makeSnapshot(["m-a"]), "fp-1", logger);
assert.equal(existsSync(diskSnapshotPath("t3b-bis")), false);
assert.match(messages.join("\n"), /failed|EEXIST|ENOTDIR|error/i);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a truncated file reads as no snapshot without throwing", async () => {
const disk = isolateDisk();
try {
const { logger } = makeLogger();
await writeDiskSnapshot("t4-truncated", makeSnapshot(["m-a"]), "fp-1", logger);
const file = diskSnapshotPath("t4-truncated");
const full = readFileSync(file, "utf8");
writeFileSync(file, full.slice(0, Math.floor(full.length / 2)));
const back = await readDiskSnapshot("t4-truncated", "fp-1");
assert.equal(back, undefined);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("a successful write leaves no entry but the destination", async () => {
const disk = isolateDisk();
try {
await writeDiskSnapshot("t5-clean", makeSnapshot(["m-a"]), "fp-1");
const file = diskSnapshotPath("t5-clean");
assert.deepEqual(strayEntries(file), []);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("the replaced snapshot stays owner-only", async (t) => {
if (process.platform === "win32") {
t.skip("file mode semantics are POSIX-only");
return;
}
const disk = isolateDisk();
try {
await writeDiskSnapshot("t6-mode", makeSnapshot(["m-a"]), "fp-1");
const file = diskSnapshotPath("t6-mode");
assert.equal((statSync(file).mode & 0o077) === 0, true);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
it("round-trips a valid snapshot with and without a logger", async () => {
const disk = isolateDisk();
try {
const { logger } = makeLogger();
const snapshot = makeSnapshot(["m-a"]);
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1");
const plain = await readDiskSnapshot("t7-roundtrip", "fp-1");
assert.deepEqual(
(plain?.models ?? []).map((entry) => entry.id),
["m-a"]
);
await writeDiskSnapshot("t7-roundtrip", snapshot, "fp-1", logger);
const logged = await readDiskSnapshot("t7-roundtrip", "fp-1", logger);
assert.deepEqual(
(logged?.models ?? []).map((entry) => entry.id),
["m-a"]
);
} finally {
disk.restore();
rmSync(disk.dir, { recursive: true, force: true });
}
});
});

View File

@@ -6,6 +6,33 @@ interface CapturedCall {
kind: "catalog" | "integration";
}
/**
* Wait until `read()` stops changing, then return the settled value.
*
* The plugin's optional tier lands asynchronously after a publish. Waiting for
* it with a fixed `sleep(5)` raced the work: under load the tier arrived after
* the sleep, so the *next* assertion counted its reload and read 2 where it
* expected 1. Polling until the value holds steady for a few consecutive turns
* ties the wait to the work instead of to the clock.
*/
async function settle<T>(read: () => T, quietTurns = 3, timeoutMs = 5000): Promise<T> {
const { setTimeout: sleep } = await import("node:timers/promises");
const deadline = Date.now() + timeoutMs;
let last = read();
let stable = 0;
while (stable < quietTurns && Date.now() < deadline) {
await sleep(5);
const current = read();
if (current === last) {
stable += 1;
} else {
last = current;
stable = 0;
}
}
return last;
}
interface FakeCtx {
options: Record<string, unknown>;
catalog: {
@@ -163,7 +190,6 @@ describe("plugin-v2 entrypoint", () => {
const { mkdtempSync } = await import("node:fs");
const { tmpdir } = await import("node:os");
const { join } = await import("node:path");
const { setTimeout: sleep } = await import("node:timers/promises");
const dir = mkdtempSync(join(tmpdir(), "omniroute-lazy-"));
const prevDataDir = process.env.OPENCODE_DATA_DIR;
process.env.OPENCODE_DATA_DIR = dir;
@@ -222,16 +248,15 @@ describe("plugin-v2 entrypoint", () => {
await cb(draft);
assert.equal(reloads, 0, "the first publish sets the baseline, it does not reload");
assert.equal(modelsCall, 1);
await sleep(5);
// The optional tier lands after that first publish and brings combos and
// the overlay with it — one reload, so the picker shows them without
// waiting for the next refresh.
const afterFirstUpgrade = reloads;
const afterFirstUpgrade = await settle(() => reloads);
assert.ok(afterFirstUpgrade <= 1, `at most one reload for the first upgrade, got ${reloads}`);
await cb(draft);
assert.equal(reloads, afterFirstUpgrade + 1, "a new model id reloads once");
assert.equal(modelsCall, 2);
await sleep(5);
await settle(() => reloads);
await cb(draft);
assert.equal(reloads, afterFirstUpgrade + 1, "an identical run never reloads");
assert.equal(modelsCall, 3);

View File

@@ -0,0 +1,371 @@
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { mkdtempSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import plugin from "../src/index.js";
import { publishCatalog } from "../src/catalog.js";
import type { CatalogDraft } from "@opencode-ai/plugin/v2/promise";
import type { ModelV2Info, ProviderV2Info } from "@opencode-ai/sdk/v2/types";
const MODELS_URL = "https://gw.example.com/v1/models";
const COMBOS_URL = "https://gw.example.com/api/combos";
const PRICING_MODELS_URL = "https://gw.example.com/api/pricing/models";
const MGMT_ENV_VAR = "OMNIROUTE_MANAGEMENT_API_KEY";
const INFERENCE_ENV_VAR = "OMNIROUTE_API_KEY";
function okJson(body: unknown) {
return { ok: true, status: 200, statusText: "OK", json: async () => body };
}
interface Harness {
seen: Map<string, string>;
warns: string[];
restore: () => void;
}
function installHarness(combos: unknown[]): Harness {
const seen = new Map<string, string>();
const warns: string[] = [];
const origFetch = globalThis.fetch;
const origWarn = console.warn;
const origLog = console.log;
const origError = console.error;
console.warn = (...args: unknown[]) => {
warns.push(String(args[0]));
};
console.log = () => {};
console.error = (...args: unknown[]) => {
warns.push(String(args[0]));
};
globalThis.fetch = (async (url: unknown, init?: { headers?: Record<string, string> }) => {
const href = String(url);
seen.set(href, String(init?.headers?.Authorization ?? ""));
if (href.includes("/api/combos/auto")) return okJson({ combos: [] });
if (href.includes("/api/pricing/models")) {
return okJson({
providers: {
demo: {
id: "demo",
name: "Demo",
models: [{ id: "team-combo", name: "Team Combo" }],
},
},
});
}
if (href.includes("/api/pricing")) return okJson({});
if (href.includes("/api/free-tier/summary")) return okJson({ perModel: [] });
if (href.includes("/api/combos")) return okJson({ combos });
return okJson({ data: [{ id: "m1" }] });
}) as typeof fetch;
return {
seen,
warns,
restore() {
globalThis.fetch = origFetch;
console.warn = origWarn;
console.log = origLog;
console.error = origError;
},
};
}
async function withIsolatedEnv<T>(
mgmt: string | undefined,
inference: string | undefined,
fn: () => Promise<T>
): Promise<T> {
const prevMgmt = process.env[MGMT_ENV_VAR];
const prevInference = process.env[INFERENCE_ENV_VAR];
// Like tests/management-token.test.ts:176-180: a fresh OPENCODE_DATA_DIR
// per case keeps the real disk snapshot out of the run, so a filtered 'it'
// never gets a warm snapshot served without fetch.
const prevDataDir = process.env.OPENCODE_DATA_DIR;
process.env.OPENCODE_DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-mgmt-env-"));
if (mgmt === undefined) delete process.env[MGMT_ENV_VAR];
else process.env[MGMT_ENV_VAR] = mgmt;
if (inference === undefined) delete process.env[INFERENCE_ENV_VAR];
else process.env[INFERENCE_ENV_VAR] = inference;
try {
return await fn();
} finally {
if (prevDataDir === undefined) delete process.env.OPENCODE_DATA_DIR;
else process.env.OPENCODE_DATA_DIR = prevDataDir;
if (prevMgmt === undefined) delete process.env[MGMT_ENV_VAR];
else process.env[MGMT_ENV_VAR] = prevMgmt;
if (prevInference === undefined) delete process.env[INFERENCE_ENV_VAR];
else process.env[INFERENCE_ENV_VAR] = prevInference;
}
}
function setupHarness(options: Record<string, unknown>) {
const catalogCallbacks: Array<(draft: unknown) => Promise<void>> = [];
const ctx = {
options,
catalog: {
transform: (cb: (draft: unknown) => Promise<void>) => {
catalogCallbacks.push(cb);
return Promise.resolve({ dispose: async () => {} });
},
},
integration: {
transform: () => Promise.resolve({ dispose: async () => {} }),
},
};
return { catalogCallbacks, ctx };
}
function stubDraft() {
const published = new Map<string, Record<string, unknown>>();
const draft = {
provider: { update: (_id: string, fn: (p: Record<string, unknown>) => void) => fn({}) },
model: {
update: (pid: string, mid: string, fn: (m: Record<string, unknown>) => void) => {
const key = pid + "/" + mid;
let entry = published.get(key);
if (entry === undefined) {
entry = { id: mid, providerID: pid };
published.set(key, entry);
}
fn(entry);
},
},
};
return { draft, published };
}
function fallbackWarns(warns: string[]): string[] {
return warns.filter((w) => w.includes("managementReadToken"));
}
async function runSetup(ctx: unknown): Promise<void> {
await (plugin as unknown as { setup: (ctx: unknown) => Promise<void> }).setup(ctx);
}
describe("plugin-v2 management token environment source", () => {
it("uses the managementReadToken option for /api/* while models keep apiKey", async () => {
await withIsolatedEnv(undefined, undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("reads the management token from the environment when the option is absent", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
assert.equal(h.seen.get(MODELS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("prefers the option over the environment", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
} finally {
h.restore();
}
});
});
it("falls back to the inference key with a single early warning when neither is set", async () => {
await withIsolatedEnv(undefined, undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
});
await runSetup(ctx);
const atSetup = fallbackWarns(h.warns);
assert.equal(
atSetup.length,
1,
`expected exactly one early fallback warning, got: ${JSON.stringify(h.warns)}`
);
assert.match(atSetup[0] ?? "", /managementReadToken/);
assert.match(atSetup[0] ?? "", new RegExp(MGMT_ENV_VAR));
assert.ok(!(atSetup[0] ?? "").includes("chat-key"), "warning must not leak the key");
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
assert.equal(
fallbackWarns(h.warns).length,
1,
"the fallback warning stays a single setup-time notice"
);
} finally {
h.restore();
}
});
});
it("treats an empty option as absent so the environment wins", async () => {
await withIsolatedEnv("mgmt-env-token", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-env-token");
} finally {
h.restore();
}
});
});
it("treats an empty environment value as absent so the option wins", async () => {
await withIsolatedEnv("", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "mgmt-option-token",
});
await runSetup(ctx);
assert.deepEqual(fallbackWarns(h.warns), []);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer mgmt-option-token");
} finally {
h.restore();
}
});
});
it("falls back with a warning when both the option and the environment are empty", async () => {
await withIsolatedEnv("", undefined, async () => {
const h = installHarness([]);
try {
const { catalogCallbacks, ctx } = setupHarness({
baseURL: "https://gw.example.com",
providerId: "omniroute",
apiKey: "chat-key",
managementReadToken: "",
});
await runSetup(ctx);
assert.equal(fallbackWarns(h.warns).length, 1);
const { draft } = stubDraft();
await catalogCallbacks[0](draft);
assert.equal(h.seen.get(COMBOS_URL), "Bearer chat-key");
} finally {
h.restore();
}
});
});
it("enriches the catalog from the environment token alone", async () => {
const providers = new Map<string, ProviderV2Info>();
const models = new Map<string, ModelV2Info>();
const draft = {
provider: {
list: () => [],
get: (id: string) => providers.get(id) as never,
update: (id: string, fn: (p: ProviderV2Info) => void) => {
const p = (providers.get(id) ?? { id }) as ProviderV2Info;
fn(p);
providers.set(id, p);
},
remove: () => {},
},
model: {
get: () => undefined,
update: (pid: string, mid: string, fn: (m: ModelV2Info) => void) => {
const k = pid + "/" + mid;
const m = (models.get(k) ?? { id: mid, providerID: pid }) as ModelV2Info;
fn(m);
models.set(k, m);
},
remove: () => {},
default: { get: () => undefined, set: () => {} },
},
} as unknown as CatalogDraft;
let seenCombos = "";
let seenPricing = "";
const res = await withIsolatedEnv("mgmt-env-token", undefined, async () =>
publishCatalog(
draft,
{
providerId: "omniroute",
baseURL: "https://gw.example.com",
apiKey: "chat-key",
managementReadToken: process.env[MGMT_ENV_VAR],
timeoutMs: 1000,
modelCacheTtlMs: 300000,
usableOnly: false,
},
{
fetcher: async () => [{ id: "m1" }],
combosFetcher: async (_base, token) => {
seenCombos = token;
return [{ id: "team-combo", models: [{ kind: "model", model: "m1" }] }];
},
enrichmentFetcher: async (_base, token) => {
seenPricing = token;
// The process env is the source under test: the resolver output
// flows in through the option above, so report success only when
// the flow under test actually carried it.
if (token !== "mgmt-env-token") return new Map();
return new Map([["team-combo", { name: "Team Combo" }]]);
},
}
)
);
assert.deepEqual(res, { models: 1, combos: 1, autoCombos: 0 });
assert.equal(seenCombos, "mgmt-env-token");
assert.equal(seenPricing, "mgmt-env-token");
const entry = models.get("omniroute/team-combo");
assert.ok(entry, "expected the combo entry in the published catalog");
assert.equal(entry?.name, "Team Combo");
});
});

View File

@@ -29,6 +29,38 @@ describe("parsePluginOptions", () => {
it("requires baseURL", () => {
assert.throws(() => parsePluginOptions({}), /baseURL/);
});
it("rejects a baseURL that is not an http(s) URL", () => {
// `new URL()` reads "localhost:20128" as the scheme "localhost:" followed
// by a path, so a gateway address typed without "http://" parses. Every
// model would then be published with "localhost:20128/v1" as its api url
// and every call would fail in the client on an unknown scheme, with no
// request on the wire and nothing in the gateway logs.
for (const baseURL of [
"localhost:20128",
"localhost:20128/v1",
"ftp://gw.example.com/v1",
"gw.example.com/v1",
]) {
assert.throws(
() => parsePluginOptions({ baseURL }),
/baseURL must be an http\(s\) URL/,
`expected ${baseURL} to be rejected`
);
}
});
it("accepts http and https baseURLs, with or without a port or path", () => {
for (const baseURL of [
"http://localhost:20128/v1",
"http://localhost:20128",
"https://gw.example.com/v1",
"https://gw.example.com/omniroute/v1",
]) {
assert.equal(parsePluginOptions({ baseURL }).baseURL, baseURL);
// Padding a copied address is trimmed rather than rejected, matching the
// treatment `headroomUrl` already gets in the settings schema.
assert.equal(parsePluginOptions({ baseURL: ` ${baseURL} ` }).baseURL, baseURL);
}
});
it("rejects unknown top-level keys (strict)", () => {
assert.throws(() => parsePluginOptions({ baseURL: "https://gw.example.com", bogus: 1 }));
});

View File

@@ -5,7 +5,11 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { createHash } from "node:crypto";
import plugin from "../src/index.js";
import { diskSnapshotPath, snapshotIdentityFingerprint } from "../src/cache.js";
import {
diskSnapshotPath,
isStaleSnapshotModel,
snapshotIdentityFingerprint,
} from "../src/cache.js";
import { legacyApiToInfoApi } from "../src/catalog.js";
function isolateDisk(): { dir: string; restore: () => void } {
@@ -97,7 +101,7 @@ function downFetch(): typeof fetch {
const fingerprint = snapshotIdentityFingerprint("https://gw.example.com", "k-snapfix", "k-snapfix");
describe("plugin-v2 snapshot stale-entry filter", () => {
it("snapshot with 2 entries without api block + 1 valid: only the valid one is published + warn emitted", async () => {
it("snapshot with 3 unusable pre-mapped entries + 1 valid: only the valid one is published + warn emitted", async () => {
const disk = isolateDisk();
const providerId = "snapfix-mixed";
mkdirSync(join(disk.dir, "plugins"), { recursive: true });
@@ -106,11 +110,15 @@ describe("plugin-v2 snapshot stale-entry filter", () => {
JSON.stringify({
v: 2,
identityFingerprint: fingerprint,
// Two pre-mapped entries with a broken api block (missing npm) plus
// one plain raw entry (no api block: synthesized at publish time).
// Three pre-mapped entries with an unusable api block missing npm,
// empty npm, and a well-formed npm with no url (the shape a snapshot
// written by an older build carries, and the one that reaches the host
// as a bare `Invalid URL`) — plus one plain raw entry, which has no api
// block at all and gets one synthesized at publish time.
models: [
{ id: "stale-a", api: {} },
{ id: "stale-b", api: { npm: "" } },
{ id: "stale-c", api: { id: "openai-compatible", npm: "@ai-sdk/openai-compatible" } },
{ id: "good-1", context_length: 128000 },
],
combos: [],
@@ -137,7 +145,7 @@ describe("plugin-v2 snapshot stale-entry filter", () => {
);
});
assert.ok(
warns.some((w) => w.includes("dropping 2 stale snapshot entries without api block")),
warns.some((w) => w.includes("dropping 3 stale snapshot entries with an unusable api block")),
`expected stale-drop warn, got: ${JSON.stringify(warns)}`
);
} finally {
@@ -216,4 +224,56 @@ describe("plugin-v2 snapshot stale-entry filter", () => {
// Sanity: sha256 helper used above matches the plugin identity scheme.
assert.equal(createHash("sha256").update("x").digest("hex").length, 64);
});
it("legacyApiToInfoApi throws unless api.url is an http(s) url", () => {
const npm = "@ai-sdk/openai-compatible";
for (const api of [
{ id: "openai-compatible", npm },
{ id: "openai-compatible", npm, url: "" },
{ id: "openai-compatible", npm, url: " " },
// Non-empty but uncallable: the AI SDK reaches `fetch` and fails there.
{ id: "openai-compatible", npm, url: "/v1" },
{ id: "openai-compatible", npm, url: "gw.example.com/v1" },
{ id: "openai-compatible", npm, url: "ftp://gw.example.com/v1" },
]) {
assert.throws(
() => legacyApiToInfoApi(api as unknown as { id: string; npm: string; url: string }),
/api block carries no http\(s\) url/,
`expected a publish-time refusal for ${JSON.stringify(api)}`
);
}
// A complete block still publishes unchanged.
assert.deepEqual(
legacyApiToInfoApi({
id: "openai-compatible",
npm: "@ai-sdk/openai-compatible",
url: "https://gw.example.com/v1",
}),
{
id: "openai-compatible",
type: "aisdk",
package: "@ai-sdk/openai-compatible",
url: "https://gw.example.com/v1",
}
);
});
it("isStaleSnapshotModel drops a pre-mapped entry whose api.url is unusable", () => {
const npm = "@ai-sdk/openai-compatible";
// Present-but-unusable url: stale, for the same reason a missing npm is.
for (const url of [undefined, "", " ", "/v1", "gw.example.com/v1", "ftp://gw/v1"]) {
assert.equal(
isStaleSnapshotModel({ id: "a/b", api: { id: "x", npm, ...(url === undefined ? {} : { url }) } }),
true,
`expected ${JSON.stringify(url)} to be treated as stale`
);
}
// Complete block: publishable.
assert.equal(
isStaleSnapshotModel({ id: "a/b", api: { id: "x", npm, url: "https://gw/v1" } }),
false
);
// No api block at all stays publishable: it is synthesized at publish time.
assert.equal(isStaleSnapshotModel({ id: "a/b" }), false);
});
});

View File

@@ -1745,9 +1745,9 @@
}
},
"node_modules/toml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz",
"integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==",
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/toml/-/toml-4.3.0.tgz",
"integrity": "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==",
"dev": true,
"license": "MIT",
"engines": {

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/telemetry.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/feature-defaults.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts tests/management-read-token.test.ts tests/auto-sync.test.ts tests/model-allowlist.test.ts tests/log-level.test.ts tests/effort-tier-variants.test.ts tests/naming.test.ts tests/free-budget-magnitude.test.ts tests/models-fetcher.test.ts tests/issue-13000-cold-start-combo-limit.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [
@@ -68,6 +68,7 @@
"typescript": "^5.9.3"
},
"overrides": {
"esbuild": "^0.28.1"
"esbuild": "^0.28.1",
"toml": "^4.1.2"
}
}

View File

@@ -220,7 +220,11 @@ const optionsSchema = z
* to 60000. Default when unset: 300000.
*/
autoSyncIntervalMs: z.number().int().nonnegative().optional(),
baseURL: z.string().url().optional(),
baseURL: z
.string()
.trim()
.refine(isHttpUrl, "baseURL must be an http(s) URL, for example http://localhost:20128")
.optional(),
managementReadToken: z.string().min(1).optional(),
features: featuresSchema.optional(),
})
@@ -482,6 +486,22 @@ export const DEFAULT_ANTHROPIC_PREFIXES = ["cc", "claude", "anthropic", "kiro",
* (it appends `/v1/messages` automatically), so callers should branch on
* format first.
*/
/**
* A url the AI SDK can actually call. `new URL()` alone is not enough: it
* parses `localhost:20128` as the scheme `localhost:` and `ftp://host` as ftp,
* both of which reach `fetch` and fail there. Mirrors the `isHttpUrl` guard the
* settings schema applies to `headroomUrl`.
*/
export function isHttpUrl(value: unknown): boolean {
if (typeof value !== "string") return false;
try {
const { protocol } = new URL(value);
return protocol === "http:" || protocol === "https:";
} catch {
return false;
}
}
export function ensureV1Suffix(url: string): string {
const trimmed = trimTrailingSlashes(url);
return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
@@ -4611,9 +4631,21 @@ export function buildStaticProviderEntry(
.map((m) => m.max_output_tokens)
.filter((v): v is number => typeof v === "number" && v > 0);
if (contextValues.length > 0 && outputValues.length > 0) {
// Prefer the server-computed aggregate (accounts for explicit
// context_length overrides and members outside memberEntries, e.g.
// not yet resolved in /v1/models) over the raw Math.min(member)
// lower bound. Mirrors mapComboToModelV2's limit.context logic
// (#13000) so the static catalog and the dynamic hook agree.
const preferredContext =
typeof combo.computed_context_length === "number" && combo.computed_context_length > 0
? combo.computed_context_length
: contextValues.length > 0
? Math.min(...contextValues)
: undefined;
if (preferredContext !== undefined && outputValues.length > 0) {
entry.limit = {
context: Math.min(...contextValues),
context: preferredContext,
output: Math.min(...outputValues),
};
}
@@ -5491,6 +5523,32 @@ export function createOmniRouteConfigHook(
const modelsFetchOk = !modelsFetchThrew && localRawModels.length > 0;
// Snapshot backfill for computed_context_length: a live /api/combos
// response can come back without this field (server hasn't finished
// recomputing it yet, e.g. just after a restart) even though the
// combo's members and identity are otherwise unchanged. When that
// happens, prefer the last-known-good value from the warm disk
// snapshot over the Math.min(member) fallback in
// mapComboToModelV2() — never overwrite any other combo field
// (models/name/etc.) with stale data, only this one derived number.
if (warmSnapshot) {
const snapshotComboById = new Map(warmSnapshot.rawCombos.map((c) => [c.id, c]));
for (const combo of localRawCombos) {
const hasLive =
typeof combo.computed_context_length === "number" &&
combo.computed_context_length > 0;
if (hasLive) continue;
const stale = snapshotComboById.get(combo.id);
if (
stale &&
typeof stale.computed_context_length === "number" &&
stale.computed_context_length > 0
) {
combo.computed_context_length = stale.computed_context_length;
}
}
}
// Disk-cache fallback (cold first run, no warm snapshot): when the
// live fetch returned no models AND features.diskCache !== false,
// hydrate from the last-known-good snapshot so OC still surfaces a

View File

@@ -0,0 +1,221 @@
/**
* Repro for #13000: combo context limits fall back to Math.min(member)
* instead of using computed_context_length after cold start — no disk
* snapshot fallback.
*
* Scenario (mirrors the report): a warm disk snapshot holds the combo with
* its correct server-computed `computed_context_length` (245000, from all 6
* members). After a restart, the live refresh's combos fetch returns the
* SAME combo but without `computed_context_length` (e.g. the value hasn't
* propagated yet), and the live models fetch only resolves 2 of the 6
* members (the rest not yet in /v1/models). The background refresh then
* republishes the provider block built from this degraded live data,
* downgrading a previously-known-good 245000 limit to Math.min(163840,
* 1_000_000) = 163840 — exactly the member-minimum described in the issue.
*/
import test from "node:test";
import assert from "node:assert/strict";
import type { Config } from "@opencode-ai/plugin";
import {
createOmniRouteConfigHook,
_resetInflightRefresh,
type OmniRouteAutoCombosFetcher,
type OmniRouteCombosFetcher,
type OmniRouteCompressionMetaFetcher,
type OmniRouteEnrichmentFetcher,
type OmniRouteFetchCache,
type OmniRouteModelsFetcher,
type OmniRouteProvidersFetcher,
type OmniRouteRawCombo,
type OmniRouteRawModelEntry,
type OmniRouteReadAuthJson,
type OmniRouteStaticProviderEntry,
type OmniRouteDiskSnapshotReader,
type OmniRouteDiskSnapshotWriter,
} from "../src/index.js";
test.beforeEach(() => {
_resetInflightRefresh();
});
function stubReadAuthJson(value: Record<string, unknown>): OmniRouteReadAuthJson {
return async () => value as never;
}
function authStub() {
return stubReadAuthJson({
"opencode-omniroute": {
type: "api",
key: "sk-test",
baseURL: "https://or.example.com/v1",
},
});
}
function makeInput(): Config {
return { provider: {} } as unknown as Config;
}
// The two members resolvable in the degraded live /v1/models response.
const MEMBER_DEEPSEEK: OmniRouteRawModelEntry = {
id: "deepseek-v4-pro",
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
context_length: 163_840,
max_output_tokens: 64_000,
input_modalities: ["text"],
output_modalities: ["text"],
};
const MEMBER_GLM: OmniRouteRawModelEntry = {
id: "glm-5.2",
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
context_length: 1_000_000,
max_output_tokens: 16_384,
input_modalities: ["text"],
output_modalities: ["text"],
};
// The other member that IS present once the server is fully warm.
const MEMBER_GLM_53_HIGH: OmniRouteRawModelEntry = {
id: "GLM-5.3-high",
capabilities: { tool_calling: true, reasoning: true, vision: false, thinking: false },
context_length: 245_000,
max_output_tokens: 128_000,
input_modalities: ["text"],
output_modalities: ["text"],
};
const COMBO_MODELS: OmniRouteRawCombo["models"] = [
{ kind: "model", model: "deepseek-v4-pro", weight: 25 },
{ kind: "model", model: "glm-5.2", weight: 25 },
{ kind: "model", model: "GLM-5.3-high", weight: 50 },
];
test("issue #13000: warm combo limit (245000) survives a degraded post-restart refresh instead of downgrading to Math.min(member)", async () => {
const warmSnapshot: Omit<import("../src/index.js").OmniRouteFetchCacheEntry, "expiresAt"> = {
rawModels: [MEMBER_DEEPSEEK, MEMBER_GLM, MEMBER_GLM_53_HIGH],
rawCombos: [
{
id: "orchestrator",
name: "orchestrator",
models: COMBO_MODELS,
computed_context_length: 245_000,
},
],
rawAutoCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
};
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
const combosFetcher: OmniRouteCombosFetcher = async () => [
{
id: "orchestrator",
name: "orchestrator",
models: COMBO_MODELS,
// computed_context_length intentionally omitted.
},
];
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => warmSnapshot;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const sharedCache: OmniRouteFetchCache = new Map();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", modelCacheTtl: 60_000 },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
autoCombosFetcher,
enrichmentFetcher,
compressionMetaFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
cache: sharedCache,
}
);
const input = makeInput();
await hook(input);
// Let the detached background refresh (degraded live data) complete and
// republish the block.
await new Promise((r) => setTimeout(r, 100));
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const comboModelAfter = entryAfter.models["orchestrator"];
assert.ok(comboModelAfter, "combo model still published after refresh");
assert.equal(
comboModelAfter.limit.context,
245_000,
`expected the combo limit to stay at the known-good 245000, but got ${comboModelAfter.limit.context} ` +
`(Math.min(member) fallback — the exact bug described in #13000)`
);
});
test("issue #13000 (control): no warm snapshot exists — Math.min(member) fallback is still used (expected, documented behavior)", async () => {
const fetcher: OmniRouteModelsFetcher = async () => [MEMBER_DEEPSEEK, MEMBER_GLM];
const combosFetcher: OmniRouteCombosFetcher = async () => [
{
id: "orchestrator",
name: "orchestrator",
models: COMBO_MODELS,
// computed_context_length intentionally omitted.
},
];
const autoCombosFetcher: OmniRouteAutoCombosFetcher = async () => [];
const enrichmentFetcher: OmniRouteEnrichmentFetcher = async () => new Map();
const compressionMetaFetcher: OmniRouteCompressionMetaFetcher = async () => [];
const providersFetcher: OmniRouteProvidersFetcher = async () => [];
// No prior snapshot on disk.
const diskSnapshotReader: OmniRouteDiskSnapshotReader = async () => undefined;
const diskSnapshotWriter: OmniRouteDiskSnapshotWriter = async () => {};
const sharedCache: OmniRouteFetchCache = new Map();
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", modelCacheTtl: 60_000 },
{
readAuthJson: authStub(),
fetcher,
combosFetcher,
autoCombosFetcher,
enrichmentFetcher,
compressionMetaFetcher,
providersFetcher,
diskSnapshotReader,
diskSnapshotWriter,
cache: sharedCache,
}
);
const input = makeInput();
await hook(input);
const entryAfter = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
const comboModelAfter = entryAfter.models["orchestrator"];
assert.ok(comboModelAfter, "combo model published on cold first run");
// No snapshot to backfill from — Math.min(163840, 1_000_000) = 163840.
assert.equal(
comboModelAfter.limit.context,
163_840,
"pure cold start with no snapshot must keep using the Math.min(member) fallback"
);
});

View File

@@ -59,6 +59,26 @@ test("parseOmniRoutePluginOptions: invalid baseURL (not a URL) → throws", () =
assert.throws(() => parseOmniRoutePluginOptions({ baseURL: "not-a-url" }), /baseURL/i);
});
test("parseOmniRoutePluginOptions: baseURL without an http(s) scheme → throws", () => {
// `new URL()` reads "localhost:20128" as the scheme "localhost:" followed by
// a path, so the address parses and the models are published with an api url
// no client can call.
for (const baseURL of ["localhost:20128", "localhost:20128/v1", "ftp://or.example.com", "or.example.com"]) {
assert.throws(
() => parseOmniRoutePluginOptions({ baseURL }),
/baseURL must be an http\(s\) URL/,
`expected ${baseURL} to be rejected`
);
}
});
test("parseOmniRoutePluginOptions: http and https baseURLs are accepted, padding trimmed", () => {
for (const baseURL of ["http://localhost:20128", "https://or.example.com/v1"]) {
assert.equal(parseOmniRoutePluginOptions({ baseURL }).baseURL, baseURL);
assert.equal(parseOmniRoutePluginOptions({ baseURL: ` ${baseURL} ` }).baseURL, baseURL);
}
});
test("parseOmniRoutePluginOptions: unknown key → throws (strict mode catches typos)", () => {
assert.throws(
() =>

View File

@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 356 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 359 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
| Database | `src/lib/db/` | SQLite domain modules (169 migrations) |
| Database | `src/lib/db/` | SQLite domain modules (176 migrations) |
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
@@ -578,14 +578,31 @@ own dedicated branch, and you MUST confirm the base branch with the operator bef
# HARD LINKS (`cp -al`), never a symlink: ~5s for the whole tree and near-zero extra
# disk (the inodes are shared), and unlike a symlink it does not break the dev server.
cp -al "$(git -C <main_checkout> rev-parse --show-toplevel)/node_modules" node_modules
# `.husky/_` is gitignored, so a fresh worktree does NOT have it and
# `core.hooksPath=.husky/_` then points at a directory that does not exist —
# every pre-commit gate goes silently mute. Copy it too.
cp -a "$(git -C <main_checkout> rev-parse --show-toplevel)/.husky/_" .husky/_
```
`scripts/dev/new-worktree.sh <branch> [base]` does all of the above (canonical path,
hard-linked `node_modules`, `.husky/_`) and then **verifies** the hook is actually
executable, so prefer it over running the steps by hand.
**Never `ln -s` node_modules.** Turbopack rejects a symlink that resolves outside the
project root, so `npm run dev` dies with a FATAL panic (`Symlink [project]/node_modules
is invalid, it points out of the filesystem root`) while typecheck, lint and the test
runners all keep passing — the error names "filesystem root", not the worktree, so it
reads like a Next/build bug and costs real time to trace (incident 2026-07-31, #9043).
**A worktree without `.husky/_` runs NO pre-commit gate — and says nothing.** `git`
resolves `core.hooksPath` relative to the worktree top; when the directory is missing it
simply finds no hook and commits. Nothing is printed, the commit succeeds, and the
identity/lint/docs gates never ran. This is how 59 commits carrying a stale identity
override (name of a contributor + the maintainer's e-mail) got past
`scripts/check/check-git-identity.sh` between 2026-08-29 and 09-02 — they were all made in
`cp -al` worktrees. Verify with `ls .husky/_/pre-commit` inside a new worktree, or just use
`scripts/dev/new-worktree.sh`, which fails loudly when the hook is not executable.
3. **Work, commit, push, open the PR — all from inside the worktree.** Never `git checkout` a
different branch inside a worktree another session might share.
4. **Tear down only your own** worktree + branch when done, from the main checkout:

View File

@@ -92,20 +92,884 @@
## [3.8.51] — TBD
_Living section — cycle opened at the v3.8.50 freeze (parallel-cycle model). Bullets are aggregated from `changelog.d/` fragments at each `/generate-release` phase._
_Living section — reconciled 2026-09-07 from all cycle commits (`release/v3.8.50` tip `091589089c``release/v3.8.51` tip `d6f315018a`, 696 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
### 📊 Release by the numbers
| | |
| --- | ---: |
| 👥 People who contributed | **120** |
| 📝 Commits in the cycle | **696** |
| 🔀 Pull requests referenced | **672** |
| 📋 Changelog entries | **685** |
| 🙌 Contributors credited in entries | **118** |
| 🤖 Automated dependency commits | 18 |
**Entries by type**
| Type | Count |
| --- | ---: |
| 🐛 Fixes | 403 |
| ✨ Features | 118 |
| 🧹 Chore | 72 |
| 📚 Docs | 36 |
| 🧪 Tests | 29 |
| ⚡ Performance | 7 |
| ♻️ Refactor | 7 |
| 🏗️ Build | 6 |
| ⚙️ CI | 2 |
| ⏪ Reverts | 2 |
| 📦 Dependencies | 2 |
| 🔒 Security | 1 |
### 🏆 Top 25 contributors this cycle
_By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
| # | Contributor | Commits |
| ---: | --- | ---: |
| 🥇 | diegosouzapw | 240 |
| 🥈 | Paco Cartones (@pacocartones) | 58 |
| 🥉 | Bob.Hou (@HouMinXi) | 57 |
| 4 | Dizzle (@maxmad64bis) | 27 |
| 5 | Webman (@jonlwheat2-gif) | 22 |
| 6 | Ravi Tharuma (@RaviTharuma) | 21 |
| 7 | backryun | 17 |
| 8 | Markus Hartung (@hartmark) | 17 |
| 9 | Nguyen Thanh Dat (@ntdatt812) | 16 |
| 10 | Rafa Martins (@rafacpti23) | 10 |
| 11 | Syed Raheemuddin (@raheemuddin786) | 10 |
| 12 | Koosha Paridehpour (@KooshaPari) | 9 |
| 13 | Nguyễn Viết Tuấn (@TheDemonTuan) | 8 |
| 14 | MumuTW | 7 |
| 15 | Paijo (@oyi77) | 7 |
| 16 | Tobias Andersen (@turbolego) | 6 |
| 17 | Bl0ck (@Bl0ck154) | 5 |
| 18 | SHANMUGAPRIYAN (@geek007git) | 5 |
| 19 | KaspaPulse | 5 |
| 20 | Abhishek Sharma (@abhisheksharma2411) | 4 |
| 21 | Abhishek Divekar (@adivekar-utexas) | 4 |
| 22 | Andrew B. (@AndrianBalanescu) | 4 |
| 23 | NoxzRCW | 4 |
| 24 | opensource-elearning | 4 |
| 25 | Alvin T. Veroy (@alvinveroy) | 3 |
### ✨ New Features
- **feat(audio):** proxy native ElevenLabs voices, text-to-speech, and speech-to-text HTTP routes through stored OmniRoute credentials, preserving query strings, multipart uploads, binary responses, and upstream errors (#10556). ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- Added Google AI Studio Gemini batch text-to-speech support through `POST /v1/audio/speech`. ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- Run synchronous RTK and Caveman request compression in a bounded worker-thread pool, keeping large `/v1/responses` compression heaps outside the HTTP isolate while preserving strict fail-open behavior and per-engine telemetry. ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- **feat(routing):** subscription-first auto groupings — `auto/subscription` routes only through plan-included connections with a documented hard-stop overage and fails closed on exhaustion, while `auto/thrifty` orders the pool `subscription → keyless → free → cheap → premium` and steps up one rung at a time as each is exhausted. Billing class comes from a curated per-connection catalog (uncurated is treated as metered, never plan-included), both reuse STRICT_ZERO_COST's per-connection verification, and a quota reading whose `resetAt` has passed is now refreshed regardless of TTL so routing returns to plan capacity as soon as it resets ([#11146](https://github.com/diegosouzapw/OmniRoute/pull/11146)) — thanks @yourspraveen
- **feat(providers):** publish a management-authenticated versioned web-session credential contract from OmniRoute's canonical browser credential metadata ([#11340](https://github.com/diegosouzapw/OmniRoute/pull/11340)) — thanks @Zartharas
- **feat(video bridge):** harden the optional drill-down cache substrate with exact-path broker policy, canonical principal/session/media isolation, independent retained-byte quotas, cancellation-safe commits, rejection of excess or non-canonical Base64 padding and non-JPEG/truncated media, warning-sensitive full JPEG canonicalization that strips trailing polyglot bytes, server-derived dimensions, and auditable derivation metadata; production tenant binding and multi-resolution selection remain follow-up work ([#11369](https://github.com/diegosouzapw/OmniRoute/pull/11369)) ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- **feat(search):** Add Xquik X search with typed results, credential validation, REST routing, and MCP selection ([#11370](https://github.com/diegosouzapw/OmniRoute/pull/11370)) — thanks @kriptoburak
- **feat(video):** add an opt-in focused analysis mode that safely uses a normalized, 500-code-point latest-user hint for task-aware frame captions while preserving full-mode prompts, temporal-window isolation, and cache identity without storing raw task text ([#11383](https://github.com/diegosouzapw/OmniRoute/pull/11383)). ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- **feat(dashboard):** surface durable exclusive managed leases in the existing Sessions view, keeping leased clients visible across idle gaps while marking connections with in-flight work as active ([#11389](https://github.com/diegosouzapw/OmniRoute/pull/11389)) — thanks @KaspaPulse
- **build(bun):** allow Turbopack bundler flag on Bun 1.4+ with configurable Webpack fallback ([#11471](https://github.com/diegosouzapw/OmniRoute/pull/11471)) — thanks @TheDemonTuan
- feat(api): add an opt-in `modelVisibilityAllowlist`/`modelVisibilityDenylist` settings pair to curate exactly which models `/v1/models` advertises, mirrored into every `auto/*` combo candidate pool so a denied model cannot be routed to via combo selection either (#11481) ([#11997](https://github.com/diegosouzapw/OmniRoute/pull/11997))
- **feat(rankings):** the Free Provider Rankings page shows what each provider actually served over the last 24 h. It ranked by ELO alone, which left a provider that answers every call with an error in first place; the usage data was already served by the API but never requested. A provider with too small a sample shows a dash, not a number ([#11546](https://github.com/diegosouzapw/OmniRoute/pull/11546)) ([#11553](https://github.com/diegosouzapw/OmniRoute/pull/11553)) — thanks @maxmad64bis
- **feat(guardrails):** enforce a bounded, deterministic contract for Video Bridge transcripts — 256 cues, 4096 input code units and 4 KiB UTF-8 per cue, 64 KiB total text, malformed-Unicode rejection, focus-window scoping, cross-source reconciliation with contributing-source metadata, and a structural provenance trust boundary so caller JSON can never self-assert `embedded`/`audio-bridge` provenance ([#11652](https://github.com/diegosouzapw/OmniRoute/issues/11652)) ([#12009](https://github.com/diegosouzapw/OmniRoute/pull/12009))
- **feat(video):** orchestrate optional Video Bridge audio extraction and Audio Bridge STT behind a dual opt-in (operator setting AND per-request signal) — a new loopback-only broker `mode=audio` operation shares the frame path's exact process queue, deadline, AbortSignal, and byte budgets to extract a bounded mono 16 kHz PCM WAV from the same already-downloaded video, then reuses the existing Audio Bridge transcription boundary; provider segment timing is preserved when available and marked coarse otherwise, and every failure degrades to a visual-only-safe partial instead of throwing (#11654). ([#12012](https://github.com/diegosouzapw/OmniRoute/pull/12012))
- Add a tenant-bound Video Bridge drill-down lifecycle on top of the existing secure cache substrate: opaque hashed handles (never raw session/video identifiers), preview/standard/detail multiresolution variants resampled on read, response pagination capped at 8 frames and 32 MiB, and a new authenticated `/api/v1/video-bridge/drilldown` consumer route that stays disabled for remote access by default and denies cross-key access with the same response as a nonexistent handle (no existence oracle). ([#12006](https://github.com/diegosouzapw/OmniRoute/pull/12006))
- **test(video):** Add the Video Bridge FU-07/FU-09 promotion-evidence harness (#11656) — a frozen Zod manifest schema covering the 8 required scenario kinds (static scenes, rapid cuts, late facts, fades, blur, small text, close events, visual prompt injection) with a minimum of 3 repetitions per case, deterministic declarative fixture recipes (`videoBridgePromotionFixtures.ts`), a pure medians/p95 metrics aggregator, a pure FU-07/FU-09 promotion-verdict evaluator applying the ticket's exact thresholds (missing token usage always holds), a digest-only persistence layer that never retains raw media or raw model responses, and a versioned per-model promotion allowlist shipped empty with every model defaulting to `hold`. The FU-07/FU-09 promotion verdicts themselves remain HOLD — they require a real evidence run against real models on VPS 192.168.0.15. ([#12008](https://github.com/diegosouzapw/OmniRoute/pull/12008))
- **feat(video bridge):** "embedded" transcript provenance can now be legitimately earned instead of merely asserted — a bounded, allowlisted (`mov_text`/`subrip`/`webvtt`) subtitle probe runs through the loopback-only Video Bridge broker (at most 2 streams, 10s subdeadline bounded by the request deadline, 256 KiB output, 4096-code-unit lines), normalized through a bounded, ReDoS-safe WebVTT parser and Zod-validated end to end. The adapter always resolves to an explicit `success`/`absent`/`transient_failure` outcome — a subtitle failure never breaks the visual description path, and only a fingerprint-verified broker response (never a caller-declared label) can produce embedded cues (#11659). ([#12011](https://github.com/diegosouzapw/OmniRoute/pull/12011))
- Default new Antigravity-family connections (agy CLI imports and Antigravity OAuth connects) to model auto-sync, so live model discovery lands in the synced catalog and `/v1/models` picks up freshly released upstream models (e.g. Gemini 3.7 Flash tiers) without code changes. Existing connections keep their current setting; the per-connection dashboard toggle remains the opt-out. (#11685 — thanks @MumuTW) ([#11887](https://github.com/diegosouzapw/OmniRoute/pull/11887))
- **feat(ui):** enable React Compiler (`reactCompiler: true` + `babel-plugin-react-compiler`) for automatic memoization at build time ([#11783](https://github.com/diegosouzapw/OmniRoute/pull/11783)) — thanks @jonlwheat2-gif
- **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)) ([#12557](https://github.com/diegosouzapw/OmniRoute/pull/12557)) — thanks @HouMinXi
- **feat(zai):** add GLM-5.3-Flash Coding Plan support (1M context, 128K output, vision, `low|high|max` reasoning) and route `zai` GLM-5.3-family API-key traffic through the OpenAI-compatible Coding Plan endpoint with native thinking defaults ([#11801](https://github.com/diegosouzapw/OmniRoute/pull/11801)) — thanks @Neuron-Mr-White
- **feat(combo):** choose how combo models are ordered — manual, provider, score, or name — via a sort control in the dashboard builder, persisted in `config.modelSort` and re-applied on load and after add ([#11812](https://github.com/diegosouzapw/OmniRoute/pull/11812)) — thanks @maxmad64bis
- **feat(free):** custom models can be marked free-tier via `customModels[].isFree`; `isFreeModel()` is the first door and `hidePaidModels` respects it even for providers outside the free budget ([#11843](https://github.com/diegosouzapw/OmniRoute/pull/11843)) — thanks @maxmad64bis
- **feat(nodejs):** add `5dive` as a `configure` target — `omniroute configure 5dive` / `omniroute setup-5dive` write a 5dive auth profile that points an agent fleet's `claude` seats at OmniRoute, with the root-only write, the loopback-vs-`https` endpoint rule and the per-seat model pin handled explicitly ([#11852](https://github.com/diegosouzapw/OmniRoute/pull/11852)) — thanks @5dive-bot
- **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):** the provider plugin manifest now advertises a `usage-fetch` capability for the 40 providers that have a wired usage/quota fetcher, so external dashboards can read it from `GET /api/v1/provider-plugin-manifest` instead of parsing `open-sse/services/usage.ts` after every release. Discovery only — no new fetcher, no quota change, and the Dashboard quota widget stays gated by `USAGE_SUPPORTED_PROVIDERS`. `USAGE_FETCHER_PROVIDERS` moved to a zero-dependency leaf (`open-sse/services/usage/fetcherProviders.ts`) and is re-exported from `services/usage.ts`, keeping the manifest module a light leaf instead of pulling the ~490-module usage dispatcher into the manifest route. ([#11903](https://github.com/diegosouzapw/OmniRoute/pull/11903)) — thanks @maxmad64bis / @pacocartones
- **feat(plugins):** `OMNIROUTE_PLUGINS_DIR` sets the directory the runtime plugin scanner reads — and the root the plugin manager installs into — overriding the `HOME`-derived default, so a Docker/K8s deployment can point straight at its bind-mounted plugin tree instead of moving `HOME` just to relocate the scan path. An image that exports no home no longer scans `/tmp/.omniroute/plugins` in silence: the resolved directory is logged once at startup as `scanner.dir_resolved`, naming the input that won. Unset, behaviour is unchanged. Distinct from the CLI-only `OMNIROUTE_PLUGIN_PATH`, which finds `omniroute-cmd-*` command packages and never reached this scanner ([#11906](https://github.com/diegosouzapw/OmniRoute/pull/11906)) — thanks @amaleta / @pacocartones
- **feat(leases):** add an explicit owner-authenticated status action that returns only the active lease's privacy-safe configured connection and provider labels, with generation fencing and no credential or internal-id disclosure ([#11910](https://github.com/diegosouzapw/OmniRoute/pull/11910)) — thanks @KaspaPulse
- **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)) ([#12504](https://github.com/diegosouzapw/OmniRoute/pull/12504)) — thanks @HouMinXi
- **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)) ([#12495](https://github.com/diegosouzapw/OmniRoute/pull/12495)) — thanks @HouMinXi
- **feat(routing):** add a `score` Auto router strategy that selects the highest configured weighted score and reuses `explorationRate`. ([#12155](https://github.com/diegosouzapw/OmniRoute/pull/12155)) — thanks @jacobsparts
- Add a runtime feature flag to disable universal context handoffs globally without changing the default behavior. ([#12167](https://github.com/diegosouzapw/OmniRoute/pull/12167)) — thanks @jacobsparts
- **feat(providers):** the provider plugin manifest now also advertises a `usage-supported` capability for the 46 providers whose usage API is accepted by the server and Dashboard routes, so integrators can distinguish "the server will serve quota for this provider" from "a fetcher is wired" without reading TypeScript. Discovery only — no fetcher or quota change. `usage-fetch` resolves on id or alias (the usage dispatcher accepts both); `usage-supported` resolves on id alone, matching the runtime guard `USAGE_SUPPORTED_PROVIDERS.includes(providerId)`. `USAGE_SUPPORTED_PROVIDERS` moved to a zero-dependency leaf (`open-sse/services/usage/supportedProviders.ts`) and is re-exported from `providers.ts`, mirroring the `fetcherProviders` leaf from #11903 and keeping the manifest a light module. ([#12214](https://github.com/diegosouzapw/OmniRoute/pull/12214)) — thanks @maxmad64bis
- **feat(rankings):** order Free Provider Rankings by what each provider actually served — `GET /api/free-provider-rankings?sortBy=reliability` and a "Most reliable first" toggle on the page. Providers with too few calls to state a success rate keep their score order below the measured ones; the default order is unchanged ([#12218](https://github.com/diegosouzapw/OmniRoute/pull/12218)). — thanks @maxmad64bis
- **perf(sse):** defer `cloneLogPayload()` in the structured SSE collector until after the `maxEvents`/`maxBytes` cap check, eliminating ~9,800 wasted `structuredClone` calls per streaming response (6571% faster `push()`). Reducer snapshot isolation restored for OpenAI and Responses summaries ([#12241](https://github.com/diegosouzapw/OmniRoute/pull/12241)) — thanks @PauloHSOliveira ([#12243](https://github.com/diegosouzapw/OmniRoute/pull/12243))
- **feat(usage):** Devin CLI agentic quota (Codeium seat-management GetUserStatus) and OpenRouter key limits plus account credits now surface in Provider Limits ([#12256](https://github.com/diegosouzapw/OmniRoute/pull/12256) — thanks @Neuron-Mr-White)
- **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)) ([#12487](https://github.com/diegosouzapw/OmniRoute/pull/12487)) — thanks @HouMinXi
- **feat(auto-combo):** Auto-Combo scoring can now weigh how often a provider/model has actually succeeded. The engine already carried that number on every candidate — 24 hours of usage history behind a ten-sample floor, real-time metrics otherwise — and the scoring function never read it, while the weight table described `stability` as if it did. `reliability` (`1 - failureRate`, with the same field precedence and the same rate-bounding the speed ranking already uses, so a corrupt reading means "nothing observed" rather than "fails every call") is now a declared factor shipping at weight `0`, so routing is unchanged until an operator gives it one, and the `stability` description now matches what that factor computes ([#12317](https://github.com/diegosouzapw/OmniRoute/pull/12317)) — thanks @maxmad64bis
- **feat(routing):** With `freeAccessPolicy: "strict"`, the read-only candidate listing (`GET /v1/auto-combo/{channel}/candidates`) no longer hides the candidates the zero-cost guard excludes — the same read-only transparency the resilience filter already honours (#9133). Each candidate now carries `freeAccessExclusion` saying why it would be kept out, and it tells an exhausted allowance apart from a quota reading that never arrived or went stale, which used to look identical from the outside. Routing is unchanged: the listing reports, it never enforces. The separate `excludeTosAvoid` guard still drops its candidates without a reason; that gap is now documented rather than closed ([#12319](https://github.com/diegosouzapw/OmniRoute/pull/12319)) — thanks @maxmad64bis
- **feat(radar):** The Radar catalog table now shows two facts it was already receiving from the feed and dropping on the floor: the per-model rate limits (requests and tokens, per minute and per day) in a new column, and a badge when a provider's terms state it may train on the prompts you send. A limit of zero renders as zero rather than "rate-only" — for a ceiling those are opposite facts — and a model with no training statement gets no badge, because an absent statement is not a guarantee ([#12320](https://github.com/diegosouzapw/OmniRoute/pull/12320)) — thanks @maxmad64bis
- **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) ([#12377](https://github.com/diegosouzapw/OmniRoute/pull/12377)) — thanks @pacocartones
- **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 ([#12385](https://github.com/diegosouzapw/OmniRoute/pull/12385))
- **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)) ([#12390](https://github.com/diegosouzapw/OmniRoute/pull/12390)) — thanks @pacocartones
- **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)) ([#12631](https://github.com/diegosouzapw/OmniRoute/pull/12631)) — thanks @RaviTharuma
- **feat(opencode-plugin): map gateway cost/usage/tok/s onto OpenCode inference payloads** — the official plugin copies `X-OmniRoute-Response-Cost`, token counts, `X-OmniRoute-Tokens-Per-Second` / `usage.tokens_per_second`, TTFT, and the winning `X-OmniRoute-Model` onto the JSON/SSE body OpenCode already consumes. Missing tok/s is left unset (never `tokens / latency`). (#12636) — thanks @RaviTharuma
- **feat(combo):** add combo strategy `quota-weighted`: skip exhausted accounts, then weighted-draw by leftover / in-flight load; existing conversations stay pinned until the account is empty ([#12789](https://github.com/diegosouzapw/OmniRoute/pull/12789)) — thanks @HouMinXi
- **feat(models):** Account live listings become the chat catalog source for Claude, Codex, Copilot, and AGY; public metadata only fills prices on IDs those accounts already list. ([#12866](https://github.com/diegosouzapw/OmniRoute/pull/12866)) — thanks @HouMinXi
- **fix(models):** Union `agy` and `antigravity` live catalogs so `agy/gemini-3.8-flash-high` is not rejected after the prefix folds to `antigravity`. ([#12866](https://github.com/diegosouzapw/OmniRoute/pull/12866)) — thanks @HouMinXi
- **feat(opencode):** Add `@omniroute/opencode-plugin-v2`, an opencode v2 plugin that gives a v2 install a route to an OmniRoute gateway: it publishes models, combos and auto-combos into the host catalog, refreshes them lazily behind a 300s TTL, and keeps serving the last known catalog from an on-disk snapshot when the gateway is unreachable. The v1 plugin (`@omniroute/opencode-plugin`) is untouched and keeps working as before. The plugin adapts to the host instead of assuming one shape: it reads the shape the host seeds into the catalog draft and publishes the `api` block, the pre-`api` top-level fields, or both. The gateway key comes from the host credential store when one is connected, so no secret needs to sit in a config file; a source the gateway refuses is named in a warning instead of silently degrading the catalog to raw model ids; and tool schemas bound for a Gemini model are stripped of the JSON-Schema keywords Gemini rejects, so tool calling survives that route ([#12870](https://github.com/diegosouzapw/OmniRoute/pull/12870)) — thanks @maxmad64bis
- feat(services): show sanitized CLIProxyAPI account health from its authenticated management API without exposing credentials, file paths, or raw account metadata (#6342) ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- **feat(search):** Add AnySearch free web search + URL extract (webFetch) with typed results, credential validation, REST routing, and MCP selection - fallback-only ([#11690](https://github.com/diegosouzapw/OmniRoute/pull/11690)) — thanks @Xxx91n
- **feat(dashboard):** display clamped `[0, 100]%` cached input token ratio in request logs table ([#11970](https://github.com/diegosouzapw/OmniRoute/pull/11970)) ([#11970](https://github.com/diegosouzapw/OmniRoute/pull/11970)) — thanks @b3nw
- **feat(catalog):** add `OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS` feature flag to optionally filter out thinking level variants from model catalog ([#11971](https://github.com/diegosouzapw/OmniRoute/pull/11971)) ([#11971](https://github.com/diegosouzapw/OmniRoute/pull/11971)) — thanks @b3nw
- **feat(dashboard):** continuously export call logs to external analytics stores. A pluggable destination registry ships the full Logs-tab record set on an hourly `JobRegistry` cron, with a persisted per-destination cursor, batched inserts, a config UI rendered from each destination's own field descriptors, and a REST layer (`/api/log-export/*`) for CRUD, a connection test, and an on-demand run. A destination can opt into `includeBodies` to also ship the request and response payloads shown in the Logs detail pane, including the client and provider views of each call; this is off by default, and payloads inherit the dashboard's PII sanitisation, secret redaction and `noLog` handling. Google BigQuery is the first destination, using a service-account key stored encrypted at rest and streaming inserts keyed by call-log id, into a table that is day-partitioned on `timestamp` and clustered on `api_key_name`, `provider`, `model` and `status`. ([#11945](https://github.com/diegosouzapw/OmniRoute/pull/11945)) — thanks @dpozimski
- **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` ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **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) ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **feat(providers):** MaxAI image generation — 6 image models (gpt-image-1, dall-e-3, flux-1-schnell/dev/pro, sd3-medium) exposed through `POST /v1/images/generations` ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **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` ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **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 ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **feat(providers):** per-provider TLS impersonation profile (MaxAI presents a Windows Firefox-150 client fingerprint) so its bot-sensitive endpoints accept OmniRoute traffic ([#11461](https://github.com/diegosouzapw/OmniRoute/pull/11461)) — thanks @arminanton
- **feat(providers):** add **Nimble** as a web-search and web-fetch provider (`nimble-search`) — `/v1/search` routes to Nimble's search API at `lite` depth (locale, freshness and include/exclude domain filters mapped onto the shared request contract), and `/v1/web/fetch` routes to Nimble Extract, which covers all four fetch formats (`markdown`, `html`, `links`, `screenshot`) from a single call. One API key serves both surfaces. ([#11620](https://github.com/diegosouzapw/OmniRoute/pull/11620)) — thanks @wildcard
- The npm publish is automatic again, through npm Trusted Publishing (OIDC): the hosted `stage-npm` job publishes with a short-lived credential minted from GitHub's id-token — no `NPM_TOKEN`, no 2FA prompt, provenance attached. `publish_mode=staged` (owner approves with 2FA) and `direct` (token) remain available on `workflow_dispatch`. ([#11931](https://github.com/diegosouzapw/OmniRoute/pull/11931))
- **feat(providers):** Add **Opper** as an API-key gateway provider — EU-hosted AI gateway with 700+ models from 30+ providers behind one OpenAI-compatible API and one key (`OPPER_API_KEY`); model ids use `provider/model` format (e.g. `anthropic/claude-sonnet-4-6`, `openai/gpt-5`); live model catalog at `https://api.opper.ai/v3/compat/models`; entry mirrors `requesty` (same shape, `passthroughModels: true`, no static seed) ([#11629](https://github.com/diegosouzapw/OmniRoute/pull/11629))
- **feat(dashboard):** the `/dashboard/orchestration` snapshot hook now subscribes to the `agents` WebSocket channel (`agent.task.updated`) instead of `requests` as its refetch trigger, and relaxes its background poll from 5s to 30s while that WS connection is up — falling back to the tighter 5s cadence, reprogrammed live on any connect/disconnect transition, whenever the socket is down. ([#12409](https://github.com/diegosouzapw/OmniRoute/pull/12409))
- **feat(dashboard):** Orchestration canvas quick wins — search box plus state/source/provider filter chips with a one-click clear, and per-source collapse/expand, all reflected in the URL so a filtered/collapsed view is shareable and survives a refresh; the detail drawer gained a "copy trace JSON" action and hardened error/empty-state and accessibility handling; the Agents-tab edges now animate traveling particles along active (running) connections; and the canvas node/edge status colors moved off fixed hex values onto theme-aware `--orch-status-*` CSS custom properties, so they adapt correctly to light/dark mode. ([#12393](https://github.com/diegosouzapw/OmniRoute/pull/12393))
- **chore(dashboard):** Orchestration UI hardening pass and the missing component/model test coverage it called for — `OrchestratorNode`/`ActivityNode`/`OverflowNode` rendering, the `?node=`/overflow-click page routing, the Agents-tab orchestrator-click no-op and `showCompleted` toggle, and the overview kanban's done-column sort order (#12270, #12271). ([#12393](https://github.com/diegosouzapw/OmniRoute/pull/12393))
- New `/dashboard/orchestration` page: live unified view of everything running — Cloud Agent, A2A and Conductor as a real-time graph (Agents tab), the combo cascade (Routing tab, reusing the Combo Live Studio) and a state kanban (Overview tab), with a detail drawer (trace, cost, approve/cancel). Read-only over existing APIs — no new backend. Canvas concept credit: PR #11815 design ([#12261](https://github.com/diegosouzapw/OmniRoute/pull/12261))
- **feat(dashboard):** new "History" tab on `/dashboard/orchestration` — an Airflow-style grid of finished runs over a 24h/7d/30d preset window, one row per (source, identity), clicking a cell opens the existing detail drawer. It is backed by real persistence: A2A task lifecycle transitions are now written to the `a2a_tasks` table (purged after 30 days, configurable via `OMNIROUTE_A2A_HISTORY_RETENTION_DAYS`) and served by the new `GET /api/a2a/tasks/history` listing endpoint, with the task-detail route falling back to persisted history once a run leaves the in-memory snapshot. Conductor runs stay remote and are not persisted locally — the tab says so instead of silently omitting them. ([#12479](https://github.com/diegosouzapw/OmniRoute/pull/12479))
- **feat(dashboard):** the orchestration detail drawer gained a "Repeat" action for Cloud Agent, A2A and Conductor tasks — a two-click confirm (click once to arm, click again within the confirm window to fire) re-submits the original prompt/input as a new run. The button is disabled with an explanatory tooltip whenever the original input can't be recovered from the loaded task detail (e.g. it never carried a prompt, or the detail failed to load). Two limitations of the A2A variant, by design: it targets the `/a2a` JSON-RPC endpoint, which authenticates with an API key only (`REQUIRE_API_KEY=true` or a configured `OMNIROUTE_API_KEY` makes a dashboard-session repeat answer `HTTP 400` — surfaced verbatim in the drawer's error line, never as a success), and the `message/send` call is SYNCHRONOUS: the POST blocks for the whole skill run, so the success confirmation only appears once the repeated task finishes. A dashboard-authenticated A2A creation path is deliberately left to a follow-up — widening the endpoint's auth posture is an operator decision, not a side effect of this feature. ([#12508](https://github.com/diegosouzapw/OmniRoute/pull/12508))
- **feat(a2a):** A2A task execution now records which memories were consulted for the task's last user message as `metadata.memoryHits` (id/key/type/content-snippet) plus a `memory_hits` history event, purely for observability — the retrieved memory is never injected into a skill's prompt or behavior. Gated by the `OMNIROUTE_A2A_MEMORY_HITS` kill-switch (default enabled; set to `0` to skip the recall lookup entirely). The drawer's new "Memory used" section lists these hits for a2a tasks and is omitted whenever there are none. Known limitation: recall only resolves under the keyless posture — a keyed caller's task owner is a SHA-256 prefix of the API key, while memory rows are keyed by the database api-key id, and no hash→id lookup exists today, so the hit list stays empty for keyed callers. The recorded hits are also kept out of the task's own `input` (and therefore out of the persisted input and of the "Repeat" request body), so repeating a task never re-sends the previous run's memory snippets. ([#12508](https://github.com/diegosouzapw/OmniRoute/pull/12508))
- **feat(providers):** add a Perplexity Agent API provider (`perplexity-agent` / `pplx-agent`) for Perplexity `/v1/responses`, including the documented Anthropic, OpenAI, Google, xAI, DeepSeek, Z.AI, Moonshot/Kimi, NVIDIA, and Perplexity model IDs plus Anthropic-model `max_output_tokens` compatibility. ([#12103](https://github.com/diegosouzapw/OmniRoute/pull/12103)) — thanks @AIB1TAL0S
- **feat(providers):** add RPD (Requests Per Day) limit to provider rate limit overrides across UI, schemas, DB, and i18n ([#12147](https://github.com/diegosouzapw/OmniRoute/pull/12147)) ([#12147](https://github.com/diegosouzapw/OmniRoute/pull/12147)) — thanks @b3nw
- **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. ([#11513](https://github.com/diegosouzapw/OmniRoute/pull/11513)) — thanks @arminanton
- **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. ([#11513](https://github.com/diegosouzapw/OmniRoute/pull/11513)) — thanks @arminanton
- **feat(logs):** add copy-all button to request detail modal ([#11083](https://github.com/diegosouzapw/OmniRoute/pull/11083)) — thanks @AndrianBalanescu
- **feat(check):** make serviceKinds required and add the reverse-walk provider consistency gate ([#11392](https://github.com/diegosouzapw/OmniRoute/pull/11392)) — thanks @Tushar49
- **feat(providers):** only advertise verified-working connections by default ([#11448](https://github.com/diegosouzapw/OmniRoute/pull/11448)) — thanks @alltomatos
- **feat(resilience):** add hierarchical concurrency admission ([#11493](https://github.com/diegosouzapw/OmniRoute/pull/11493))
- **feat(providers):** derive + surface expiry for JWT-bearing web cookies (#11497) ([#11505](https://github.com/diegosouzapw/OmniRoute/pull/11505)) — thanks @oyi77
- **feat(tunnels):** support named cloudflared tunnels via CLOUDFLARED_CONFIG ([#11561](https://github.com/diegosouzapw/OmniRoute/pull/11561)) — thanks @ggiak
- **feat(compression):** make proactive context-compression threshold a live setting ([#11564](https://github.com/diegosouzapw/OmniRoute/pull/11564)) — thanks @ragnar-claude
- **feat(resilience):** add provider peak-hour protection ([#11622](https://github.com/diegosouzapw/OmniRoute/pull/11622)) — thanks @Neuron-Mr-White
- **feat(providers):** add 1min.ai provider ([#11631](https://github.com/diegosouzapw/OmniRoute/pull/11631)) — thanks @zero-executioner
- **feat(dashboard):** collapsible JSON tree viewer for request/response payloads ([#11703](https://github.com/diegosouzapw/OmniRoute/pull/11703)) — thanks @hartmark
- **feat(routing):** add LiquidAI LFM2.5-2.6B free tier via OpenRouter ([#11752](https://github.com/diegosouzapw/OmniRoute/pull/11752)) — thanks @brick30llc-ctrl
- **feat(pricing):** add GLM-5.3-Flash pricing, model specs, and catalog registration ([#11830](https://github.com/diegosouzapw/OmniRoute/pull/11830)) — thanks @AndrianBalanescu
- **feat(routing):** add relayMode for schema-locked context handoffs ([#11839](https://github.com/diegosouzapw/OmniRoute/pull/11839)) — thanks @raheemuddin786
- **feat(config):** add support for runtime system prompt configuration and hot-reloading ([#11841](https://github.com/diegosouzapw/OmniRoute/pull/11841)) — thanks @raheemuddin786
- **feat(routing):** expand connection-aware quota prefilter across combo strategies (#11682) ([#11850](https://github.com/diegosouzapw/OmniRoute/pull/11850)) — thanks @HouMinXi
- **feat(models):** reactive discovery sync after Antigravity model-not-found 404 ([#11888](https://github.com/diegosouzapw/OmniRoute/pull/11888)) — thanks @MumuTW
- **feat(models):** auto-aliases and token limits from synced Antigravity discovery ([#11889](https://github.com/diegosouzapw/OmniRoute/pull/11889)) — thanks @MumuTW
- **feat(settings):** raise sticky round-robin limit caps to 1000 ([#12015](https://github.com/diegosouzapw/OmniRoute/pull/12015)) — thanks @adivekar-utexas
- **feat(resilience):** operator-configurable global credential health check interval ([#12043](https://github.com/diegosouzapw/OmniRoute/pull/12043)) — thanks @adivekar-utexas
- **feat(quota):** use official OpenCode Go usage API ([#12124](https://github.com/diegosouzapw/OmniRoute/pull/12124)) — thanks @ddarkr
- **feat(resilience):** default the credential health check sweep to 60 minutes ([#12138](https://github.com/diegosouzapw/OmniRoute/pull/12138)) — thanks @adivekar-utexas
- **feat(quality):** new-code mode for the complexity and dead-code ratchets (Clean as You Code) ([#12142](https://github.com/diegosouzapw/OmniRoute/pull/12142))
- **feat(dashboard):** orchestration canvas — unified model + snapshot hook (part 1/2) ([#12156](https://github.com/diegosouzapw/OmniRoute/pull/12156))
- **feat(usage):** add Kilo Code balance and Kilo Pass quotas ([#12178](https://github.com/diegosouzapw/OmniRoute/pull/12178)) — thanks @killer30001000
- **feat(providers):** refresh vendored ChatGPT Web connector to v4.0.7 ([#12181](https://github.com/diegosouzapw/OmniRoute/pull/12181)) — thanks @backryun
- **feat(providers):** manual "Clear cooldown" action in the cooling panel ([#12224](https://github.com/diegosouzapw/OmniRoute/pull/12224)) — thanks @patrykkopycinski
- **feat(providers):** restore ChatGPT Web via clean-room browser transport ([#12239](https://github.com/diegosouzapw/OmniRoute/pull/12239)) — thanks @backryun
- **feat(sse):** wire the PROVIDER_PROFILES window gate into the global provider cooldown ([#12247](https://github.com/diegosouzapw/OmniRoute/pull/12247))
- **feat(providers):** modernize CLOVA Studio chat and embeddings ([#12277](https://github.com/diegosouzapw/OmniRoute/pull/12277)) — thanks @backryun
- **feat(browser):** adopt Obscura as primary headless browser engine with Chromium fallback ([#12286](https://github.com/diegosouzapw/OmniRoute/pull/12286)) — thanks @opensource-elearning
- **feat(sse):** full language parity for output styles (es/de/fr/it/ru/zh + autoDetect) ([#12289](https://github.com/diegosouzapw/OmniRoute/pull/12289))
- **feat(quality):** complete test:scoped — --full map rebuild, stdin selection, CI loader parity (#8084 D1) ([#12353](https://github.com/diegosouzapw/OmniRoute/pull/12353))
- **feat(providers):** refresh Fable, Cursor, and Devin catalogs ([#12367](https://github.com/diegosouzapw/OmniRoute/pull/12367)) — thanks @backryun
- **feat(video):** redact transcript text from logs and durable memory (#12150 P1) ([#12427](https://github.com/diegosouzapw/OmniRoute/pull/12427))
- **feat(dashboard):** parent-link, genuine-continuation badge, and modal perf fixes ([#12448](https://github.com/diegosouzapw/OmniRoute/pull/12448)) — thanks @hartmark
- **feat(dashboard):** adaptive context-budget dial on compression panel ([#12488](https://github.com/diegosouzapw/OmniRoute/pull/12488)) — thanks @HouMinXi
- **feat(providers):** refresh Z.ai Web models and browser transport ([#12524](https://github.com/diegosouzapw/OmniRoute/pull/12524)) — thanks @backryun
- **feat(video):** redact raw client-snapshot transcript fields in the detailed log (#12150 P2) ([#12528](https://github.com/diegosouzapw/OmniRoute/pull/12528))
- **feat(providers):** refresh NVIDIA hosted models ([#12538](https://github.com/diegosouzapw/OmniRoute/pull/12538)) — thanks @backryun
- **feat(quota):** Moonshot Open Platform balance and TPD lock for custom nodes ([#12590](https://github.com/diegosouzapw/OmniRoute/pull/12590)) — thanks @HouMinXi
- **feat(video):** redact transcript in the in-memory pending-request snapshot (#12430 item 6) ([#12596](https://github.com/diegosouzapw/OmniRoute/pull/12596))
- **feat(dashboard):** link a log entry's Conversation Context to its owning conversation ([#12646](https://github.com/diegosouzapw/OmniRoute/pull/12646)) — thanks @hartmark
- **feat(catalog):** eligibility-gated free-tier bucket ([#12669](https://github.com/diegosouzapw/OmniRoute/pull/12669))
- **feat(db):** fail-closed previous_response_id continuation for redacted video turns (#12150 P2b) ([#12707](https://github.com/diegosouzapw/OmniRoute/pull/12707))
### 🐛 Bug Fixes
- **security(streaming):** sanitize generic mid-stream error messages before emitting OpenAI,
Responses, or Claude SSE failure frames and before diagnostic logging, while preserving raw
failures for internal classification and keeping client disconnects out of provider failure state.
failures for internal classification and keeping client disconnects out of provider failure state. ([#12457](https://github.com/diegosouzapw/OmniRoute/pull/12457))
- **fix(claude):** `blockExtraUsage: false` no longer lets 5h quota preflight skip the account; extra usage is billed after the session bar is gone, so the request must reach Anthropic ([#12803](https://github.com/diegosouzapw/OmniRoute/pull/12803)) — thanks @HouMinXi
- Sanitize HuggingChat conversation-creation and message-send transport failures before they reach client error bodies or provider logs. ([#12467](https://github.com/diegosouzapw/OmniRoute/pull/12467))
- **fix(dashboard):** Keep local and theme-aware provider SVG icons at a definite layout size so Chromium does not collapse them to 0×0 after the v3.8.50 image-rendering change ([#12054](https://github.com/diegosouzapw/OmniRoute/pull/12054)) — thanks @ponkcore
- **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. ([#12506](https://github.com/diegosouzapw/OmniRoute/pull/12506))
- **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)) — thanks @gonisulaimann
- Stop advertising Gemini Live-only models as supported audio endpoints until OmniRoute proxies the bidirectional Live protocol. ([#11710](https://github.com/diegosouzapw/OmniRoute/pull/11710)) — thanks @RaviTharuma
- **fix(kie):** reroute `flux/kontext` off the KIE Market `createTask` flow — it is catalogued with `isMarket: true` but has no Market catalog page, so KIE rejected it with "model name not supported"; it now hits the dedicated `POST /api/v1/flux/kontext/generate` / `GET /api/v1/flux/kontext/record-info` endpoints instead (#11296). ([#11985](https://github.com/diegosouzapw/OmniRoute/pull/11985)) — thanks @hartmark
- **fix(kie):** correct 12 more KIE Market catalog ids that were sent to `createTask` unchanged but diverge from KIE's documented upstream `model` values — GPT Image 2 T2I/I2I (drops the `gpt/` prefix), GPT Image 1.5 T2I/I2I (`gpt-image/` namespace), Seedream 5.0 Lite T2I/I2I (drops the `.0`), all 4 Flux 2 variants (`flux-2/` namespace, generic variant renamed `flex`), and Wan 2.7 Image / Image Pro (dash instead of dot) — each verified individually against the literal example request published on docs.kie.ai. `#11326`'s "everything else already matches" claim was wrong a second time (#11296); `z-image/4.0-*`/`z-image/4.5-*` and `flux/kontext` remain open, documented as unresolved in `KIE_MARKET_UPSTREAM_MODEL_IDS`'s comment pending further verification. ([#11667](https://github.com/diegosouzapw/OmniRoute/pull/11667))
- **fix(dashboard):** `useApiKeySave.handleSaveApiKey` no longer forces a full upstream `/models` catalog sync on every non-curated provider connection save — callers can now pass `skipModelSync: true` to opt out, so a workflow that only wants to add one manual model no longer floods the provider's available-models list with hundreds/thousands of synced entries. The flag is a client-side intent signal only and is stripped before the connection payload is POSTed to `/api/providers`; default behavior (full sync on save) is unchanged when the flag is omitted (#11324) ([#11664](https://github.com/diegosouzapw/OmniRoute/pull/11664))
- **Provider connections:** keep `tokenExpiresAt` when a connection is created. The create-path allowlist omitted it, so every insert stored NULL and the dashboard token badge could read a fresh connection as expired until its first background refresh ([#11368](https://github.com/diegosouzapw/OmniRoute/pull/11368)). — thanks @ntdatt812
- **fix(combos):** the combo builder's precision-select, global-model-search, and manual-entry flows now serialize a model step's `model` string using the provider's already-computed routing-alias prefix (e.g. `oc/`) instead of rebuilding it from the raw canonical `providerId`, fixing the no-auth "OpenCode Free" provider (`opencode`) being routed to the unrelated paid "OpenCode Zen" provider (`opencode-zen`) because `opencode` doubles as a manual routing-prefix override ([#11433](https://github.com/diegosouzapw/OmniRoute/issues/11433)). ([#11670](https://github.com/diegosouzapw/OmniRoute/pull/11670))
- **fix(radar):** the catalog feed cache now keeps `generatedAt`, the date the feed's data was built, next to `fetchedAt`, the date this install downloaded it (#11435). The feed schema requires that date and the sync path validates it, but the cache dropped it — so a feed fetched minutes ago and one carrying weeks-old figures looked identical to everything downstream, including the dashboard's "Last fetched" line. `getRadarCatalog().meta` and `GET /api/radar/status` now report both dates, the latter as its own field rather than folded into `version` — and omitted entirely for the offers and intel caches, which keep no build date, where a `null` would read as "unknown" rather than "never stored". The dashboard still shows only the fetch time; surfacing the build date there needs a new translated label and is left to a follow-up. Rows cached before migration 163 read back as `null`: unknown stays unknown instead of borrowing the fetch time. The referrals cache has persisted the same date since migration 142. — thanks @maxmad64bis
- **fix(cli):** postinstall no longer fills `JWT_SECRET` and `API_KEY_SECRET` in the installed package's `.env` (#11436). `.env.example` ships both blank on purpose: the server restores them from its durable store, or generates and persists them there, in `ensureSecrets()`. Pre-filling them defeated that — the file lives inside the package directory, so `npm i -g` replaced it and postinstall wrote _different_ values, while `ensureSecrets()` (which only acts on an empty variable) never got to restore the real ones. Both secrets rotated silently on every update, invalidating dashboard sessions and API-key CRCs. `STORAGE_ENCRYPTION_KEY` left the same list for the same reason in #1622; its comment pointed at a function that no longer exists and now names the real provisioning path. — thanks @maxmad64bis
- **fix(cli):** the CLI now says when a loaded `.env` lives inside the installed package directory (#11437). It already announces every env file it reads, without distinguishing the ones that survive an update from the one that does not: `npm i -g` replaces the package directory wholesale, so values set there are gone at the next update, silently. The warning names the durable path to move them to, and fires only when that file actually supplied a value — a file entirely shadowed by a durable one supplied nothing. A development checkout stays silent: there the same path is stable and documented in `SETUP_GUIDE.md`. — thanks @maxmad64bis
- **fix(free-models):** the shared `isFreeModel()` predicate no longer reports catalog entries marked `freeType: "discontinued"` as free, so `hidePaidModels` can't route to Pollinations' seven premium models that now require a paid key ([#11441](https://github.com/diegosouzapw/OmniRoute/pull/11441)) — thanks @maxmad64bis
- fix(ui): let AnySearch use the normal provider-icon fallback when LobeHub has no matching icon (#11449)
- 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. ([#11460](https://github.com/diegosouzapw/OmniRoute/pull/11460)) — thanks @xiaoyaner0201
- 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. ([#11460](https://github.com/diegosouzapw/OmniRoute/pull/11460)) — thanks @xiaoyaner0201
- 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. ([#11460](https://github.com/diegosouzapw/OmniRoute/pull/11460)) — thanks @xiaoyaner0201
- **fix(combo):** attach the same combo-diagnostics trace (`poolSize`/`attemptOrder`/`excluded`/`terminalReason`, plus `x-omniroute-combo-*` headers) to the round-robin strategy's and the nested pipeline/fusion runtime-unit loop's "Maximum combo retry limit reached" 503 that the priority-strategy path already attaches for the identical terminal condition — previously those two paths returned a bare, contextless 503 ([#11462](https://github.com/diegosouzapw/OmniRoute/issues/11462)). ([#11665](https://github.com/diegosouzapw/OmniRoute/pull/11665))
- **fix(bun):** use native `bun:sqlite` in `bootstrap-env` and `sync-env` to avoid loading `better-sqlite3` N-API addon during Bun startup ([#11468](https://github.com/diegosouzapw/OmniRoute/pull/11468)) — thanks @TheDemonTuan
- **fix(docker-bun):** make `Dockerfile.bun` install reliable with pre-install script helpers and native `bun:sqlite` smoke check ([#11470](https://github.com/diegosouzapw/OmniRoute/pull/11470)) — thanks @TheDemonTuan
- **fix(docker-bun):** remove every vendored `better-sqlite3` native addon from the Bun runtime image so startup cannot abort before the `bun:sqlite` fallback ([#11482](https://github.com/diegosouzapw/OmniRoute/pull/11482)) — thanks @TheDemonTuan
- **fix(autoCombo):** effort/alias model variants (`gpt-5.6-sol-xhigh`, `gpt-5.6`, cursor's `claude-4.6-opus-high`) inherit their base model's task fitness instead of falling to the wildcard 0.5 ([#11492](https://github.com/diegosouzapw/OmniRoute/pull/11492)) — thanks @MumuTW
- fix(db): rate-limit repeated Arena ELO leaderboard fetch-failure warnings instead of logging one per sync attempt (#11500) ([#11989](https://github.com/diegosouzapw/OmniRoute/pull/11989)) — thanks @hartmark
- **fix(db):** dedupe the raw `[Encryption] Decryption failed...` log line emitted by the lazy-decrypt views (`createLazyRowProxy`/`createLazyConnectionView`), which power `getProviderConnections()` and were re-triggering that line on every CredentialHealth/model-sync cycle for the same corrupt or stale-key credential — a fresh Proxy over a fresh row on every cycle meant the per-proxy memoization never suppressed it, unlike the dedup `decryptConnectionFields()` already had since [#9927](https://github.com/diegosouzapw/OmniRoute/issues/9927). Now shares that dedupe tracking so the line logs at most once per credential ([#11500](https://github.com/diegosouzapw/OmniRoute/issues/11500)). ([#11678](https://github.com/diegosouzapw/OmniRoute/pull/11678))
- **fix(intelligence):** Arena ELO sync stores a synthesized base-model row for every effort/harness variant (`claude-opus-5-max``claude-opus-5`, `gpt-5.6-sol-xhigh (codex-harness)``gpt-5.6-sol`), so bare model requests reach the synced intelligence layer instead of the static fallback table; the hand-maintained `MODEL_ALIAS_MAP` — which collapsed generations (`gpt-5.5``gpt-5`) — is gone ([#11506](https://github.com/diegosouzapw/OmniRoute/pull/11506)) — thanks @MumuTW
- **fix(autoCombo,sse):** catalog hygiene — the static task-fitness table no longer ranks retired models above live ones (dead rows dropped, patterns matched on segment boundaries), the 7 deprecation aliases that forwarded to retired or non-existent models now point at the vendors' published replacements, an alias is no longer rewritten when the serving provider offers the id as-is, and a new `check:model-lifecycle` gate diffs both tables against a vendor lifecycle snapshot ([#11507](https://github.com/diegosouzapw/OmniRoute/pull/11507)) — thanks @MumuTW
- **fix(sse):** stop dropping resolved `thoughtSignature` values on parallel (multi tool-call) turns sent to Gemini 3.x — the claude→gemini and openai→gemini translators previously kept the signature only on the *first* function call of a message, causing Gemini to reject subsequent calls in the same turn with HTTP 400 "Function call is missing a thought_signature"; each function call now keeps its own resolved signature ([#11510](https://github.com/diegosouzapw/OmniRoute/issues/11510)). ([#11669](https://github.com/diegosouzapw/OmniRoute/pull/11669))
- Treat Docker publishing from a non-default release branch as an expected skip instead of a failed workflow, while preserving the guard that prevents it from overwriting the `next` channel. ([#11527](https://github.com/diegosouzapw/OmniRoute/pull/11527)) — thanks @pacocartones
- fix(search): prefer credentialed providers over duckduckgo-free fallback (#11524) — the fallback-only loop ran before the credentialed-providers loop in `executeWebSearch`, making configured providers unreachable when `duckduckgo-free` was available. ([#11565](https://github.com/diegosouzapw/OmniRoute/pull/11565)) — thanks @vsd2807
- **fix(sse):** cap the upstream headers-wait phase for STREAMING requests to a client-realistic ceiling (110s, under Codex's own ~120s hard client-abort window) instead of the flat 10-minute `FETCH_TIMEOUT_MS` default — that default was 5x longer than the body-phase readiness watchdog's own adaptive bound, so a request whose upstream never returned any response at all (not even headers, e.g. a stalled NVIDIA target behind a tool-heavy Responses→Chat translation) kept the client connection alive on keepalives only, guaranteeing the client's own patience ran out first with an opaque 499 instead of OmniRoute detecting and failing the stall fast. Non-streaming requests are unaffected — they keep the existing flat default (`open-sse/utils/fetchStartTimeoutPolicy.ts`) (#11526) ([#11668](https://github.com/diegosouzapw/OmniRoute/pull/11668))
- **fix(ci):** protocol-clients E2E harness boots the peer-stamped custom server (`run-next.mjs dev`) instead of the bare `next` CLI, so `LOCAL_ONLY` locality resolves from the real TCP peer stamp and `/api/mcp/audit` answers 200 (open bootstrap preserved via an `OMNIROUTE_E2E_BOOTSTRAP_MODE=open` credential clear in `run-next.mjs`, pinned to loopback `HOST`) instead of a deterministic 403 (#11535) ([#11549](https://github.com/diegosouzapw/OmniRoute/pull/11549)) — thanks @jonlwheat2-gif
- **fix(docs):** the Auto-Combo engine is described with the number of factors it actually declares. Four different counts were in circulation — 6, 9, 13 and 14 across `AGENTS.md`, four guides and six code comments — while `DEFAULT_WEIGHTS` declares 15. `check:docs-counts` now reads that number from the source, so adding a factor without updating the prose fails CI ([#11545](https://github.com/diegosouzapw/OmniRoute/pull/11545)) ([#11553](https://github.com/diegosouzapw/OmniRoute/pull/11553)) — thanks @maxmad64bis
- **fix(quota-share):** in-flight leases are per request, so an aborted request's slot ages out instead of being kept alive by later traffic on the same connection ([#11547](https://github.com/diegosouzapw/OmniRoute/pull/11547)) — thanks @abhisheksharma2411
- The free-tier summary route now serves the refreshed Radar catalog when the feed is active — the same numbers the dashboard shows — and states which catalog answered plus its real build date, instead of always reporting release-frozen figures with a stale curation date. The supporter-key live feed stays reserved to authenticated callers of the instance. ([#11550](https://github.com/diegosouzapw/OmniRoute/pull/11550)) — thanks @maxmad64bis
- **sse:** fix LiveWS/embed-WS servers crashing at startup under the Node/tsx runtime — `liveServer.ts`, `embedWsProxy.ts` and `apiBridgeServer.ts` imported `@/shared/utils/httpClientAbortGuard` without the `.mjs` extension, so the client-abort crash guard added by [#11556](https://github.com/diegosouzapw/OmniRoute/pull/11556) was unreachable and every dependent test failed with `ERR_MODULE_NOT_FOUND` ([#11556](https://github.com/diegosouzapw/OmniRoute/pull/11556)). ([#11671](https://github.com/diegosouzapw/OmniRoute/pull/11671)) — thanks @jonlwheat2-gif / @yxyxy
- fix(chat): shed any of the matched provider-node's routing identifiers (public prefix or internal id) before model lookup, so composites like `<connId>/<connId>/<model>` no longer reach upstream verbatim (#11557) — thanks @maxmad64bis
- **fix(cache):** a semantic-cache entry now expires on its TTL instead of surviving until the next UTC midnight, and `dbEntries` stops counting expired rows ([#11573](https://github.com/diegosouzapw/OmniRoute/pull/11573)) — thanks @ntdatt812
- **fix(api):** `/v1/models` schedules its stale-while-revalidate rebuild through Next's `after()` again, so a stale catalog reaches the client before the rebuild blocks the event loop ([#11574](https://github.com/diegosouzapw/OmniRoute/pull/11574)) — thanks @ntdatt812
- **fix(check):** the `known-symbols` executor conformance gate awaits the now-async `getExecutor()`, so it stops reporting all 142 registered aliases as dead symbols and can detect a lazy import that fails ([#11582](https://github.com/diegosouzapw/OmniRoute/pull/11582)) — thanks @ntdatt812
- **fix(pack):** register `bin/cli/utils/volatileEnvPath.mjs` in `PACK_ARTIFACT_REQUIRED_PATHS`, so the prune can no longer silently drop a module the published CLI imports on every boot ([#11588](https://github.com/diegosouzapw/OmniRoute/pull/11588)) — thanks @ntdatt812
- **i18n(vi):** translate the five reliability-column strings on the free-provider rankings page, which rendered as `__MISSING__:` markers in the Vietnamese UI ([#11589](https://github.com/diegosouzapw/OmniRoute/pull/11589)) — thanks @ntdatt812
- **resilience:** restore the expired-connection retry-budget probe in the token-health sweep — the `!isGitHubAccessTokenOnlyConnection` carve-out reintroduced by #11608 contradicted the boundary pinned by #11592, so a GitHub connection parked at `expired` with retry budget remaining was never probed and could never self-heal ([#11592](https://github.com/diegosouzapw/OmniRoute/pull/11592)). ([#11672](https://github.com/diegosouzapw/OmniRoute/pull/11672)) — thanks @jonlwheat2-gif / @ntdatt812
- **fix(dashboard):** Provider Stats now displays combo success rates on their native 0100 percentage scale instead of multiplying them twice ([#11595](https://github.com/diegosouzapw/OmniRoute/pull/11595)) — thanks @pacocartones
- **fix(providers):** route DuckDuckGo Web Chat through the verified `duck.ai` endpoint, refresh its free model catalog, and avoid forwarding unsolved anti-abuse challenges ([#11597](https://github.com/diegosouzapw/OmniRoute/pull/11597)) — thanks @solstxce
- **fix(ui):** Console log Refresh and Copy controls now expose localized accessible names, keep copy actions visible on keyboard focus, and announce copy completion safely ([#11599](https://github.com/diegosouzapw/OmniRoute/pull/11599)) — thanks @pacocartones
- Dropped the stale-`.eslintcache` `restore-keys` fallback from both "Restore ESLint file cache" steps in `ci.yml`, so the blocking `Lint` job can no longer be served per-file verdicts computed under a different lint config, suppressions file or lockfile. `quality.yml` had already dropped it in #11963; `ci.yml` — the workflow that actually gates PRs — had not (#11600). ([#11996](https://github.com/diegosouzapw/OmniRoute/pull/11996))
- fix(ci): pass `--pass-on-unpruned-suppressions` in `run-eslint-json.mjs` so the CI Lint job no longer fails when a suppression is merely orphaned by a genuine fix, mirroring the identical fix already in `validate-release-green.mjs` (#11600) ([#11983](https://github.com/diegosouzapw/OmniRoute/pull/11983)) — thanks @hartmark
- fix(handoff): enforce provider allowlist for universal handoff (#11602) — universal handoff now skips summarization when the selected summary model's provider is not included in the configured provider allowlist. ([#11619](https://github.com/diegosouzapw/OmniRoute/pull/11619)) — thanks @vsd2807
- **fix(dashboard):** Show a stable error state when Search Analytics returns an HTTP error or malformed data ([#11603](https://github.com/diegosouzapw/OmniRoute/pull/11603)) — thanks @pacocartones
- **test(gamification):** pin the aggregate profile level to the XP-derived semantics of #11604`getAggregateXp()` now derives `currentLevel` from the summed XP (`calculateLevel(sum)`), not `MAX(stored current_level)`, and the #3484 fixture levels are aligned with the XP curve ([#11604](https://github.com/diegosouzapw/OmniRoute/pull/11604)). ([#11676](https://github.com/diegosouzapw/OmniRoute/pull/11676)) — thanks @jonlwheat2-gif / @pacocartones
- **fix(dashboard):** Keep the Profile level and progress aligned with aggregate XP, including bounded handling for invalid totals ([#11604](https://github.com/diegosouzapw/OmniRoute/pull/11604)) — thanks @pacocartones
- **fix(dashboard):** Prevent locked hidden badges from revealing their icon or opening private badge details before they are earned ([#11605](https://github.com/diegosouzapw/OmniRoute/pull/11605)) — thanks @pacocartones
- **fix(dashboard):** Restore keyboard focus after shared modals close and cancel delayed autofocus during cleanup ([#11607](https://github.com/diegosouzapw/OmniRoute/pull/11607)) — thanks @pacocartones
- **fix(dashboard):** Enable Enter and Space activation for clickable data-table rows without hijacking nested controls ([#11610](https://github.com/diegosouzapw/OmniRoute/pull/11610)) — thanks @pacocartones
- **fix(autoCombo,sse):** vendor-retired catalog ids are dropped from the auto-combo candidate pool and no longer win on leftover `arena_elo` / `user_override` rows; `getModelLifecycleDecision` consults `model-lifecycle.json` (prefix-stripped) so aggregator traffic is not `untracked` for ids the snapshot already knows ([#11625](https://github.com/diegosouzapw/OmniRoute/issues/11625)) ([#11626](https://github.com/diegosouzapw/OmniRoute/pull/11626)) — thanks @MumuTW
- **test(chatcore):** move Codex/Claude combo fixtures off models the lifecycle guard now rejects — `gpt-5.1-codex`/`gpt-5-codex` are vendor-retired (snapshot, #11626) and `claude-3-5-sonnet-20241022` is shut down, so native-passthrough and combo-fallback tests switched to `gpt-5.6-sol` and `claude-sonnet-4.6` ([#11626](https://github.com/diegosouzapw/OmniRoute/pull/11626)). ([#11675](https://github.com/diegosouzapw/OmniRoute/pull/11675)) — thanks @jonlwheat2-gif / @MumuTW
- fix(opencode-plugin): stop a free-tier budget that rounds up across a magnitude from rendering as `1000K`/`1000M` in the model picker — `fmtTokens` chose its unit from the raw token count and then rounded with `toFixed(1)`, so 999,950999,999 printed as `1000K` rather than `1M` and just under a billion printed as `1000M` rather than `1B` ([#11684](https://github.com/diegosouzapw/OmniRoute/pull/11684)) — thanks @abhisheksharma2411
- Cap prose-derived Antigravity quota resets at the configured model cooldown maximum while preserving authoritative Retry-After headers and Google RetryInfo hints. (#11695) Thanks @pacocartones. ([#11823](https://github.com/diegosouzapw/OmniRoute/pull/11823))
- Compare `_rebaseline_*` notes structurally in the ratchet-bank verifier so an object-valued note no longer reports itself as rewritten on an unmodified tree — the nightly `bank-ratchet-shrinks` lane had been aborting on every run since 2026-08-11, leaving 41,672 lines of earned file-size shrinks unbanked (#11702). ([#11892](https://github.com/diegosouzapw/OmniRoute/pull/11892)) — thanks @MumuTW
- **fix(build):** `prepublish.ts` bundles the ChatGPT Web (Codex) MCP bridge through `runBuildTool()` instead of spawning `npx.cmd` raw, fixing the build crash on Node ≥ 20/Windows where `.cmd` shims cannot be spawned without a shell (EINVAL) ([#11704](https://github.com/diegosouzapw/OmniRoute/issues/11704)) ([#11706](https://github.com/diegosouzapw/OmniRoute/pull/11706)) — thanks @jonlwheat2-gif
- fix(codex): keep `parallel_tool_calls:false` on the translated Codex Responses Lite path (#11707) ([#11984](https://github.com/diegosouzapw/OmniRoute/pull/11984)) — thanks @hartmark
- **fix(tests):** the new Gemini Web Images retirement test (#11708) called the now-async `getExecutor()` synchronously, throwing `TypeError: getExecutor(...).getProvider is not a function` instead of asserting Gemini Web chat stays available. Awaited to match the R0.3 executor-registry refactor (#11220). ([#11868](https://github.com/diegosouzapw/OmniRoute/pull/11868))
- **fix(search):** Allow REST and MCP web-fetch requests to select the advertised `anysearch-search` provider explicitly ([#11716](https://github.com/diegosouzapw/OmniRoute/pull/11716)) — thanks @pacocartones
- **fix(providers):** Keep overnight peak-hour protection active after midnight according to the window's configured start day ([#11718](https://github.com/diegosouzapw/OmniRoute/pull/11718)) — thanks @pacocartones
- **fix(providers):** correct all five g4f.space gateway descriptions to document proof-of-work credits or member API keys, variable limits, and the remote prompt boundary instead of promising free keyless access at five requests per minute ([#11738](https://github.com/diegosouzapw/OmniRoute/pull/11738)).
- **fix(db):** `model_capabilities` is created by a migration instead of lazily on the first models.dev sync, so a clean install and an upgraded install converge on the same schema regardless of which features have run ([#11845](https://github.com/diegosouzapw/OmniRoute/pull/11845))
- **fix(ci):** `check:install-upgrade` now fails on an `npm` install truncated by ENOSPC (npm reports it as a warning and still exits 0), authenticates its health probe so the version assertion works against the hardened health payload, frees the clean-install tree before the upgrade phase, and no longer reports a schema divergence computed from a boot that never served ([#11845](https://github.com/diegosouzapw/OmniRoute/pull/11845))
- **fix(routing):** harden session-affinity key derivation against oversized explicit IDs and empty structural-payload collisions, while recognizing Gemini and common text request shapes ([#11746](https://github.com/diegosouzapw/OmniRoute/pull/11746)) — thanks @MumuTW
- **fix(packages/browser-pool):** regenerate `package-lock.json` so the `packages/browser-pool` workspace's locked `playwright`/`@types/node` (and transitives) match its `package.json` specs, fixing cache-only/offline installs (`npm ci --offline`, Nix `buildNpmPackage`) that previously failed with `ENOTCACHED` ([#11747](https://github.com/diegosouzapw/OmniRoute/issues/11747)) — thanks @benjaminkitt ([#11784](https://github.com/diegosouzapw/OmniRoute/pull/11784)) — thanks @geekyNads
- **fix(assets):** Stop distributing 79 provider and CLI logos without verified source/license provenance, keep every provider/tool available through the internal generic icon, and prevent those IDs from falling through to an external icon CDN. ([#11750](https://github.com/diegosouzapw/OmniRoute/pull/11750))
- **fix(providers):** retire the common `chatgpt-web` / `cgpt-web` provider because its pre-key and proof-of-work provenance could not be cleared ([#11754](https://github.com/diegosouzapw/OmniRoute/pull/11754)) — explicit chat and image requests, slash-prefixed models, persisted aliases, proxy overrides, and combo targets now fail closed with a sanitized HTTP `410` / `PROVIDER_RETIRED` before upstream dispatch. Migration 163 tombstones matching connections and invalidates their active leases without deleting history or widening API-key allowlists. The independent `chatgpt-web-codex` / `cgpt-codex` provider and its MIT-noticed vendored implementation remain available.
- **fix(usage):** quota and usage refresh no longer 409 when an exclusive lease reserves the connection ([#11758](https://github.com/diegosouzapw/OmniRoute/pull/11758)) — thanks @TheDemonTuan
- Keep the embedding registry's vector width and `embedding` type on models when a synced model exists for the same id, so `/v1/models` no longer reports registry-described embedding models widthless or untyped (#11761) — thanks @rahilmavani
- Correct `google/gemini-embedding-001` on the OpenRouter route to 3072 dimensions, the width it returns when `dimensions` is not sent (#11761) — thanks @rahilmavani
- **fix(vision-bridge):** Restrict automatic vision routing and fallbacks to models available in authoritative live provider catalogs, including aliases and effort variants ([#11767](https://github.com/diegosouzapw/OmniRoute/issues/11767)) — thanks @pacocartones ([#11813](https://github.com/diegosouzapw/OmniRoute/pull/11813))
- **fix(providers):** reclassify Cerebras as a one-time $5 signup credit (payment method required, 30-day validity), not a recurring no-card 1M tokens/day trial ([#11773](https://github.com/diegosouzapw/OmniRoute/issues/11773)) ([#12591](https://github.com/diegosouzapw/OmniRoute/pull/12591)) — thanks @HouMinXi
- fix(cli): stop prepublish from re-rebuilding the already-built ESM-only opencode-plugin dist (#11787) ([#11990](https://github.com/diegosouzapw/OmniRoute/pull/11990)) — thanks @hartmark
- **fix(db):** Fresh installs no longer log a non-fatal `no such table: compression_run_telemetry` warning when retention cleanup runs before the lazily-created telemetry table exists ([#11802](https://github.com/diegosouzapw/OmniRoute/pull/11802)) — thanks @RaviTharuma
- fix(sse): set X-OmniRoute-Selected-Connection-Id on successful combo dispatches so downstream consumers stop falling back to an empty connection id (#11810) ([#11986](https://github.com/diegosouzapw/OmniRoute/pull/11986)) — thanks @hartmark
- **fix(services):** embedded CLIProxyAPI lifecycle checks now use public `/healthz`, while model discovery uses the configured dedicated data-plane API key instead of the management password ([#11811](https://github.com/diegosouzapw/OmniRoute/pull/11811)) — thanks @RaviTharuma
- Fix persisted model steps returning an error in the playground route simulator, and warn when structural steps cannot be simulated. ([#11862](https://github.com/diegosouzapw/OmniRoute/pull/11862)) — thanks @pacocartones
- **fix(providers):** Antigravity's dynamic mitmAlias table no longer routes `gemini-3.7-flash-{high,medium,low}` to a literal tier-suffixed upstream id just because one connected account's own discovery listed it directly — those display ids always resolve through the safe `gemini-3.7-flash-tiered` static alias, so one account's Google-provisioned access no longer 404s every sibling account of the provider ([#11824](https://github.com/diegosouzapw/OmniRoute/issues/11824), [#11651](https://github.com/diegosouzapw/OmniRoute/issues/11651)) ([#11988](https://github.com/diegosouzapw/OmniRoute/pull/11988)) — thanks @hartmark
- **fix(dashboard):** Expose Profile loading, errors, page structure, and XP progress to assistive technologies ([#11838](https://github.com/diegosouzapw/OmniRoute/pull/11838)) — thanks @pacocartones
- **fix(socks):** `Agent.connectTimeout` now bounds both SOCKS handshake and TLS connect, and `family === null` no longer falls back to `fetch-socks` ([#11842](https://github.com/diegosouzapw/OmniRoute/pull/11842)) — thanks @maxmad64bis
- **perf(compression):** OOM mitigations for large payload hashing, memoization, and token estimation ([#11844](https://github.com/diegosouzapw/OmniRoute/pull/11844) — thanks @AndrianBalanescu)
- Excluded Next.js Node File Trace manifests (`*.nft.json`) from the published npm tarball. They are build-time metadata and are never read while serving, but had grown to 668.7 MB — 61% of the package — which pushed the upload past the registry limit and made `npm publish` fail with `413 Payload Too Large`. ([#11864](https://github.com/diegosouzapw/OmniRoute/pull/11864))
- **fix(cli):** keep the global OmniRoute server `--base-url` separate from provider-node `--endpoint` values in add, update, and validate commands ([#11860](https://github.com/diegosouzapw/OmniRoute/pull/11860)) — thanks @pacocartones
- **fix(config):** Nous Research's `Hermes-4-405B` model now displays as "Hermes 4 405B (Nous Research)" in both the provider registry and the free-model catalog, instead of the mislabelled "Hermes 4 7B" ([#11861](https://github.com/diegosouzapw/OmniRoute/issues/11861)) — thanks @Karan825 ([#11993](https://github.com/diegosouzapw/OmniRoute/pull/11993))
- **fix(provider/nous):** inject required user= tag into Nous Research inference requests to resolve upstream 400 "missing tags" error ([#11861](https://github.com/diegosouzapw/OmniRoute/issues/11861)) — thanks @Karan825 ([#12044](https://github.com/diegosouzapw/OmniRoute/pull/12044))
- **fix(build):** `npm run build` now fails in one second with a named package and a copy-pasteable fix when npm silently drops an externalised optional native dependency, instead of dying four minutes in with `Module not found: Can't resolve 'better-sqlite3'` ([#11863](https://github.com/diegosouzapw/OmniRoute/pull/11863)) — thanks @ujjawalkaushik1110
- **fix(install):** `postinstall` no longer throws `ReferenceError: isAndroid is not defined` — failing the whole `npm install` — when the `better-sqlite3` rebuild fallback times out; the manual-fix guidance is reachable again ([#11863](https://github.com/diegosouzapw/OmniRoute/pull/11863)) — thanks @ujjawalkaushik1110
- Fixed the Alibaba free-tier allowlist test that went red on its own once the shipped catalog's `validUntil` (2026-08-27) passed, leaving every PR and `main` with a failing `Unit Tests (1/8)`. The test now builds its own packs with dates it controls, and covers the expired-pack fallback that production has actually been serving. ([#11867](https://github.com/diegosouzapw/OmniRoute/pull/11867))
- Split the npm registry upload into its own GitHub-hosted job. npm refuses `--provenance` from a self-hosted runner (`422 ... Only "github-hosted" runners are supported`), which blocked the v3.8.50 publish; the heavy verification cannot move to a hosted runner, so it now hands the proven tarball over instead. ([#11868](https://github.com/diegosouzapw/OmniRoute/pull/11868))
- **fix(ui):** the shared `DataTable` loading state no longer reads its decorative ⏳ glyph out to assistive technology, and now carries the same `role="status"` / `aria-live="polite"` / `aria-busy="true"` semantics as `PageLoading` ([#11869](https://github.com/diegosouzapw/OmniRoute/pull/11869)) — thanks @pacocartones
- **fix(api):** Bound the `timeout` accepted by `POST /api/providers/[id]/login` to the same 15s600s window the Adobe Firefly and Conol browser-login paths already enforce, so a request can no longer pin a headful browser and the single active-login slot with an arbitrarily large poll budget. ([#11871](https://github.com/diegosouzapw/OmniRoute/pull/11871)) — thanks @pacocartones
- **fix(api):** Stop the tunnel and MITM routes from returning raw child-process error text, which disclosed host paths, binary install locations and Tailscale `tskey-*` credentials in a body some of these routes serve to non-loopback callers, and return a real 400 instead of a framework 500 when a tunnel request body fails validation. ([#11872](https://github.com/diegosouzapw/OmniRoute/pull/11872)) — thanks @pacocartones
- **fix(open-sse):** stream TTFT/ITL are sampled from a monotonic clock (`performance.now`) instead of `Date.now`, so an NTP correction or wall-clock jump can no longer inflate a genuine TTFT or produce a negative one that the `ttft >= 0` guard silently drops — protecting the router's OTel/EWMA/`usage_history`/speed-ranking signals ([#11873](https://github.com/diegosouzapw/OmniRoute/pull/11873)) — thanks @pacocartones
- **fix(providers):** xAI `reasoning_effort: "xhigh"` now reaches grok-4.6+ instead of being silently clamped to `"high"` ([#11879](https://github.com/diegosouzapw/OmniRoute/pull/11879)) — thanks @NoxzRCW
- **fix(dashboard):** the providers page no longer crashes into the error boundary when a provider id collides with an `Object.prototype` member (`constructor`, `__proto__`); icon lookups are own-property guarded ([#11880](https://github.com/diegosouzapw/OmniRoute/pull/11880)) — thanks @NoxzRCW
- **fix(skills):** injected skill tools declared in shorthand (`{"content": "string"}`) now forward valid JSON Schema, unblocking providers that validate tool schemas strictly such as Zhipu GLM on the Console Go tier ([#11881](https://github.com/diegosouzapw/OmniRoute/pull/11881)) — thanks @NoxzRCW
- **fix(api):** `POST /api/playground/simulate-route` now surfaces `combo-ref` and `provider-wildcard` persisted combo steps with a specific warning (naming the referenced combo, or the unresolved `provider/model` wildcard) instead of folding them into a generic "unsupported step" count; a `provider-wildcard` step is also now included as an unresolved target so the operator can see it is in the route (ported from [#11882](https://github.com/diegosouzapw/OmniRoute/pull/11882) — thanks @NoxzRCW). ([#11930](https://github.com/diegosouzapw/OmniRoute/pull/11930))
- **fix(translator):** the streaming OpenAI→Claude translator keeps upstream usage, including prompt-cache tokens, when it arrives on a trailing `choices: []` chunk (Fireworks and any upstream using `stream_options.include_usage`) ([#11883](https://github.com/diegosouzapw/OmniRoute/pull/11883)) — thanks @NoxzRCW
- **fix(cli):** `omniroute update --apply` now tells you explicitly whether a running server was detected and, if so, that you must run `omniroute restart` to apply the update — it never restarted anything and previously implied the update was already live once files were installed. The dashboard's npm-mode Update flow (`/api/system/version`) now tries OmniRoute's own PID-file-managed supervisor before falling back to pm2, and reports an honest "restart required" step instead of a silent pm2-only "skipped" that read like a completed update. The server-side latest-version lookup backing the dashboard's update banner also gained `--prefer-online`, closing the same stale-npm-cache class already fixed in the CLI's own copy for #4376 ([#11885](https://github.com/diegosouzapw/OmniRoute/issues/11885)). ([#12005](https://github.com/diegosouzapw/OmniRoute/pull/12005))
- **fix(resilience):** clear persisted LKGP pins when a target suffers connection/provider exhaustion or is skipped before dispatch due to cooldown/exhaustion/unavailability, preventing subsequent requests from repeatedly prioritizing known-dead providers ([#11911](https://github.com/diegosouzapw/OmniRoute/issues/11911)). ([#12013](https://github.com/diegosouzapw/OmniRoute/pull/12013)) — thanks @HouMinXi
- **fix(sse):** the OpenAI→Claude stream translator now defers the terminal `message_delta`/`message_stop` emission until the real usage block has arrived (or a genuine end-of-stream flush forces it) instead of emitting it immediately on `finish_reason` — previously, when the trailing usage-only chunk (`{"choices":[],"usage":{...}}`) arrived *after* the `finish_reason` chunk (the normal order for Fireworks/vLLM/Together and other `stream_options.include_usage` upstreams), the client-visible `message_delta` still carried stale/zero usage even though `state.usage` was internally corrected too late to matter (ported from [#11915](https://github.com/diegosouzapw/OmniRoute/pull/11915) — thanks @HouMinXi). ([#11933](https://github.com/diegosouzapw/OmniRoute/pull/11933))
- **fix(sse):** `stripStore()` now forces `store=false` for stateless OpenAI-compatible Responses-API targets (unless the connection has explicitly opted in via `providerSpecificData.openaiStoreEnabled`), instead of only handling the `openai`/`agentrouter` cases — a client-supplied `store` value previously passed through untouched to backends that don't actually persist responses server-side ([#11916](https://github.com/diegosouzapw/OmniRoute/pull/11916) — thanks @HouMinXi).
- **fix(models):** custom provider-node models (synced, custom, and alias-backed) now appear under their configured prefix in the unified catalog when the operator's model-id prefix mode is canonical, instead of being dropped whenever alias-inclusion was otherwise disabled ([#11918](https://github.com/diegosouzapw/OmniRoute/pull/11918) — thanks @HouMinXi).
- **fix(models):** the unified model catalog now suppresses stale static registry models (including effort-tier variants) for any provider whose active connection has an authoritative live synced catalog, not only providers already using exclusive-synced-listing — a connection with `providerUsesAuthoritativeLiveCatalog` previously kept serving both the live-synced models and the stale static rows side by side ([#11919](https://github.com/diegosouzapw/OmniRoute/pull/11919) — thanks @HouMinXi).
- **fix(ui):** `ProviderIcon`'s three remaining unguarded lookups (`PROVIDER_ICON_ALIASES`, `LOCAL_SVG_ALIASES`, `THEMED_SVGS`) now use `Object.hasOwn()` own-property checks — a provider id such as `constructor` or `__proto__` previously resolved through the prototype chain instead of falling through to the unknown-provider CDN fallback (`getLobeProviderIcon()` itself was already guarded by [#11880](https://github.com/diegosouzapw/OmniRoute/pull/11880)); `ProviderPageHeader`'s `color` field is also now optional, matching the rest of the component's defensive typing (ported from [#11920](https://github.com/diegosouzapw/OmniRoute/pull/11920) — thanks @HouMinXi). ([#11935](https://github.com/diegosouzapw/OmniRoute/pull/11935)) — thanks @NoxzRCW
- fix(ollama): preserve multi-byte UTF-8 content split across stream chunks in the Ollama NDJSON transform, which previously corrupted CJK/emoji into U+FFFD (#11921) — thanks @pacocartones
- **fix(providers):** OrcaRouter chat requests now target `/v1/chat/completions` instead of the bare `/v1` API root, fixing the upstream `404 Invalid URL (POST /v1)` ([#11923](https://github.com/diegosouzapw/OmniRoute/pull/11923)). — thanks @echel0nn
- **fix(plugins):** deliver the `onStreamComplete` event to disk-installed plugins. The event shipped in v3.8.50 (#9669) was emitted internally but had no plugin-facing wiring, so no plugin could ever subscribe: the manifest schema silently dropped `hooks.onStreamComplete`, and the loader/manager only knew the seven legacy hooks. `onStreamComplete` is now a declarable manifest hook, wired through the loader and registered by the manager like the other hooks, and its payload carries a `requestId` so consumers can correlate the stream-completion event with the originating request ([#11934](https://github.com/diegosouzapw/OmniRoute/pull/11934)) — thanks @amaleta / @pacocartones
- **fix(db):** the Qdrant embedding-model dropdown now lists local/self-hosted providers (Ollama, LM Studio, vLLM, etc.) — an active connection is treated as "configured" when the provider allows an optional API key, not only when it has a real key or OAuth, so a running local embedding provider is no longer hidden from the picker ([#11949](https://github.com/diegosouzapw/OmniRoute/issues/11949)) ([#11995](https://github.com/diegosouzapw/OmniRoute/pull/11995))
- **fix(providers):** Vertex AI Anthropic partner-model discovery now calls the Model Garden `v1beta1` publisher list (`/v1beta1/publishers/anthropic/models`, global) and parses its `publisherModels` envelope, so Claude models auto-synced from Vertex populate the active live catalog and route at request time instead of returning `Model '<id>' is not available in the active live catalog` ([#11991](https://github.com/diegosouzapw/OmniRoute/issues/11991)) — thanks @fabioluissilva ([#11998](https://github.com/diegosouzapw/OmniRoute/pull/11998))
- **fix(cli):** support `--base-url` alongside `--endpoint` in `omniroute nodes add`, `update`, and `validate` subcommands to prevent global `--base-url` shadowing issues ([#11999](https://github.com/diegosouzapw/OmniRoute/issues/11999)). ([#12033](https://github.com/diegosouzapw/OmniRoute/pull/12033)) — thanks @HouMinXi
- **fix(providers):** `cloudflare-ai` no longer refuses image content parts for every Workers AI model ([#12002](https://github.com/diegosouzapw/OmniRoute/pull/12002)) — the plain-string `content` requirement behind #2539 is carried by the _model_ schema, not by the `/ai/v1/chat/completions` endpoint (measured: an all-text part array returns 200 on `@cf/mistralai/mistral-small-3.1-24b-instruct`, `@cf/meta/llama-4-scout-17b-16e-instruct` and `@cf/meta/llama-3.3-70b-instruct-fp8-fast`, and 400 on the text-only `@cf/qwen/qwen2.5-coder-32b-instruct`). `transformRequest()` flattened every array and threw on the first non-text part (#6390), so image input was refused for vision-capable Cloudflare models that accept it. All-text arrays are still flattened — the one shape every model accepts — while an array carrying a non-text part is passed through untouched, so the attachment is still never silently dropped. Regression guards: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`. — thanks @davidlinfr
- **fix(resilience):** decouple the limiter-managed execution backstop from the queue-wait budget — new `requestQueue.executionMaxWaitMs` (env `RATE_LIMIT_EXECUTION_MAX_WAIT_MS`, default 600000 = 10 min) now feeds Bottleneck's post-dispatch `expiration`, while `requestQueue.maxWaitMs` keeps its documented queue-wait semantics. Previously the queue-wait budget doubled as the execution expiration, so legitimate long-running calls on non-incremental gateways (whole generation buffered before the first upstream byte, e.g. Console Go / Command Code tiers serving GLM models) were killed mid-flight at the queue budget with a false 504 `RATE_LIMIT_EXECUTION_TIMEOUT` — the local limiter undercut the provider-aware upstream fetch-start timeouts. The surfaced 504 message now names `requestQueue.executionMaxWaitMs`; the error keeps the #4165 guarantees (disclaims an upstream timeout, preserves the Bottleneck error as `cause`, branded code + trusted provenance, classified request-scoped so combo falls back). A real queue-wait bound (the `Promise.race` around `limiter.schedule()` sketched in #9533) remains future work. (#12025) ([#12027](https://github.com/diegosouzapw/OmniRoute/pull/12027)) — thanks @alvinveroy
- **Call logs:** keep the `error` field when an artifact exceeds the storage cap, instead of replacing it with the omission marker. The error is the only field that says *why* a request failed and is typically ~90 bytes next to the multi-hundred-KB bodies that trip the cap, so dropping it left a size-limited row undiagnosable — a provider outage, a local timeout and an upstream 400 all rendered identically. It is now preserved at every fallback stage, truncated to 4KB if it is itself large ([#12026](https://github.com/diegosouzapw/OmniRoute/issues/12026)). ([#12095](https://github.com/diegosouzapw/OmniRoute/pull/12095)) — thanks @ntdat812
- **fix(diagnostics):** preserve the error field (truncated to 4KB with a `[truncated: …]` suffix) in every call-log artifact size-limit fallback stage. Previously the minimal fallback replaced the error with `[omitted: call log artifact size limit exceeded]`, so an oversized artifact row showed nothing about WHY the request failed — e.g. 91 of 847 opencode-go 504 rows on one production instance were undiagnosable from the dashboard. Oversized request/response bodies are still omitted exactly as before; the error cap is independent of the payload sizes that tripped the fallback. (#12026) ([#12027](https://github.com/diegosouzapw/OmniRoute/pull/12027)) — thanks @alvinveroy
- **fix(sse):** OpenAI Responses clients that declare the native `web_search` tool now receive a spec-shaped `web_search_call` output item with `action.sources` alongside the preserved function-call round-trip, so search results executed through OmniRoute's own search backend are consumable by standard Responses clients (Codex, pi-web-access, …). ([#12031](https://github.com/diegosouzapw/OmniRoute/pull/12031)) — thanks @watchingdogs
- **fix(cli):** use in-thread alias resolver hooks on modern runtimes to avoid deprecation noise and improve Node.js forward compatibility ([#12073](https://github.com/diegosouzapw/OmniRoute/issues/12073)). ([#12083](https://github.com/diegosouzapw/OmniRoute/pull/12083)) — thanks @xiaoyaner0201
- **fix(combo):** an operator-set **Agent Features → Context length** on a combo is now honored at request time. The value was persisted and advertised through `/v1/models`, but `resolveComboContextLimit()` never consulted it — so a multi-target combo whose members carry no per-model window fell through to the provider's generic `defaultContextLength` (openrouter 128000, command-code 200000) and rejected large requests with `Input exceeds context window … limit 128000` despite the combo being explicitly sized much larger. An identical single-target combo worked, because it collapses to its concrete target before the guard runs. Invalid values (0/negative/NaN/Infinity) are ignored, so the existing target → combo-min → fallback order is unchanged. ([#12090](https://github.com/diegosouzapw/OmniRoute/pull/12090)) — thanks @adivekar-utexas
- **fix(sse):** passthrough streams now estimate usage on finish when upstream closes without usage even with `stream_options.include_usage` — avoids `0 tokens / 0%` for providers that stay silent (and correctly handles trailing empty-choices usage) ([#12151](https://github.com/diegosouzapw/OmniRoute/pull/12151)) — thanks @maxmad64bis
- **fix(combos):** clearing an agent feature in the combos editor now persists — unchecking context cache protection, or emptying the system message or tool filter, sends an explicit `null` instead of dropping the field from the `PUT` body, which the update merge read as "leave unchanged" ([#12177](https://github.com/diegosouzapw/OmniRoute/pull/12177)) — thanks @foreveryh / @ntdatt812
- **fix(memory):** self-hosted embedding endpoints now vectorize — the vector width is measured from the first embedding that comes back instead of being read from a registry that cannot describe them, so `vec_memories` is created and memories stop piling up unvectorized behind a green health check ([#12180](https://github.com/diegosouzapw/OmniRoute/pull/12180)) — thanks @kanade-hoshino / @ntdatt812
- **fix(translator):** the leading `system` message now reaches Responses-API upstreams when its `content` is a content-part array — it was read as `typeof content === "string" ? content : ""`, so a prompt-caching client (Anthropic `cache_control`, the shape LiteLLM and the Anthropic SDK emit) had its entire system prompt replaced by an empty `instructions`. The request was still accepted with a normal `prompt_tokens` count, so the model answered with no instructions and nothing in the response said they were missing. Mid-conversation system turns already handled the array shape ([#7056](https://github.com/diegosouzapw/OmniRoute/pull/7056)); only the first one did not ([#12206](https://github.com/diegosouzapw/OmniRoute/issues/12206)). Regression guard: `tests/unit/translator-openai-responses-system-content-parts.test.ts`. ([#12207](https://github.com/diegosouzapw/OmniRoute/pull/12207)) — thanks @Gorillaz322
- **fix(free-tier):** `/api/free-tier/summary` no longer computes its totals from a Radar feed built before the catalog the running release ships. When the cached feed is older — or carries no build date at all — the route answers from the shipped catalog, resolved through the operator's local model state so disabled and tombstoned models stay out of the numbers ([#12215](https://github.com/diegosouzapw/OmniRoute/pull/12215)). — thanks @maxmad64bis
- **fix(oauth):** Keep a Claude personal workspace and a Team organization as separate connections — they share the same email and `accountUUID`, so the email-only OAuth dedup let the second login overwrite the first account's tokens; `organizationUUID` now disambiguates them, the way `workspaceId` does for Codex ([#12222](https://github.com/diegosouzapw/OmniRoute/pull/12222)) — thanks @davidebaraldo
- **fix(resilience):** a 402 on a single paid model of a passthrough/gateway provider (e.g. `kilo-gateway`, `ollama-cloud`) no longer terminalizes the whole connection with a never-auto-recovered `credits_exhausted` status — only the paid model is locked out, so free models on the same key keep serving. 402 variant of [#3027](https://github.com/diegosouzapw/OmniRoute/issues/3027). Single-credential providers are unaffected — a 402 there is still treated as the key being genuinely out of credit ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239) / [#10616](https://github.com/diegosouzapw/OmniRoute/issues/10616)) ([#12242](https://github.com/diegosouzapw/OmniRoute/issues/12242)) — thanks @brick30llc-ctrl ([#12266](https://github.com/diegosouzapw/OmniRoute/pull/12266)) — thanks @geekyNads
- **fix(sse):** trust `finish_reason: "length"`/`"max_tokens"` over the reasoning-consumed-token ratio in response quality validation, so a reasoning model truncated below the old 90% threshold correctly fails and retries instead of returning empty content as a silent "success" ([#12262](https://github.com/diegosouzapw/OmniRoute/pull/12262)) — thanks @brick30llc-ctrl
- **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(combo):** Expose the two Auto-Combo scoring factors nobody could set — the combo validation schema and the dashboard weight sliders both declared 13 of the scorer's 15 factors, so `connectionDensity` (spreads load across a provider's connections) and `quality` were dropped on save and offered nowhere. The sliders also shipped their own default table that differed from the engine's on every non-zero factor and summed to 1.05, so the percentages shown next to them added up to 105%. Both lists now match `DEFAULT_WEIGHTS`, and a test keeps them there. Note that a combo whose stored `weights` omitted the two keys was effectively running with them at zero and the other thirteen renormalized upward; it now runs with the engine's intended distribution, so its routing does shift ([#12314](https://github.com/diegosouzapw/OmniRoute/pull/12314)) — thanks @maxmad64bis
- **fix(docs):** The free-tier reference no longer says its numbers come "confidence tagged per row" — no catalog entry carries a confidence tag and the API serves none, so every figure on that page is an estimate of the same, unstated quality. The page now states what an entry does vouch for: an independently documented hard stop (set by hand with the source in a comment, never defaulted to `true`) and a prompt-training disclosure, both with live counts the `check:docs-counts` gate keeps honest ([#12318](https://github.com/diegosouzapw/OmniRoute/pull/12318)) — thanks @maxmad64bis
- **fix(usage):** `adobe-firefly` and `firefly` have had a working usage fetcher since Adobe Firefly landed, but neither was ever added to the registration list, so the provider-plugin manifest, `genericQuotaFetcher` and the free-access quota cache all reported them as having no usage support — while `USAGE_SUPPORTED_PROVIDERS` said the opposite. Both are now declared, which also means their credit balance is fetched like any other declared provider's: `registerGenericQuotaFetchers` now registers a generic quota fetcher for them, and `resolveFreeAccessState` no longer returns early. A test holds the registration list to the dispatcher's switch in both directions, which is what the module's own docstring already asked for in prose ([#12321](https://github.com/diegosouzapw/OmniRoute/pull/12321)) — thanks @maxmad64bis
- **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)) ([#12425](https://github.com/diegosouzapw/OmniRoute/pull/12425)) — thanks @KrzysiekSko
- **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 ([#12359](https://github.com/diegosouzapw/OmniRoute/pull/12359)) — thanks @pacocartones
- **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)) ([#12360](https://github.com/diegosouzapw/OmniRoute/pull/12360)) — thanks @pacocartones
- **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)) ([#12361](https://github.com/diegosouzapw/OmniRoute/pull/12361)) — thanks @pacocartones
- **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)) ([#12362](https://github.com/diegosouzapw/OmniRoute/pull/12362)) — thanks @pacocartones
- **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 ([#12365](https://github.com/diegosouzapw/OmniRoute/pull/12365)) — thanks @pacocartones
- **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)) ([#12369](https://github.com/diegosouzapw/OmniRoute/pull/12369)) — thanks @gonisulaimann
- **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)) ([#12371](https://github.com/diegosouzapw/OmniRoute/pull/12371)) — thanks @gonisulaimann
- **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)) ([#12373](https://github.com/diegosouzapw/OmniRoute/pull/12373)) — thanks @pacocartones
- **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 ([#12374](https://github.com/diegosouzapw/OmniRoute/pull/12374)) — thanks @pacocartones
- **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 ([#12375](https://github.com/diegosouzapw/OmniRoute/pull/12375)) — thanks @pacocartones
- **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)) ([#12376](https://github.com/diegosouzapw/OmniRoute/pull/12376)) — thanks @pacocartones
- **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)) ([#12379](https://github.com/diegosouzapw/OmniRoute/pull/12379)) — thanks @pacocartones
- **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) ([#12380](https://github.com/diegosouzapw/OmniRoute/pull/12380)) — thanks @pacocartones
- **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 ([#12381](https://github.com/diegosouzapw/OmniRoute/pull/12381)) — thanks @pacocartones
- **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 ([#12386](https://github.com/diegosouzapw/OmniRoute/pull/12386)) — thanks @pacocartones
- **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 ([#12387](https://github.com/diegosouzapw/OmniRoute/pull/12387)) — thanks @pacocartones
- **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) ([#12389](https://github.com/diegosouzapw/OmniRoute/pull/12389)) — thanks @pacocartones
- **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)) ([#12632](https://github.com/diegosouzapw/OmniRoute/pull/12632)) — thanks @HouMinXi
- **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)) ([#12595](https://github.com/diegosouzapw/OmniRoute/pull/12595))
- 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. ([#12607](https://github.com/diegosouzapw/OmniRoute/pull/12607)) — thanks @RaviTharuma
- **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)). ([#12628](https://github.com/diegosouzapw/OmniRoute/pull/12628)) — thanks @RaviTharuma
- **fix(cursor):** a non-array `tool_calls` on an assistant message no longer crashes the cursor request translator with a `TypeError`; both loops now require an array ([#12689](https://github.com/diegosouzapw/OmniRoute/issues/12689)) ([#12691](https://github.com/diegosouzapw/OmniRoute/pull/12691)) — thanks @soroush5
- **fix(chat):** Chat Completions no longer return empty `content` after a server-owned memory or skills tool; the first provider send and account/model recovery now share one pipeline so a follow-up round-trip can fill the reply ([#12696](https://github.com/diegosouzapw/OmniRoute/issues/12696)) — thanks @HouMinXi ([#12867](https://github.com/diegosouzapw/OmniRoute/pull/12867))
- **fix(dashboard):** batch-deleting provider keys no longer toasts failure after a successful delete when the confirm button's click event is forwarded as `onAfter` ([#12711](https://github.com/diegosouzapw/OmniRoute/pull/12711)) — thanks @HouMinXi
- **fix(glm):** drop the extra 16th argument to `createSSETransformStreamWithLogger` that TypeScript rejected (TS2554) and that never reached the TransformStream ([#12711](https://github.com/diegosouzapw/OmniRoute/pull/12711)) ([#12770](https://github.com/diegosouzapw/OmniRoute/pull/12770)) — thanks @HouMinXi
- **fix(dashboard):** pass `nodeMap` into Runtime `QuotaGroup` so a quota monitor in error/exhausted/alerting no longer throws `ReferenceError: nodeMap is not defined`. ([#12868](https://github.com/diegosouzapw/OmniRoute/pull/12868)) — thanks @HouMinXi
- Restricted API keys whose `allowedModels` lists a combo name no longer skip every combo member at pre-dispatch (`ALL_TARGETS_SKIPPED`). Inner-target filtering still applies when the allow-list is a provider prefix or `disableNonPublicModels` is on ([#12899](https://github.com/diegosouzapw/OmniRoute/pull/12899)). — thanks @HouMinXi
- **fix(combos):** Effort-suffixed combo members inherit the base model's `model_context_overrides` row so priority order is not inverted on large requests ([#12926](https://github.com/diegosouzapw/OmniRoute/pull/12926)) — thanks @HouMinXi
- **fix(catalog):** Picker-added `customModels` enter the dispatch-time live catalog so combo members and bare inference no longer 400 ([#12934](https://github.com/diegosouzapw/OmniRoute/pull/12934)) — thanks @HouMinXi
- **fix(docker):** re-derive the Docker build's worker-pool memory budget from the MEASURED ~4.5 GB per-process RSS (the issue owner's own VPS dmesg OOM-killer reproduction) instead of the stale 2560 MB/worker inference #11419 shipped, and lower `OMNIROUTE_BUILD_WORKERS` 3 → 2 so 1 parent + 1 page-data worker (2 processes × 4.5 GB = 9 GB) fits the 12.288 GB (75%) budget on a 16 GB GitHub Actions runner — the previous default (1 parent + 2 workers = 13.5 GB) still overcommitted the runner and kept "Publish to Docker Hub" failing with `cannot allocate memory` after #11419 merged (#7518). ([#11663](https://github.com/diegosouzapw/OmniRoute/pull/11663))
- **fix(electron):** the packaged Windows desktop build now passes the #7592 cold-restart smoke end to end. Five defects were found and fixed on the way: (1) optional-pack staging failed on any machine whose `tar` is GNU tar — it reads the drive letter in an absolute `-f C:\...` archive path as a remote rsh host (`Cannot connect to C:`), so staging now passes a bare filename with `cwd` at the tarball directory and surfaces tar stderr on failure; (2) the Electron `files` allowlist was missing `lib/loginHeaderCapture.js`, so the packaged main process crashed at startup with `Cannot find module './lib/loginHeaderCapture'`; (3) electron-builder ≥26 injects an `!**/node_modules/**` ignore into every extraResources pattern list that no positive filter can override, which silently dropped the staged runtime `node_modules` (including the better-sqlite3 N-API prebuild) from `resources/app` — a fresh v3.8.50 desktop build would have shipped with no native SQLite driver and reproduced the #7592 sql.js fallback on every machine; an `afterPack` hook now restores the staged `node_modules` after packing; (4) the packaged-app smoke harness redirected `USERPROFILE` into its temp DATA_DIR, but Electron resolves the Roaming profile from `%USERPROFILE%\AppData\Roaming\<name>` (USERPROFILE takes precedence over the APPDATA env var) and the path service throws instead of creating it, so `requestSingleInstanceLock()` returned false and the app exited(0) silently before `app.whenReady()` — the harness now pre-creates the derived tree, and `ensureSmokeEnvDirs` is exported and unit-tested; (5) the #7592 driver assertion parsed a `[DB] Driver: ...` line that the server's primary DB path never printed (only the unused `openDatabaseAsync()` did) — `getDbInstance()` now emits the same line on open so the guard can actually assert the native driver. Also: the smoke env-allowlist unit test hardcoded POSIX paths and could only pass on Linux/macOS; expectations are now host-agnostic, with new regression tests for the USERPROFILE-derived tree and `tarPack` under absolute Windows-style paths ([#11443](https://github.com/diegosouzapw/OmniRoute/pull/11443)) — thanks @jonlwheat2-gif
- fix(sse): stop the auto-combo candidates inspector from silently dropping model-locked/cooled-down rows (#9133) ([#11994](https://github.com/diegosouzapw/OmniRoute/pull/11994)) — thanks @hartmark
- **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 ([#12465](https://github.com/diegosouzapw/OmniRoute/pull/12465))
- **fix(video-bridge):** preserve chronological video and transcript fusion, source-frame timestamps, transcript provenance, full contact-sheet intervals, and pre-abort callback isolation ([#11681](https://github.com/diegosouzapw/OmniRoute/pull/11681))
- **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. ([#12454](https://github.com/diegosouzapw/OmniRoute/pull/12454))
- Absorb `Error [AbortError]: request_signal_aborted` and DOMException AbortError shapes in the process-level client-abort crash guard so routine client disconnects no longer kill the server (exit code 7). ([#12165](https://github.com/diegosouzapw/OmniRoute/pull/12165)) — thanks @alvinveroy
- **fix(sse):** Treat Adapta Web `type:error` SSE events as sanitized non-stream failures instead of empty HTTP 200 completions. ([#12459](https://github.com/diegosouzapw/OmniRoute/pull/12459))
- **fix(autoCombo):** layer 3 (`models_dev_tier`) no longer scores retired models as premium, merge cross-provider capability rows deterministically, and inherit tier scores through variant ids ([#11508](https://github.com/diegosouzapw/OmniRoute/issues/11508)). `getModelsDevTierFitness` now vetoes ids present in `config/quality/model-lifecycle.json` with status `retired` before any other signal; `loadModelCapabilities()` aggregates per `model_id` (any-non-null-true for booleans, max for `limit_context`) instead of last-write-wins over SQLite's undefined row order; a capabilities miss retries through `resolveScoresAs()`'s base id and reports `models_dev_tier:inherited`, matching layers 12 (#11492). Adds `getModelsDevTierFitnessWithSource()` and an `invalidateCapabilitiesCache()` test/ops hook. ([#11598](https://github.com/diegosouzapw/OmniRoute/pull/11598)) — thanks @Meet6338-X
- fix(sse): accept Claude `content:[]` + `stop_reason: max_tokens`/`tool_use` in `detectMalformedNonStream` (match `isEmptyContentResponse`) so Claude Code `/model` probes with `max_tokens: 1` no longer become a false 502 ([#11568](https://github.com/diegosouzapw/OmniRoute/pull/11568)) — thanks @Seramicx
- **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. ([#12469](https://github.com/diegosouzapw/OmniRoute/pull/12469))
- **fix(pwa):** do not intercept dashboard navigations so Chrome can fall back from a stale HTTP/3 Alt-Svc advertisement (UDP :20128 is unpublished; F5 on a long-lived tab hung until a new tab opened a fresh TCP connection). ([#12767](https://github.com/diegosouzapw/OmniRoute/pull/12767)) — thanks @HouMinXi
- **fix(executors):** handle DuckDuckGo ERR_BN_LIMIT (418) without retrying — when the upstream returns `418 ERR_BN_LIMIT` (rate-limit/ban), the executor now returns the error immediately instead of burning another VQD acquisition that would only count against the IP limit. The retry logic for `418 ERR_CHALLENGE` (unsolved challenge) remains unchanged. ([#11598](https://github.com/diegosouzapw/OmniRoute/pull/11598)) ([#12110](https://github.com/diegosouzapw/OmniRoute/pull/12110)) — thanks @oyi77 / @Meet6338-X
- Fixed DuckDuckGo streaming responses losing JSON lines and UTF-8 characters split across network chunks. ([#11528](https://github.com/diegosouzapw/OmniRoute/pull/11528)) — thanks @pacocartones
- Harden generated Cloudflare, Vercel, and Deno relays by stripping hop-by-hop, framing, and proxy authentication headers before forwarding requests upstream. ([#11533](https://github.com/diegosouzapw/OmniRoute/pull/11533)) — thanks @pacocartones
- **feat(grok-cli):** Provider Limits shows grok-cli banked reset credits from `GetRemainingResets` (including a real zero; a failed RPC omits the row) and the existing View credits button now calls `ConsumerUiSvc/RedeemReset` for grok-cli. Live tokens use nested fields 10/20/30 (id + Timestamp), not compact 1/2/3. ([#12805](https://github.com/diegosouzapw/OmniRoute/pull/12805)) — thanks @HouMinXi
- **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 ([#11390](https://github.com/diegosouzapw/OmniRoute/pull/11390)) — thanks @rqzbeh
- 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. ([#12435](https://github.com/diegosouzapw/OmniRoute/pull/12435))
- **fix(dashboard):** Moonshot/Kimi Open Platform voucher and cash leftover percentages follow the bucket balance, so an empty wallet no longer paints those rows as 100% while Available is 0% ([#12733](https://github.com/diegosouzapw/OmniRoute/pull/12733)) — thanks @HouMinXi
- **fix(combo):** return non-retryable HTTP 400 when all candidates for a pinned native Codex turn are unavailable due to model-scoped lockout, terminating the turn cleanly while preserving turn continuity and enabling standard Combo routing on subsequent turns ([#12240](https://github.com/diegosouzapw/OmniRoute/pull/12240)) — thanks @mdigitalbh81
- Fixed auto-router strategies preserving the selected connection through ranking and dispatch when multiple connections share the same provider and model. ([#11530](https://github.com/diegosouzapw/OmniRoute/pull/11530)) — thanks @pacocartones
- Preserve Chat Completions JSON-object response formats when translating requests to the Responses API. ([#11529](https://github.com/diegosouzapw/OmniRoute/pull/11529)) — thanks @pacocartones
- Fix idempotency fingerprints so Responses API requests include `input` and other semantic generation fields using stable serialization, while excluding credentials and request metadata. ([#11532](https://github.com/diegosouzapw/OmniRoute/pull/11532)) — thanks @pacocartones
- **fix(api):** Generated API CLI commands now enforce required OpenAPI request bodies; Combo test commands forward the required `comboName` body, while API keys created by older writers after migration 149 preserve legacy allow-all Combo access without widening explicit empty allowlists — thanks @marcelokarval ([#12070](https://github.com/diegosouzapw/OmniRoute/pull/12070))
- **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. ([#12458](https://github.com/diegosouzapw/OmniRoute/pull/12458))
- HuggingChat now turns HTTP 200 JSONL generation failures into a sanitized 502 before content, or a fixed public stream failure after partial output, so fallback and request persistence no longer record a false successful stop. ([#12456](https://github.com/diegosouzapw/OmniRoute/pull/12456))
- **fix(providers):** keep 1min.ai HTTP 200 stream errors out of assistant content, preserve partial output, and expose sanitized terminal errors so pre-content failures can fall back. ([#12466](https://github.com/diegosouzapw/OmniRoute/pull/12466))
- **fix(sessions):** preserve legacy Sessions data when the additive exclusive-session projection is unavailable, returning an empty projection and warning only once per contiguous outage instead of failing the endpoint; the Sessions badge intentionally reflects the merged legacy and exclusive row count introduced by [#11389](https://github.com/diegosouzapw/OmniRoute/pull/11389) ([#11469](https://github.com/diegosouzapw/OmniRoute/pull/11469)) — thanks @KaspaPulse
- Render Nimble Search with the generic provider icon and serve Opper's proven logo locally. ([#12415](https://github.com/diegosouzapw/OmniRoute/pull/12415))
- **fix(quality):** `config/quality/provider-assets-provenance.jsonl` was stale against the physical `public/providers/` tree (still listing 225 assets and 5 duplicate-content groups after several provider-retirement PRs removed 85 icon files), which would have made `check:provider-asset-provenance` fail on the next full run. Reconciled the manifest to the real 142-file snapshot (142 asset records, 1 duplicate-content group, 2 newly-registered files marked `unresolved` pending provenance research), repinned `auditedCommit`, and updated the regression test's hardcoded counts to match. ([#11876](https://github.com/diegosouzapw/OmniRoute/pull/11876))
- fix(ui): wrap quota card footer action buttons to prevent clipping Refresh button on narrow cards — when extra action buttons such as `View credits` are present on Codex cards, allow the footer row to flex-wrap with `shrink-0` buttons so `Refresh now` remains visible and clickable across all responsive card widths ([#11464](https://github.com/diegosouzapw/OmniRoute/pull/11464)) — thanks @adevwithpurpose
- Keep Antigravity Gemini usable when the same connection's Claude weekly quota is empty; generic quota cache stays per-connection for every other provider. ([#12637](https://github.com/diegosouzapw/OmniRoute/pull/12637)) — thanks @HouMinXi
- Fixed Tailscale tunnel setup to reject ports outside the valid 1-65535 range and safely install Windows MSI packages from paths containing apostrophes. ([#11534](https://github.com/diegosouzapw/OmniRoute/pull/11534)) — thanks @pacocartones
- Classify Cloudflared and Tailscale process-management routes as local-only and spawn-capable while preserving remote access to read-only tunnel status endpoints. ([#11531](https://github.com/diegosouzapw/OmniRoute/pull/11531)) — thanks @pacocartones
- Electron release workflow: the `publish-npm` job now grants `actions: read` to the reusable `npm-publish.yml` it calls (its `publish` job requests it), which is what made GitHub refuse the whole v3.8.50 run at startup and ship the release with zero desktop assets; a `workflow_dispatch` now builds the requested tag instead of the dispatching branch and can skip the npm leg (`publish_npm=false`) when only re-attaching assets ([#11974](https://github.com/diegosouzapw/OmniRoute/pull/11974))
- **fix(dashboard):** The Combos page usage guide now reads its dismissal through `useSyncExternalStore` instead of correcting SSR state inside an effect, removing an extra commit of the page tree on every load (and the `react-hooks/set-state-in-effect` error it raised). ([#12671](https://github.com/diegosouzapw/OmniRoute/pull/12671))
- Electron release: `electron/package-lock.json` regained the optional `electron-builder-squirrel-windows` subtree (13 entries) that `npm ci` had been refusing as out of sync, `electron-release.yml` gained a `build_ref` dispatch input and stops regenerating release notes on a re-attach dispatch, and the npm publish workflow attaches the SBOM to the GitHub Release on dispatch publishes too — so the v3.8.51 tag ships every desktop asset and the SBOM like v3.8.49 did ([#12022](https://github.com/diegosouzapw/OmniRoute/pull/12022))
- Drained the reds every PR against `release/v3.8.51` was born with: documented the three service-worker build-id variables, registered the six retirement/tunnel tests with the mutation gate, approved `eslint-plugin-react-hooks` in the dependency allowlist, added the six `combo.sort.*` strings to `vi` and `pt-BR`, pointed the ChatGPT Web doc at the real migration-168 test, worded the g4f hint around the member key, and realigned four tests to the retired-provider catalog and the legacy-schema fixtures the retirement migrations touch. ([#11940](https://github.com/diegosouzapw/OmniRoute/pull/11940))
- **fix(providers):** Claude, Grok, LMArena, Notion, and Perplexity web-cookie transports now use pooled `wreq-js` 3.2 instead of the native sidecar, with all nine supported bindings pinned and audited, and the applicable platform binding plus native-license evidence included in each release artifact ([#12429](https://github.com/diegosouzapw/OmniRoute/pull/12429), supersedes [#11753](https://github.com/diegosouzapw/OmniRoute/pull/11753)).
- **fix(providers):** Zed Hosted streaming failures now trigger fallback before content and end partial streams with a sanitized structured error instead of fake assistant text and a normal-success stop. ([#12455](https://github.com/diegosouzapw/OmniRoute/pull/12455))
- **fix(test):** capture hook result after render (direct commit `b85d0abb0b`)
- **fix(sync):** repair the two regressions the v3.8.50 sync-back left on release/v3.8.51 (direct commit `5ade9e0851`)
- **revert(models):** drop the memo reset hook ported from #10055 (direct commit `282de841ae`)
- **fix(typecheck):** guard the optional connectionId before the reactive model sync (direct commit `f2825bad75`)
- **fix(quota-share):** release the winner's reserved in-flight slot (#11371) ([#11408](https://github.com/diegosouzapw/OmniRoute/pull/11408)) — thanks @oyi77
- **fix(providers):** stop silently dropping reasoning effort on opencode families (#10788) ([#11409](https://github.com/diegosouzapw/OmniRoute/pull/11409)) — thanks @oyi77
- **fix(combo):** enable genuine quota-aware routing for generic providers (antigravity, claude, etc.) ([#11411](https://github.com/diegosouzapw/OmniRoute/pull/11411)) — thanks @HouMinXi
- **fix(oauth):** allow expired token retry before deactivation and activate expired retry loop (#P0) ([#11414](https://github.com/diegosouzapw/OmniRoute/pull/11414)) — thanks @HouMinXi
- **perf(executors):** lazy-load the executor registry — defer class imports + construction to first use (#11220) ([#11421](https://github.com/diegosouzapw/OmniRoute/pull/11421)) — thanks @oyi77
- **fix:** make release/v3.8.50 CI gates green (run 32786966560 root causes) ([#11450](https://github.com/diegosouzapw/OmniRoute/pull/11450)) — thanks @jonlwheat2-gif
- **fix(sse):** exact-domain cookie match and origin-equality URL assertions ([#11452](https://github.com/diegosouzapw/OmniRoute/pull/11452)) — thanks @hartmark
- **fix(providers):** send Responses API payload during OpenAI-compatible validation ([#11454](https://github.com/diegosouzapw/OmniRoute/pull/11454)) — thanks @tuandinh0801
- **fix(command-code):** fallback to /alpha/generate for Go plan without Provider API access ([#11455](https://github.com/diegosouzapw/OmniRoute/pull/11455)) — thanks @Chewji9875
- **fix(responses-continuation):** fail closed on a log-truncated stored input/output array ([#11473](https://github.com/diegosouzapw/OmniRoute/pull/11473)) — thanks @hartmark
- **fix(cli):** write jcode settings as [providers.omniroute] in config.toml ([#11484](https://github.com/diegosouzapw/OmniRoute/pull/11484)) — thanks @hongnoul
- **fix(logging):** raise chat-log array truncation cap to 1000 and unify duplicate implementations ([#11499](https://github.com/diegosouzapw/OmniRoute/pull/11499)) — thanks @hartmark
- **fix:** restore live websocket URL helpers ([#11509](https://github.com/diegosouzapw/OmniRoute/pull/11509)) — thanks @PauloFH
- **perf(core):** eliminate async waterfalls across home, cache, and db operations (#11396) ([#11512](https://github.com/diegosouzapw/OmniRoute/pull/11512)) — thanks @HouMinXi
- **fix(kiro):** add runtime.us-east-1.kiro.dev as first-attempt endpoint ([#11517](https://github.com/diegosouzapw/OmniRoute/pull/11517)) — thanks @rifqiawl
- **fix(providers):** remove duplicate 128-tool truncation in OpencodeExecutor (#11444) ([#11518](https://github.com/diegosouzapw/OmniRoute/pull/11518)) — thanks @HouMinXi
- **fix(providers):** poll onboardUser LRO before classifying BYOP ([#11519](https://github.com/diegosouzapw/OmniRoute/pull/11519)) — thanks @rifqiawl
- **fix(translator):** preserve exact tool name casing for Claude tool_use payloads (#11487) ([#11520](https://github.com/diegosouzapw/OmniRoute/pull/11520)) — thanks @HouMinXi
- **fix(kimi-web):** align health probe with executor domain ([#11521](https://github.com/diegosouzapw/OmniRoute/pull/11521)) — thanks @pacocartones
- **fix(cli):** preserve existing admin password during setup ([#11522](https://github.com/diegosouzapw/OmniRoute/pull/11522)) — thanks @pacocartones
- **fix(mitm):** keep Windows DNS elevation off the POSIX sudo path (#11430) ([#11542](https://github.com/diegosouzapw/OmniRoute/pull/11542)) — thanks @official-burak
- **fix(sse):** scale chat admission by ingest byte budget instead of a fixed request count ([#11548](https://github.com/diegosouzapw/OmniRoute/pull/11548)) — thanks @TheDemonTuan
- **fix:** accept Gemini Web cookie exports ([#11554](https://github.com/diegosouzapw/OmniRoute/pull/11554)) — thanks @rafacpti23
- **fix(i18n):** complete Brazilian Portuguese translations ([#11558](https://github.com/diegosouzapw/OmniRoute/pull/11558)) — thanks @rafacpti23
- **fix(sse):** explicit types for openai-responses pureHelpers — clears last failing core typecheck gate ([#11567](https://github.com/diegosouzapw/OmniRoute/pull/11567)) — thanks @jonlwheat2-gif
- **fix(opencode):** hydrate Proxy Pool references for no-auth connections ([#11584](https://github.com/diegosouzapw/OmniRoute/pull/11584)) — thanks @AStupidBear
- **fix(ci):** resolve inherited release/v3.8.51 base-red CI failures ([#11608](https://github.com/diegosouzapw/OmniRoute/pull/11608)) — thanks @jonlwheat2-gif
- **fix(db):** fallback load STORAGE_ENCRYPTION_KEY from env files ([#11614](https://github.com/diegosouzapw/OmniRoute/pull/11614)) — thanks @benzntech
- **fix(providers):** mark Brave Search as no longer having a free tier ([#11621](https://github.com/diegosouzapw/OmniRoute/pull/11621)) — thanks @turbolego
- **fix:** pass tool result content through as string in OpenAI/Claude -> Gemini translation ([#11624](https://github.com/diegosouzapw/OmniRoute/pull/11624)) — thanks @Hsia97
- **fix(sse):** scope credential health to exact key ([#11628](https://github.com/diegosouzapw/OmniRoute/pull/11628)) — thanks @KaspaPulse
- **fix(install):** guard prepare script against missing husky ([#11634](https://github.com/diegosouzapw/OmniRoute/pull/11634)) — thanks @Prajeeth-12
- **fix(security):** redact NVIDIA_API_KEY at the log sink in the ad-hoc diag script ([#11639](https://github.com/diegosouzapw/OmniRoute/pull/11639))
- **fix(types):** clear the two type regressions red on release/v3.8.51 ([#11640](https://github.com/diegosouzapw/OmniRoute/pull/11640)) — thanks @jonlwheat2-gif
- **fix(build):** resolve workspace: protocol dependencies to concrete semver versions during prepublish (#11540) ([#11641](https://github.com/diegosouzapw/OmniRoute/pull/11641)) — thanks @HouMinXi
- **fix(search):** properly resolve configured search connection in /v1/responses pipeline (#11524) ([#11642](https://github.com/diegosouzapw/OmniRoute/pull/11642)) — thanks @HouMinXi
- **fix(authz):** trust cf-connecting-ip for via-proxy detection when peer is Cloudflare edge (#11514) ([#11643](https://github.com/diegosouzapw/OmniRoute/pull/11643)) — thanks @HouMinXi
- **fix(security):** compare the OIDC state cookie in constant time ([#11646](https://github.com/diegosouzapw/OmniRoute/pull/11646))
- **fix(security):** namespace the dedup hash by the calling API key ([#11649](https://github.com/diegosouzapw/OmniRoute/pull/11649))
- **fix(models):** gate the Codex-native catalog loop on prefix mode (#11632) ([#11662](https://github.com/diegosouzapw/OmniRoute/pull/11662)) — thanks @xiaoyaner0201
- **fix(dashboard):** project Codex routing quota in account pools ([#11666](https://github.com/diegosouzapw/OmniRoute/pull/11666)) — thanks @KaspaPulse
- **fix(ddgw):** normaliza system no upstream da release v3.8.51 ([#11692](https://github.com/diegosouzapw/OmniRoute/pull/11692)) — thanks @RhianB14
- **fix(memory):** honor category filter in GET /api/memory ([#11699](https://github.com/diegosouzapw/OmniRoute/pull/11699)) — thanks @Sabeekhann
- **fix(ci):** detect stale workspace lockfile entries ([#11714](https://github.com/diegosouzapw/OmniRoute/pull/11714)) — thanks @pacocartones
- **fix(ci):** stamp dist/BUILD_SHA before the npm publish provenance gate ([#11721](https://github.com/diegosouzapw/OmniRoute/pull/11721))
- **fix(docker):** decouple the best-effort Bun image from the release manifest ([#11724](https://github.com/diegosouzapw/OmniRoute/pull/11724))
- **perf(compression):** avoid cloning memo misses twice ([#11727](https://github.com/diegosouzapw/OmniRoute/pull/11727)) — thanks @RaviTharuma
- **fix(docker):** let the bun digest artifact be absent, not fatal ([#11740](https://github.com/diegosouzapw/OmniRoute/pull/11740))
- **fix(shared):** block cloud-metadata hosts under default remote-image guard ([#11755](https://github.com/diegosouzapw/OmniRoute/pull/11755)) — thanks @santosraju99-hub
- **fix(api):** preserve caller-provided X-Correlation-Id on chat completions ([#11760](https://github.com/diegosouzapw/OmniRoute/pull/11760)) — thanks @aniruddhaadak80
- **fix(resilience):** lock permanently retired models instead of short backoff (Gemini ban prevention) ([#11762](https://github.com/diegosouzapw/OmniRoute/pull/11762)) — thanks @turbolego
- **fix:** mark Vercel AI Gateway as passthroughModels ([#11771](https://github.com/diegosouzapw/OmniRoute/pull/11771)) — thanks @Rahulsharma0810 / @Rahulsharma0810
- **fix(resilience):** stop hammering permanently-moved endpoints and billing-suspended accounts ([#11774](https://github.com/diegosouzapw/OmniRoute/pull/11774)) — thanks @turbolego
- **fix(lease):** remove global static reservation and gate routing on live active lease occupancy ([#11775](https://github.com/diegosouzapw/OmniRoute/pull/11775)) — thanks @TheDemonTuan
- **fix(ci):** size the install-upgrade gate to a measured run, and log the pack cost ([#11776](https://github.com/diegosouzapw/OmniRoute/pull/11776))
- **fix(pwa):** stop serving the stale shell after deploys ([#11779](https://github.com/diegosouzapw/OmniRoute/pull/11779)) — thanks @HouMinXi
- **fix(combo):** lock GitHub models rejected as "not supported" for future requests ([#11781](https://github.com/diegosouzapw/OmniRoute/pull/11781)) — thanks @turbolego
- **fix(cli):** probe both IPv4 and IPv6 loopback for server readiness (#11766) ([#11794](https://github.com/diegosouzapw/OmniRoute/pull/11794)) — thanks @vermasomesh835
- **fix(sse):** preserve CLIProxyAPI wrappers per connection ([#11797](https://github.com/diegosouzapw/OmniRoute/pull/11797)) — thanks @RaviTharuma
- **fix(providers):** honor model auto-fetch opt-in on create ([#11798](https://github.com/diegosouzapw/OmniRoute/pull/11798)) — thanks @RaviTharuma
- **fix(dashboard):** make model auto-fetch order independent ([#11805](https://github.com/diegosouzapw/OmniRoute/pull/11805)) — thanks @RaviTharuma
- **fix(docker):** make GHA cache exports best effort ([#11806](https://github.com/diegosouzapw/OmniRoute/pull/11806)) — thanks @RaviTharuma
- **fix(kiro):** do not permanently ban on 'User is not authorized to make this call' ([#11809](https://github.com/diegosouzapw/OmniRoute/pull/11809)) — thanks @Deftera186
- **fix(sse):** keep cache-write tokens in OpenAI-shaped usage ([#11814](https://github.com/diegosouzapw/OmniRoute/pull/11814)) — thanks @TheDemonTuan
- **fix(docs):** resolve relative markdown and wiki links across Fumadocs and GitHub wiki ([#11834](https://github.com/diegosouzapw/OmniRoute/pull/11834)) — thanks @AndrianBalanescu
- **fix(chat):** preserve unstripped model string for passthrough provider routing ([#11840](https://github.com/diegosouzapw/OmniRoute/pull/11840)) — thanks @raheemuddin786
- **fix(mcp):** avoid async initialization inside sync esm wrappers (#11569) ([#11849](https://github.com/diegosouzapw/OmniRoute/pull/11849)) — thanks @HouMinXi
- **fix(sse):** preserve coherent markdown token boundaries across stream translation chunks (#11606) ([#11851](https://github.com/diegosouzapw/OmniRoute/pull/11851)) — thanks @HouMinXi
- **fix(ci):** run the install-upgrade gate on disk, not on the /tmp tmpfs ([#11855](https://github.com/diegosouzapw/OmniRoute/pull/11855))
- **fix(skills):** expand shorthand property types in injected tool schemas ([#11857](https://github.com/diegosouzapw/OmniRoute/pull/11857)) — thanks @alvinveroy
- **fix(ci):** publish npm from a hosted runner so provenance is accepted ([#11877](https://github.com/diegosouzapw/OmniRoute/pull/11877))
- **fix(lint):** pin react hooks plugin to 7.0.1 ([#11886](https://github.com/diegosouzapw/OmniRoute/pull/11886))
- **fix(ci):** surface and clear release-green ESLint failure ([#11890](https://github.com/diegosouzapw/OmniRoute/pull/11890)) — thanks @MumuTW
- **fix(guardrails):** prevent duplicate prompt-injection-guard log output ([#11936](https://github.com/diegosouzapw/OmniRoute/pull/11936)) — thanks @patrykkopycinski
- **fix(auth):** downgrade expected transient states from warn to debug ([#11937](https://github.com/diegosouzapw/OmniRoute/pull/11937)) — thanks @patrykkopycinski
- **fix(codex):** fail over image generation for imported free plans ([#11948](https://github.com/diegosouzapw/OmniRoute/pull/11948)) — thanks @Bl0ck154
- **fix(codex):** normalize non-stream responses ([#11951](https://github.com/diegosouzapw/OmniRoute/pull/11951)) — thanks @Bl0ck154
- **fix(images):** forward Antigravity image size ([#11952](https://github.com/diegosouzapw/OmniRoute/pull/11952)) — thanks @Bl0ck154
- **fix(db):** invalidate connection cache after upsert ([#11953](https://github.com/diegosouzapw/OmniRoute/pull/11953)) — thanks @Bl0ck154
- **fix(codex):** restore imported account state ([#11954](https://github.com/diegosouzapw/OmniRoute/pull/11954)) — thanks @Bl0ck154
- **fix(sse):** give extended-thinking targets the reasoning readiness budget ([#11959](https://github.com/diegosouzapw/OmniRoute/pull/11959)) — thanks @abhisheksharma2411
- **fix(dashboard):** use opaque background and readable text color on cost chart tooltips ([#11960](https://github.com/diegosouzapw/OmniRoute/pull/11960)) — thanks @ZaimMarzuki
- **fix(antigravity):** send complete loadCodeAssist metadata (ideType/platform/pluginType as numeric enums) ([#11969](https://github.com/diegosouzapw/OmniRoute/pull/11969)) — thanks @rifqiawl
- **fix(core):** resolve DB init race condition and reasoning translation ([#12003](https://github.com/diegosouzapw/OmniRoute/pull/12003)) — thanks @raheemuddin786
- **fix(admission):** exclude reclaimable page cache from the cgroup pressure ratio ([#12017](https://github.com/diegosouzapw/OmniRoute/pull/12017)) — thanks @HouMinXi
- **fix(build):** prune non-production trees in NFT trace excludes and tsconfig ([#12028](https://github.com/diegosouzapw/OmniRoute/pull/12028)) — thanks @Chewji9875
- **fix(migrations):** add renamed migration compatibility for 056/073/077/101 ([#12036](https://github.com/diegosouzapw/OmniRoute/pull/12036)) — thanks @oyi77
- **fix(sse):** default crash-guard logger to console.warn, not console ([#12042](https://github.com/diegosouzapw/OmniRoute/pull/12042)) — thanks @HouMinXi
- **fix(catalog):** derive vision/modalities for built-in auto combos from effective target pool ([#12046](https://github.com/diegosouzapw/OmniRoute/pull/12046)) — thanks @Prajeeth-12
- **fix(release):** the packaged-app smoke verifies the database opened, not a driver line the primary path never prints (twin of #12032) ([#12047](https://github.com/diegosouzapw/OmniRoute/pull/12047))
- **fix(barrel):** migrate src/app/ off the @/lib/localDb barrel import (#11795 Phase 2) ([#12051](https://github.com/diegosouzapw/OmniRoute/pull/12051)) — thanks @jonlwheat2-gif
- **fix(barrel):** migrate src/lib/ off the localDb barrel to direct db imports (#59) ([#12052](https://github.com/diegosouzapw/OmniRoute/pull/12052)) — thanks @jonlwheat2-gif
- **fix(barrel):** migrate open-sse, src/shared, src/sse, src/models, src/domain off the @/lib/localDb barrel import (#11795 Phase 4) ([#12053](https://github.com/diegosouzapw/OmniRoute/pull/12053)) — thanks @jonlwheat2-gif
- **fix(barrel):** delete the @/lib/localDb barrel — every consumer migrated (#11795 Phase 5) ([#12055](https://github.com/diegosouzapw/OmniRoute/pull/12055)) — thanks @jonlwheat2-gif
- **fix(radar):** restore D12 public boundary ([#12057](https://github.com/diegosouzapw/OmniRoute/pull/12057))
- **fix(dev):** bound webpack and Tailwind scans ([#12075](https://github.com/diegosouzapw/OmniRoute/pull/12075)) — thanks @backryun
- **fix(dev):** isolate root layout settings reads ([#12076](https://github.com/diegosouzapw/OmniRoute/pull/12076)) — thanks @backryun
- **fix(dev):** reduce instrumentation executor fan-out (phase 3) ([#12078](https://github.com/diegosouzapw/OmniRoute/pull/12078)) — thanks @backryun
- **fix(dev):** make logging resources HMR-singleton ([#12079](https://github.com/diegosouzapw/OmniRoute/pull/12079)) — thanks @backryun
- **fix(sse):** keep unavailable forced connections scoped ([#12080](https://github.com/diegosouzapw/OmniRoute/pull/12080)) — thanks @keeltrace
- **fix(dev):** isolate batch dispatch from instrumentation ([#12081](https://github.com/diegosouzapw/OmniRoute/pull/12081)) — thanks @backryun
- **fix(ci):** reconcile release test contract drift ([#12082](https://github.com/diegosouzapw/OmniRoute/pull/12082)) — thanks @backryun
- **fix(ci):** accept CVE-2025-68121 in the prebuilt tls-client .so, auto-close base-red issues, guard Scorecard on the default branch ([#12085](https://github.com/diegosouzapw/OmniRoute/pull/12085))
- **fix(db):** drop three consumer-less 1proxy exports — dead-code base-red on release/v3.8.51 after the barrel deletion (#12055) ([#12087](https://github.com/diegosouzapw/OmniRoute/pull/12087))
- **fix(api):** clear the six API-route TypeScript regressions the new gate landed red on ([#12094](https://github.com/diegosouzapw/OmniRoute/pull/12094))
- **fix(release):** never let the tag-push Create Release append auto notes to the curated body ([#12096](https://github.com/diegosouzapw/OmniRoute/pull/12096))
- **fix(sse):** honor CLIProxyAPI environment API key ([#12099](https://github.com/diegosouzapw/OmniRoute/pull/12099)) — thanks @RaviTharuma
- **fix(oauth):** bind Google refresh to the client that issued the token ([#12106](https://github.com/diegosouzapw/OmniRoute/pull/12106)) — thanks @HouMinXi
- **fix(dashboard):** make RequestLoggerDetail loadable outside Next — CSS via globals.css + CJS/ESM interop (#11703 base-reds) ([#12114](https://github.com/diegosouzapw/OmniRoute/pull/12114))
- **fix(leases):** project status lease row to lease columns so joined connection PII never escapes ([#12115](https://github.com/diegosouzapw/OmniRoute/pull/12115)) — thanks @geek007git
- **fix(plugins):** do not kill the plugin process when a fire-and-forget hook times out ([#12116](https://github.com/diegosouzapw/OmniRoute/pull/12116)) — thanks @geek007git
- **fix(guardrails):** restore injection-guard logging on middleware-only routes ([#12117](https://github.com/diegosouzapw/OmniRoute/pull/12117)) — thanks @geek007git
- **fix(plugins):** refresh stored manifest from disk on activate so new hook fields reach existing installs ([#12120](https://github.com/diegosouzapw/OmniRoute/pull/12120)) — thanks @geek007git
- **fix(codex):** preserve existing provider state when bulk-import upserts a matching connection ([#12122](https://github.com/diegosouzapw/OmniRoute/pull/12122)) — thanks @geek007git
- **fix(ci):** clear the two base-reds the 2026-08-30 merge batch left on release/v3.8.51 (round 3) ([#12123](https://github.com/diegosouzapw/OmniRoute/pull/12123))
- **fix(sse):** strip type:'custom' from Claude tools on agentrouter dispatch ([#12126](https://github.com/diegosouzapw/OmniRoute/pull/12126)) — thanks @wahidsadik371-coder
- **fix(sse):** stop advertising video providers the dispatcher cannot run ([#12131](https://github.com/diegosouzapw/OmniRoute/pull/12131)) — thanks @quiterunner-commits
- **fix(search):** treat HTTP 432 and plan limit errors as transient cooldown ([#12139](https://github.com/diegosouzapw/OmniRoute/pull/12139)) — thanks @HouMinXi
- **fix(sse):** refuse an AI Horde queue that cannot fit the request budget ([#12143](https://github.com/diegosouzapw/OmniRoute/pull/12143)) — thanks @quiterunner-commits
- **fix(ci):** clear the base-reds the afternoon merge batch left on release/v3.8.51 (round 5: provider count 352, TS2554/TS2677) ([#12144](https://github.com/diegosouzapw/OmniRoute/pull/12144))
- **fix(translator):** strip neutral tool_choice when tools absent in Responses-to-Chat (#12141) ([#12166](https://github.com/diegosouzapw/OmniRoute/pull/12166)) — thanks @HouMinXi
- **fix(guardrails):** pass providerId to getResolvedModelCapabilities in checkComboVision (#12112) ([#12169](https://github.com/diegosouzapw/OmniRoute/pull/12169)) — thanks @HouMinXi
- **fix(translator):** strip plaintext reasoning content for opaque responses backends (#12128) ([#12171](https://github.com/diegosouzapw/OmniRoute/pull/12171)) — thanks @HouMinXi
- **perf(stream):** compile hot-path regexes once, bound token caches, fix quadratic buffering ([#12179](https://github.com/diegosouzapw/OmniRoute/pull/12179)) — thanks @opensource-elearning
- **fix:** resolve compression worker file using runtime anchors instead of… ([#12183](https://github.com/diegosouzapw/OmniRoute/pull/12183)) — thanks @NightStalker-87
- **fix:** prevent Claude Code session kills via liveness-aware readiness + auto model echo ([#12189](https://github.com/diegosouzapw/OmniRoute/pull/12189)) — thanks @opensource-elearning
- **fix(translator):** preserve falsy primitive values in Gemini and Antigravity function response results ([#12191](https://github.com/diegosouzapw/OmniRoute/pull/12191)) — thanks @Siva010
- **fix(oauth):** mark empty Antigravity projectId as degraded and clear stale errors (#11284) ([#12205](https://github.com/diegosouzapw/OmniRoute/pull/12205)) — thanks @HouMinXi
- **fix(providers):** gate the Codex auto-ping usage read on the shared quota throttle ([#12209](https://github.com/diegosouzapw/OmniRoute/pull/12209)) — thanks @abhisheksharma2411
- **fix(combos):** prioritize SQLite row id over inner JSON id and notify delete errors ([#12213](https://github.com/diegosouzapw/OmniRoute/pull/12213)) — thanks @HouMinXi
- **fix(providers):** mark featherless.ai as no longer having a free tier (`hasFree: false`) ([#12216](https://github.com/diegosouzapw/OmniRoute/pull/12216)) — thanks @turbolego
- **fix(deepseek-web):** stop Turbopack dev panic in the PoW worker path resolver ([#12221](https://github.com/diegosouzapw/OmniRoute/pull/12221)) — thanks @hartmark
- **fix(chat-admission):** clarify local 503 source ([#12223](https://github.com/diegosouzapw/OmniRoute/pull/12223)) — thanks @keeltrace
- **fix(dev):** silence webpack runtime module warnings ([#12228](https://github.com/diegosouzapw/OmniRoute/pull/12228)) — thanks @backryun
- **fix(db):** use module.require for CommonJS runtime driver loading ([#12230](https://github.com/diegosouzapw/OmniRoute/pull/12230)) — thanks @raheemuddin786
- **fix(memory):** honest probe-driven FTS5 keyword status + memory_id rowid sync ([#12231](https://github.com/diegosouzapw/OmniRoute/pull/12231)) — thanks @raheemuddin786
- **fix(system):** propagate abort signal to stream reader in HTTP version checks ([#12232](https://github.com/diegosouzapw/OmniRoute/pull/12232)) — thanks @raheemuddin786
- **fix(resilience):** isolate local host execution errors from provider circuit breakers ([#12233](https://github.com/diegosouzapw/OmniRoute/pull/12233)) — thanks @raheemuddin786
- **fix(sse):** sort injected tools deterministically for prompt caching ([#12234](https://github.com/diegosouzapw/OmniRoute/pull/12234)) — thanks @raheemuddin786
- **fix(combo):** always clear the loop-safety timer, not just on the happy path (#11804) ([#12245](https://github.com/diegosouzapw/OmniRoute/pull/12245))
- **fix(quality):** base-red pair — stryker tap registration + turn-pin suites aligned to the window gate ([#12255](https://github.com/diegosouzapw/OmniRoute/pull/12255)) — thanks @backryun
- **fix(dev):** qualify Turbopack runtime boundaries ([#12258](https://github.com/diegosouzapw/OmniRoute/pull/12258)) — thanks @backryun
- **fix(combo):** fall back to full pool when collapsed sole survivor is context-too-small ([#12278](https://github.com/diegosouzapw/OmniRoute/pull/12278)) — thanks @opensource-elearning
- **fix(combo):** bound the pre-dispatch unavailable skip so a stale label cannot dark a pool (#12168) ([#12285](https://github.com/diegosouzapw/OmniRoute/pull/12285))
- **fix(security):** strip Qwen/Alibaba console-session cookies from provider API responses ([#12287](https://github.com/diegosouzapw/OmniRoute/pull/12287))
- **fix(usage):** console-aware Token Plan guidance + subscription hint on bailian 401 ([#12288](https://github.com/diegosouzapw/OmniRoute/pull/12288))
- **fix(resourcePressure):** log numeric detail on every rejection, recover faster ([#12293](https://github.com/diegosouzapw/OmniRoute/pull/12293)) — thanks @hartmark
- **fix(release):** drain the 2026-09-01 base-red window — passthrough usage regression + radarPage i18n keys ([#12327](https://github.com/diegosouzapw/OmniRoute/pull/12327))
- **fix(ci):** openapi-security-tiers checker must honor routeGuard patterns + imported prefixes ([#12350](https://github.com/diegosouzapw/OmniRoute/pull/12350)) — thanks @ggiak
- **perf(ci):** cache node_modules in the npm-ci-retry composite (#8084 D3) ([#12408](https://github.com/diegosouzapw/OmniRoute/pull/12408))
- **revert(ui):** point CTAs back at their real destinations ([#12410](https://github.com/diegosouzapw/OmniRoute/pull/12410))
- **fix(release):** drain the 2026-09-02 base-red — rerank-providers import + api-typecheck baseline ratchet ([#12414](https://github.com/diegosouzapw/OmniRoute/pull/12414))
- **fix(types):** make system prompt injection noImplicitAny-safe ([#12416](https://github.com/diegosouzapw/OmniRoute/pull/12416))
- **fix(quality):** validate typecheck baseline schema ([#12419](https://github.com/diegosouzapw/OmniRoute/pull/12419))
- **fix(memory):** resolve rerank provider node cache import ([#12421](https://github.com/diegosouzapw/OmniRoute/pull/12421))
- **fix(ci):** repair release-root regressions — pack dedup, web-session syntax, uc-image ids ([#12423](https://github.com/diegosouzapw/OmniRoute/pull/12423)) — thanks @backryun
- **fix(providers):** separate MaxAI and UC credential contracts ([#12431](https://github.com/diegosouzapw/OmniRoute/pull/12431))
- **fix(cli):** preserve Claude settings on config set ([#12432](https://github.com/diegosouzapw/OmniRoute/pull/12432)) — thanks @KrzysiekSko
- **fix(providers):** repair the maxai credential block truncated by merge auto-resolve ([#12433](https://github.com/diegosouzapw/OmniRoute/pull/12433))
- **fix(quality):** record the 2026-09-02 merged growth in the file-size baseline ([#12434](https://github.com/diegosouzapw/OmniRoute/pull/12434))
- **fix(uc):** route UC error strings through sanitizeErrorMessage; allowlist the retired codex id ([#12437](https://github.com/diegosouzapw/OmniRoute/pull/12437))
- **fix(adapta):** redact streamed upstream errors ([#12438](https://github.com/diegosouzapw/OmniRoute/pull/12438))
- **fix(sse):** remap non-contiguous upstream tool_calls index to a gap-free output_index ([#12445](https://github.com/diegosouzapw/OmniRoute/pull/12445)) — thanks @hartmark
- **fix(quality):** detect duplicate tool_calls entries in one response ([#12446](https://github.com/diegosouzapw/OmniRoute/pull/12446)) — thanks @hartmark
- **fix(conversations):** resolve turn content OmniRoute never sends back to the client ([#12447](https://github.com/diegosouzapw/OmniRoute/pull/12447)) — thanks @hartmark
- **fix(combo):** do not treat credits-exhausted 401 as auth skip ([#12449](https://github.com/diegosouzapw/OmniRoute/pull/12449)) — thanks @RaviTharuma
- **fix(auth):** do not park healthy quota accounts as expired ([#12452](https://github.com/diegosouzapw/OmniRoute/pull/12452)) — thanks @RaviTharuma
- **fix(responses-continuation):** fail closed on a collector-truncated, empty output array ([#12460](https://github.com/diegosouzapw/OmniRoute/pull/12460)) — thanks @hartmark
- **fix(logging):** raise the SSE payload collector's default cap ([#12461](https://github.com/diegosouzapw/OmniRoute/pull/12461)) — thanks @hartmark
- **fix(resilience):** surface Responses failed.error.message in 502s ([#12472](https://github.com/diegosouzapw/OmniRoute/pull/12472)) — thanks @RaviTharuma
- **fix(security):** unbiased maxai X-Random nonce + stricter URL/regex assertions ([#12502](https://github.com/diegosouzapw/OmniRoute/pull/12502))
- **fix(video):** re-anchor transcript log-redaction so PII/credential maskers can't reopen the leak (#12150 P1 follow-up) ([#12503](https://github.com/diegosouzapw/OmniRoute/pull/12503))
- **fix(monitoring):** serve cached credentialHealth off the request path ([#12533](https://github.com/diegosouzapw/OmniRoute/pull/12533)) — thanks @RaviTharuma
- **fix(quota):** keep Antigravity Gemini usable when Claude weekly is empty ([#12566](https://github.com/diegosouzapw/OmniRoute/pull/12566)) — thanks @HouMinXi
- **fix(authz):** hard-gate every credential export and CLI-config write (GHSA-5926-2w35-7h4q) ([#12600](https://github.com/diegosouzapw/OmniRoute/pull/12600))
- **fix(docs):** restore the Next build — REMOVED_PROVIDERS.md had no frontmatter (base-red #12581) ([#12610](https://github.com/diegosouzapw/OmniRoute/pull/12610))
- **fix(quality):** re-point the zcodeProtocol public-creds allowlist to line 313 ([#12615](https://github.com/diegosouzapw/OmniRoute/pull/12615))
- **fix(security):** close 3 advisories — search baseUrl exfil, sk- in the error sanitizer, bifrost relay header leak ([#12620](https://github.com/diegosouzapw/OmniRoute/pull/12620))
- **fix(providers):** sanitize boolean required and nested bare maps for Gemini (#12269) ([#12624](https://github.com/diegosouzapw/OmniRoute/pull/12624)) — thanks @HouMinXi
- **fix(resilience):** keep Overloaded STREAM_EARLY_EOF off the provider breaker ([#12626](https://github.com/diegosouzapw/OmniRoute/pull/12626)) — thanks @HouMinXi
- **fix(catalog):** re-audit free-tier quotas against official pages ([#12649](https://github.com/diegosouzapw/OmniRoute/pull/12649))
- **fix(dashboard):** keep a request's pending-tracking id stable across combo target retries ([#12650](https://github.com/diegosouzapw/OmniRoute/pull/12650)) — thanks @hartmark
- **fix(tests):** retire dead model ids from the chat-pipeline integration suite (base-red #12581) ([#12670](https://github.com/diegosouzapw/OmniRoute/pull/12670))
- **fix:** resolve SqliteError no such table compression_run_telemetry during cleanup ([#12682](https://github.com/diegosouzapw/OmniRoute/pull/12682)) — thanks @groovecityJO
- **fix(combo):** treat a pin-only step as implicit connection allowlist ([#12697](https://github.com/diegosouzapw/OmniRoute/pull/12697)) — thanks @HouMinXi
- **fix(ci):** ship .npmrc in published package so legacy-peer-deps applies to consumers (#11544) ([#12699](https://github.com/diegosouzapw/OmniRoute/pull/12699)) — thanks @KooshaPari
- **fix(docker):** pin 4 CLI tools to exact versions (#12576) ([#12703](https://github.com/diegosouzapw/OmniRoute/pull/12703)) — thanks @KooshaPari
- **fix(docker):** document and harden cli profile trust boundary (#12570) ([#12706](https://github.com/diegosouzapw/OmniRoute/pull/12706)) — thanks @KooshaPari
- **fix(sse):** redact video transcript in pre-guardrail rejected-request logs (#12150 P2 item 7) ([#12710](https://github.com/diegosouzapw/OmniRoute/pull/12710))
- **fix(i18n):** quote <name> placeholder in OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES description (#12505) ([#12769](https://github.com/diegosouzapw/OmniRoute/pull/12769)) — thanks @KooshaPari
- **fix(api/pricing):** surface validation error message as string, not raw object (#12494) ([#12771](https://github.com/diegosouzapw/OmniRoute/pull/12771)) — thanks @KooshaPari
- **fix(sse):** re-enable prompt compression for native Codex passthrough ([#12834](https://github.com/diegosouzapw/OmniRoute/pull/12834)) — thanks @thomasmaerz
### 📝 Maintenance
---
- **docs(readme):** reconcile live v3.8.50 provider, free-tier, CLI, routing, test, community, sponsor, acknowledgment, and SVG metrics with their audited source denominators, including a deduplicated OmniRoute-in-Action snapshot and distinct contributor rankings for merged pull requests, GitHub-attributed commits, and Git history ([#11356](https://github.com/diegosouzapw/OmniRoute/pull/11356)). ([#11434](https://github.com/diegosouzapw/OmniRoute/pull/11434)) — thanks @hartmark
- **docs:** sync the canonical provider count 354 → 356 across `README.md`, `AGENTS.md`, `llm.txt` (+ 42 i18n mirrors), the four README SVG diagrams, `docs/reference/PROVIDER_REFERENCE.md` (regenerated) and the `package.json` description after Opper (#11629) and 1min.ai (#11631) boarded the catalog — closes the `check:docs-counts-sync` strict drifts that kept `release/v3.8.51` red ([#11449](https://github.com/diegosouzapw/OmniRoute/issues/11449)). ([#11673](https://github.com/diegosouzapw/OmniRoute/pull/11673)) — thanks @jonlwheat2-gif
- **test(providers):** update count-derived assertions after the v3.8.51 provider additions — `APIKEY_PROVIDERS` 233 → 235 (Opper #11629 + 1min.ai #11631), reserved-prefix REGISTRY walk 395 → 398, `WEB_FETCH_PROVIDERS` now includes `nimble-search` (#11620), and the provider translate-path golden snapshot regenerated ([#11449](https://github.com/diegosouzapw/OmniRoute/issues/11449)). ([#11674](https://github.com/diegosouzapw/OmniRoute/pull/11674)) — thanks @jonlwheat2-gif
- **test(sse):** bump the hard-lease connection-query inventory for `src/lib/tokenHealthCheck.ts` to 2 — the verify-only web-cookie sweep added by #11495 split the single `getProviderConnections` call into oauth + cookie variants, which the frozen inventory had not tracked ([#11495](https://github.com/diegosouzapw/OmniRoute/pull/11495)). ([#11677](https://github.com/diegosouzapw/OmniRoute/pull/11677)) — thanks @jonlwheat2-gif / @oyi77
- **chore(release):** clear the `release/v3.8.51` base-red gates — docs migration count (+38 i18n mirrors), stryker `tap.testFiles`, lockfile registry host, stale ESLint suppressions, the 7 lint regressions they were hiding, the stale `APIKEY_PROVIDERS` partition count (231 → 233), the `glm-5.3-max` tier missing from the GLM test table, a dead `modelType === "chat"` comparison in the models catalog, and the `resolveLiveWsUrl` / `sanitizeLiveWsPort` helpers from #11388 that a later merge dropped from `wsPath.ts` (dashboard live WebSocket URL resolution) ([#11502](https://github.com/diegosouzapw/OmniRoute/pull/11502)) — thanks @MumuTW
- **chore(free-models):** the free-tier regime table now declares which totals figure each regime feeds and whether it may take the no-auth shortcut, so the sets that used to repeat those answers by hand are derived from it — a new `freeType` no longer compiles until it has answered every question, instead of silently contributing to no total ([#11537](https://github.com/diegosouzapw/OmniRoute/pull/11537)) — thanks @maxmad64bis
- **docs(routing):** LKGP strategy copy now states provider-level stickiness and the explicit rules fallback instead of promising per-target pinning ([#11555](https://github.com/diegosouzapw/OmniRoute/pull/11555)) — thanks @maxmad64bis
- **test(chatcore):** pin the response tool-name alias resolution against the ordering hazard that broke every Gemini/Antigravity MCP tool call in v3.8.49. `extractRequestToolIdentityMap` deletes `translatedBody._toolNameMap`, so the later read is always undefined and the ledger survives only through the `requestToolIdentityMap` fallback — removing that fallback previously left the entire tool-name suite green. The resolution moves into `resolveResponseToolNameMap()` next to the map it depends on, with a regression guard that fails without the recovery. No behaviour change. ([#11575](https://github.com/diegosouzapw/OmniRoute/pull/11575)) — thanks @amartinawi
- **docs(env):** document `OMNIROUTE_CORPUS_CACHE_SIZE` in `.env.example` and `ENVIRONMENT.md`, so the env/docs contract check stops failing Docs Gates on every branch ([#11576](https://github.com/diegosouzapw/OmniRoute/pull/11576)) — thanks @ntdatt812
- **chore(stryker):** register `route-guard-tunnel-processes-local-only` and `universal-quota-aware-routing` in `tap.testFiles`, so their mutant kills count for the three modules they cover ([#11577](https://github.com/diegosouzapw/OmniRoute/pull/11577)) — thanks @ntdatt812
- **test(authz):** pin `LOCAL_ONLY_API_GET_EXEMPTIONS` by exact membership instead of by entry count, so the guard names the offending path and also catches a substitution ([#11580](https://github.com/diegosouzapw/OmniRoute/pull/11580)) — thanks @ntdatt812
- **test(build):** pin `resolveNextBuildBundlerFlag` to the `OMNIROUTE_USE_TURBOPACK` contract it implements, across runtimes, instead of asserting an unimplemented Bun override ([#11583](https://github.com/diegosouzapw/OmniRoute/pull/11583)) — thanks @ntdatt812
- **test(claude-web):** the Claude Web transport test's barrel anchors no longer pin the exact construction spelling. #11421 made the executor map lazy, so `/"claude-web": new ClaudeWebExecutor\(\)/` stopped matching and the test went red on `release/v3.8.51` itself — a base-red gate every open PR inherited. The anchors now match the alias key and the class name and tolerate either spelling in between, and were checked to still fail when an alias is genuinely repointed at another executor ([#11585](https://github.com/diegosouzapw/OmniRoute/pull/11585)) — thanks @ntdat812
- **test(models):** pin `qwen3.8-max` resolution per provider — two of the four catalogs now serve the bare id and no longer carry `-preview` ([#11590](https://github.com/diegosouzapw/OmniRoute/pull/11590)) — thanks @ntdatt812
- **test(executors):** drop the stale 128-tool truncation assertion that contradicted the #11444 pass-through contract ([#11591](https://github.com/diegosouzapw/OmniRoute/pull/11591)) — thanks @ntdatt812
- **test(token-health):** pin the current terminal-skip boundary — expired with retry budget is probed, `account_deactivated` stays skipped ([#11592](https://github.com/diegosouzapw/OmniRoute/pull/11592)) — thanks @ntdatt812
- **test(models):** follow #11503's retarget of the `gemini-3-pro-high` alias to the hyphenated catalog id ([#11593](https://github.com/diegosouzapw/OmniRoute/pull/11593)) — thanks @ntdatt812
- **refactor(video bridge):** extract per-video acquisition, whole-result caching, description, and metrics/abort/cleanup out of `VideoBridgeGuardrail.preCall` into a `processVideoPart` seam in a new `videoBridgePipeline.ts`, behind explicit `VideoMediaBrokerPort`, `VideoAudioTranscriptionPort`, and `VideoDrilldownPort` boundaries; `preCall` now only handles request traversal, policy, and response aggregation. The Video tab's FFmpeg/ffprobe runtime status is now an explicit `unknown` / `restricted` / `unavailable` / `available` state instead of a nullable boolean pair, fixing a case where an in-flight or failed probe was mislabeled as "install FFmpeg" ([#11657](https://github.com/diegosouzapw/OmniRoute/issues/11657)). ([#12016](https://github.com/diegosouzapw/OmniRoute/pull/12016))
- **test(opencode-plugin):** add unit test coverage for `formatFreeBudget()` naming helper ([#11660](https://github.com/diegosouzapw/OmniRoute/pull/11660)) — thanks @f9td56dbgh-hub
- **docs(video):** clarify that the Video Bridge transcript `source` field (`client`, `embedded`, `audio-bridge`) is presently caller-declared and not yet server-verified — OmniRoute enforces the enum shape but does not cryptographically confirm that an `embedded`/`audio-bridge` label came from a server-owned extraction ([#11661](https://github.com/diegosouzapw/OmniRoute/issues/11661)). ([#12001](https://github.com/diegosouzapw/OmniRoute/pull/12001))
- **chore(providers):** retire the distributed Raycast Relay and Hailuo Web integrations after a provenance audit confirmed substantial derivation from GPL-3.0 sources; the official GitHub and MiniMax API providers remain available. Migration 163 disables legacy connections and invalidates their active leases while preserving connection IDs, audit/history rows, quota snapshots, usage records, and fail-closed API-key connection policies. ([#11691](https://github.com/diegosouzapw/OmniRoute/pull/11691))
- **chore(providers):** retire the Felo Web provider while its GPL-derived provenance remains on licensing HOLD ([#11698](https://github.com/diegosouzapw/OmniRoute/pull/11698)) — stale runtime and database references now fail closed while existing connection, lease, and usage history remains preserved for auditability.
- **fix(sse):** retire the Gemini Web image-generation bridge while provenance and licensing remain on HOLD, preserving Gemini Web chat/auth/tools and legitimate Gemini/Nano Banana image providers ([#11708](https://github.com/diegosouzapw/OmniRoute/pull/11708))
- **chore(audio):** retire the built-in EdgeTTS provider and its 11-voice catalog while provenance/licensing review remains on HOLD; the generic `/v1/audio/speech` endpoint and the other speech providers remain available ([#11711](https://github.com/diegosouzapw/OmniRoute/pull/11711))
- **fix(providers):** retire the unofficial Qwen Web integration and its `qwen-web`/`qw` identities while third-party provenance remains on HOLD ([#11713](https://github.com/diegosouzapw/OmniRoute/pull/11713)) — direct requests now fail closed with a sanitized HTTP 410 and combos skip the retired target, while migration guards preserve connection IDs, allowlists, usage/audit history and invalidate active leases. Supported Qwen API, Qwen Code, cloud, token-plan, audio, local and model-name surfaces remain available; restoring Qwen Web requires a qualified provenance/license decision.
- **fix(providers):** retire `microsoft-designer-web` and its `msdesigner` alias from runtime and catalogs while preserving stored credentials, provider history and allowlists as inactive records — source dependency on gpt4free's [`MicrosoftDesigner.py`](https://github.com/xtekky/gpt4free/blob/0943d16eef757a44deb60651c07bbe5fa958cafe/g4f/Provider/needs_auth/MicrosoftDesigner.py) is proven at snapshot `0943d16eef757a44deb60651c07bbe5fa958cafe`, whose [`LICENSE`](https://github.com/xtekky/gpt4free/blob/0943d16eef757a44deb60651c07bbe5fa958cafe/LICENSE) is GPL-3.0; expressive adaptation is probable, which records provenance/licensing risk without asserting a legal conclusion ([#11720](https://github.com/diegosouzapw/OmniRoute/pull/11720)).
- **docs(legal):** centralize pinned MIT notices for the vendored GCF codec and local flag assets ([#11726](https://github.com/diegosouzapw/OmniRoute/pull/11726))
- Split the 2,952-line `adobeFireflyClient.ts` into nine focused modules (catalog, credentials, payload, ARP session, upload, responses, IMS, account, poll) behind an unchanged barrel — no consumer edits and no public API change (#11728). — thanks @MumuTW
- **fix(dashboard):** replaced the Model Lockout card's unprovenanced notification MP3 with a short, locally synthesized Web Audio chime ([#11731](https://github.com/diegosouzapw/OmniRoute/pull/11731)) — both toggles retain optional audible feedback without shipping replacement media or third-party code, and unsupported, suspended, or rejected audio contexts fail open without blocking the setting change.
- **chore(deepseek-web):** remove the provenance-unresolved DeepSeek PoW WASM binary and its runtime loader/tracing while retaining the existing JavaScript solver (slower at high difficulty) ([#11732](https://github.com/diegosouzapw/OmniRoute/pull/11732)).
- **chore(assets):** remove four unused provider assets whose redistribution provenance could not be verified while retaining the active Freebuff SVG ([#11735](https://github.com/diegosouzapw/OmniRoute/pull/11735))
- **docs(assets):** retired two raster images whose redistribution provenance could not be verified ([#11736](https://github.com/diegosouzapw/OmniRoute/pull/11736)) — PicoClaw documentation now reuses the existing SVG, while Zoo Code remains a text link without an image.
- **docs(legal):** add pinned notices for six LobeHub-derived provider SVGs and 65 byte-exact theSVG assets, retaining registry claims as unverified metadata with explicit trademark and HOLD boundaries ([#11737](https://github.com/diegosouzapw/OmniRoute/pull/11737)).
- **chore(quality):** added a versioned 225/225 provider-asset provenance manifest and blocking drift gate ([#11743](https://github.com/diegosouzapw/OmniRoute/pull/11743)) — the inventory pins magic MIME and SHA-256 for every physical file, records immutable source matches for 71 assets, keeps 69 probable and 85 unresolved entries explicit but non-blocking, and treats provenance as source evidence rather than copyright or trademark clearance.
- **refactor(mitm):** replace four previously derived Traffic Inspector utilities — conversation normalization, SSE merging, secret masking, and header sanitization — with independent clean-room implementations based on public provider schemas, WHATWG SSE, and RFC 6750, while keeping the two independently classified passthrough surfaces unchanged ([#11748](https://github.com/diegosouzapw/OmniRoute/pull/11748)).
- Added a unit test that fails seven days before any dated pack under `config/` (`validUntil` and sibling keys) lapses, naming the file and key. The Alibaba free-tier pack expired on 2026-08-27 and turned every PR red the next morning with no commit involved; renewal now happens on someone's terms, not the clock's. ([#11891](https://github.com/diegosouzapw/OmniRoute/pull/11891))
- `check:workflows` now fails (under `--strict`/`--ratchet`) when any job routed to a self-hosted runner publishes with `--provenance` — npm rejects that with `422` at the registry, which in v3.8.50 only surfaced after the tag and Docker images were public. ([#11895](https://github.com/diegosouzapw/OmniRoute/pull/11895))
- `scripts/ops/runner-janitor.sh` now proves a path is idle with one `lsof` snapshot and removes stale leftovers itself (tmpfs after 3 h — it is RAM — disk after 24 h), kills orphan `next-build` processes, prunes checkouts of stopped runners, and alerts on memory pressure; `--dry-run` shows exactly what it would do. `docs/ops/RUNNER_BOX.md` reconciled to the measured box (31 GB, 10 listeners). ([#11893](https://github.com/diegosouzapw/OmniRoute/pull/11893))
- The `next-build` artefact (1.3 GB) is now written and read under `$RUNNER_TEMP` (per-runner, on disk) instead of `/tmp`, which on the self-hosted pool is a 12 GB tmpfs in RAM. Landing it there took 2732 of the publish job's 76 minutes, and the fixed `/tmp/e2e-build.tar.gz` name let E2E jobs on different runners overwrite each other's download. ([#11896](https://github.com/diegosouzapw/OmniRoute/pull/11896))
- The CI `build` job now runs in two concurrency lanes — `main` and pull requests — so a release build is never queued behind (or OOM-killed beside) PR builds on the self-hosted pool, which holds one `next-build` comfortably and two at the edge. ([#11901](https://github.com/diegosouzapw/OmniRoute/pull/11901))
- Re-freeze the ESLint suppressions on `release/v3.8.51` from a clean-room measurement (2 stale file entries pruned, 55 pre-existing `no-explicit-any` in six new files frozen under #11924) and drop the dead `GPT_SIZE_MAP` constant orphaned by the Adobe Firefly client split, so `No new ESLint warnings` stops failing every PR with exit 2 (Refs #11924) ([#11955](https://github.com/diegosouzapw/OmniRoute/pull/11955))
- Type the 55 `no-explicit-any` sites that had been frozen under #11924 — four redundant casts in `socksConnectorWithFamily.ts` (undici/socks types already accept them) and the mocks/fixtures of the socks-timeout and isFree suites — and drop their suppression entries; the ESLint ratchet shrinks from 5487 to 5432 (Closes #11924) ([#11975](https://github.com/diegosouzapw/OmniRoute/pull/11975))
- Take the two hosted-runner builds off the PR rail: `Build App` (`build.yml`) is `workflow_dispatch`-only and `DAST smoke (PR)` runs only for PRs into `main` — the 7 GB hosted VM cannot build `release/v3.8.51` in any profile (19/30 red, VM shutdown ~8 min into `next build`) and both checks had turned into permanent noise on every release PR; the bundle stays validated by `ci.yml` on `main` and by `nightly-release-green` on `release/**` (Closes #11946) ([#11962](https://github.com/diegosouzapw/OmniRoute/pull/11962))
- Move the four nightly jobs that build the backend (`nightly-schemathesis`, `nightly-llm-security` promptfoo + garak, `nightly-resilience` axe-a11y) off the hosted 7 GB runner — where they died on `release/v3.8.51` unseen — onto the box's new `omni-light` pool (two listeners), and document the reshaped fleet (4 active OmniRoute listeners: 2 `omni-build` + 2 `omni-light`, janitor ceiling 4) (Closes #11965) ([#11967](https://github.com/diegosouzapw/OmniRoute/pull/11967))
- Stop painting every fork PR into `release/**` red: `quality.yml` `Build (advisory)` is skipped (GitHub still reports `continue-on-error` failures as check FAILURE). Hosted `ubuntu-latest` cannot finish `npm run build` on this tree — same class as #11962 taking `build.yml` off the PR rail. `docker-publish.yml` amd64 now runs on the `.113` `omni-build` pool (31 GB / 32 cores, two listeners) with Turbopack, shares the `heavy-build-main` lane with `ci.yml` `Build` so it queues instead of becoming a third heavy, and keeps per-ref concurrency (a merge storm was starting 8 concurrent OOM builds). arm64 stays on `ubuntu-24.04-arm` with webpack — there is no ARM box. Fallback when `USE_VPS_RUNNER` is off: hosted amd64 + webpack (#11976). ([#12021](https://github.com/diegosouzapw/OmniRoute/pull/12021))
- `docker-publish.yml` builds with webpack (`OMNIROUTE_USE_TURBOPACK=0`) on **both** arches, including amd64 on the `.113` `omni-build` pool. Turbopack had the RAM (31 GB, `omniroute-113-6`) and still panicked (`TurbopackInternalError: there must be a path to a root`, run 33253576569, 26 min in). The same tree's arm64 webpack build on hosted `ubuntu-24.04-arm` succeeded (run 33264823398). Dockerfile already documents webpack as the Docker escape hatch (#11976). ([#12050](https://github.com/diegosouzapw/OmniRoute/pull/12050))
- **docs(free-tier):** declare the counting vs deciding regimes for "is it free?" and guard the deciding path from DB-backed catalog resolution ([#12226](https://github.com/diegosouzapw/OmniRoute/pull/12226)) — thanks @maxmad64bis
- **docs(auto-combo):** The mode pack table in `docs/routing/AUTO-COMBO.md` now lists all six shipped packs with every weight each one sets, replacing a four-pack table whose numbers had also drifted from the source. It states plainly that no pack sets `quality`, so selecting any pack silences the observed-quality signal. Six more documents that quote the scoring factor count joined the `check:docs-counts` gate, which caught five stale claims — including one naming nine factors that do not exist — and two stale mode pack counts. The dashboard routing panel, which offered four of the six packs and labelled the default strategy "6-Factor Scoring", is now covered by a test; the two packs it was missing are `reliability-first` and `chaos-mode`, the latter labelled as the fault-injection profile it is rather than as one more routing preference ([#12316](https://github.com/diegosouzapw/OmniRoute/pull/12316)) — thanks @maxmad64bis
- **chore(quality):** rebaseline `src/lib/db/apiKeys.ts` for the ACL the key-creation path now preserves ([#12352](https://github.com/diegosouzapw/OmniRoute/pull/12352)) ([#12673](https://github.com/diegosouzapw/OmniRoute/pull/12673)) — thanks @KrzysiekSko
- **chore(providers):** bump the Claude Code wire identity and the Devin bridge image pin from `2.1.220` to `2.1.258` ([#12402](https://github.com/diegosouzapw/OmniRoute/pull/12402)) — thanks @ggiak ([#12604](https://github.com/diegosouzapw/OmniRoute/pull/12604))
- **docs(env):** align `.env.example`, the README Bun section, and the troubleshooting guide with the code: `OMNIROUTE_USE_TURBOPACK` also governs `npm run build` (not dev-only), `bun run build` follows that flag instead of auto-selecting Webpack, `OMNIROUTE_CHAT_MAX_HEAVY_IN_FLIGHT` is unset by default (no request-count cap), and the structural `503 chat_admission_busy` message matches `chatAdmissionResponses.ts` (#12404 — thanks @pacocartones)
- **chore(electron):** upgrade the desktop app to Electron 44 (Chromium 152, Node 24.18.1) ([#12217](https://github.com/diegosouzapw/OmniRoute/pull/12217)). **Requires macOS 13 (Ventura) or later** — Chromium dropped macOS 12 (Monterey), so Monterey users must stay on an earlier OmniRoute desktop build. Windows and Linux are unaffected; the app already shipped only x64/arm64, so Electron 44 dropping 32-bit builds changes nothing. Removes the `openAsHidden`/`wasOpenedAsHidden` login-item fields deleted in Electron 44 — hidden autostart continues to work through the `--hidden` argument registered with the login item ([#12554](https://github.com/diegosouzapw/OmniRoute/pull/12554)) — thanks @app/dependabot
- **chore(quality):** rebaseline `src/sse/handlers/chat.ts` for the effective-input persistence the continuation fix needs ([#12641](https://github.com/diegosouzapw/OmniRoute/pull/12641)) ([#12680](https://github.com/diegosouzapw/OmniRoute/pull/12680)) — thanks @hartmark
- `Coverage` job on `ci.yml`: the informational Codecov upload gets its own 5-minute ceiling and `continue-on-error`, and the job budget grows from 20 to 30 minutes (the 8-shard c8 merge alone takes ~10) — a stalled upload no longer ends the job `cancelled` and drags a fully green `main` run's conclusion down with it ([#11972](https://github.com/diegosouzapw/OmniRoute/pull/11972))
- CI hardening on the PR rail: the four `Unit Tests fast-path` shards get `timeout-minutes: 30` (a hung shard held a PR for 64 min instead of GitHub's 6 h default) and both ESLint file caches lose their `restore-keys` fallback, so a cache built under another suppressions file or lint config can no longer report stale verdicts (Refs #11600, #11924) ([#11963](https://github.com/diegosouzapw/OmniRoute/pull/11963))
- Every CI job that runs a `next build` (`build`, the npm `publish`, both release-green validations) now targets the `omni-build` runner label, which only two of the eight self-hosted runners carry. The box holds one build comfortably and two at the edge; a third now queues on GitHub instead of being OOM-killed by the kernel. ([#11932](https://github.com/diegosouzapw/OmniRoute/pull/11932))
- **refactor(combo):** move `handleRoundRobinCombo` (and `resolveTargetTokenLimit`) into `open-sse/services/combo/roundRobinCombo.ts`. `combo.ts` drops from 2164 to 1014 lines (`split("\n").length`); the leaf is 1199 (under the 1200 new-file cap). The round-robin call site uses a dynamic `import()` so `releaseStickyPinOnFailure` / `clearStaleLKGP` can stay exported from `combo.ts` without a static cycle. Skip / sticky / semaphore / safety-timer behavior is unchanged. ([#12811](https://github.com/diegosouzapw/OmniRoute/pull/12811)) — thanks @HouMinXi
- **chore(quality):** rebaseline the file-size caps the error-boundary campaign grew past (`open-sse/executors/codex.ts`, `open-sse/vendor/codex-chatgpt-web/bridge.ts`, both via [#12444](https://github.com/diegosouzapw/OmniRoute/pull/12444)) ([#12654](https://github.com/diegosouzapw/OmniRoute/pull/12654))
- **chore(quality):** rebaseline the file-size caps the hartmark batch grew past (`combos/page.tsx` via [#12355](https://github.com/diegosouzapw/OmniRoute/pull/12355), `open-sse/services/combo.ts` via [#12338](https://github.com/diegosouzapw/OmniRoute/pull/12338)) ([#12623](https://github.com/diegosouzapw/OmniRoute/pull/12623)) — thanks @hartmark
- **chore(quality):** rebaseline the file-size caps the HouMinXi batch grew past when its PRs stacked (`providers/page.tsx`, `chatCore.ts`, `accountFallback.ts`) — each PR measured correctly in isolation, none saw the stacking ([#12619](https://github.com/diegosouzapw/OmniRoute/pull/12619))
- **chore(quality):** rebaseline `open-sse/services/combo.ts` for the reset-aware scoring the HouMinXi batch stacked ([#12637](https://github.com/diegosouzapw/OmniRoute/pull/12637)) ([#12678](https://github.com/diegosouzapw/OmniRoute/pull/12678)) — thanks @HouMinXi
- **docs:** sync provider catalog count (direct commit `4f11b2ae3d`)
- **docs(skills):** sync omni-inference search fallback (direct commit `b4e76a7ed9`)
- **chore(lint):** prune two stale base suppressions (direct commit `1325047d56`)
- **test(auto-combo):** tolerate full-suite rotation load (direct commit `ad3e293f9f`)
- **test:** drain inherited quality snapshot reds (direct commit `21ed68d8ac`)
- **chore(quality):** rebaseline measured CLI bundle growth (direct commit `d35b3f9779`)
- **test(ui):** reproduce invalid AnySearch icon alias (direct commit `12cc6ca834`)
- **test(translator):** fix the relative imports of the relocated deferred-finish test (direct commit `777d9d1629`)
- **docs(counts):** bring the documented provider and migration counts to the measured values (direct commit `e70bea30e9`)
- **test(security):** drop the guard-target set left unused by the sync (direct commit `a32ba0e362`)
- **chore(db):** renumber model_capabilities migration 163 → 169 (cross-branch collision with 163_radar_feed_cache_generated_at) (direct commit `70f5d4cbf9`)
- **docs(cli):** document CLI_PRIME_AGENT_BIN and correct the CLI Agents count (direct commit `9058e39b61`)
- **chore(deps):** refresh runtimes and adopt ESLint 10 ([#11259](https://github.com/diegosouzapw/OmniRoute/pull/11259)) — thanks @backryun
- **refactor(local-corpus):** implement dynamic root resolution and LRU cache ([#11491](https://github.com/diegosouzapw/OmniRoute/pull/11491)) — thanks @raheemuddin786
- **chore(quality):** prune stale eslint-suppressions entries ([#11596](https://github.com/diegosouzapw/OmniRoute/pull/11596))
- **chore(ci):** unblock quality gates - env-doc sync + stryker tap.testFiles registration (#9282) ([#11609](https://github.com/diegosouzapw/OmniRoute/pull/11609)) — thanks @Meet6338-X
- **chore(ci):** point the circular-deps gate at dpdm's real JS entrypoint ([#11615](https://github.com/diegosouzapw/OmniRoute/pull/11615)) — thanks @benzntech
- **chore(quality):** rebaseline file-size for #11448/#11495/#11561 growth ([#11630](https://github.com/diegosouzapw/OmniRoute/pull/11630))
- **deps:** realign onnxruntime-node pin to @huggingface/transformers' 1.24.3 lockstep ([#11633](https://github.com/diegosouzapw/OmniRoute/pull/11633)) — thanks @jonlwheat2-gif
- **test(autoCombo):** port models_dev_tier #11508 guard from node:test to vitest ([#11635](https://github.com/diegosouzapw/OmniRoute/pull/11635)) — thanks @jonlwheat2-gif
- **chore(quality):** rebaseline file-size for #11083/#11631/#11628 growth ([#11636](https://github.com/diegosouzapw/OmniRoute/pull/11636))
- **chore(lint):** prune stale ESLint suppressions (base-red #11449) ([#11644](https://github.com/diegosouzapw/OmniRoute/pull/11644)) — thanks @jonlwheat2-gif
- **docs(changelog):** aggregate the ten v3.8.50 fragments into CHANGELOG.md ([#11683](https://github.com/diegosouzapw/OmniRoute/pull/11683))
- **chore(search):** drop unreachable credential sweep after #11642 ([#11687](https://github.com/diegosouzapw/OmniRoute/pull/11687)) — thanks @HouMinXi
- **ci:** add API route TypeScript regression gate ([#11705](https://github.com/diegosouzapw/OmniRoute/pull/11705)) — thanks @Sabeekhann
- **docs(dedup):** correct the CodeQL claim on the tenant-namespace comment ([#11712](https://github.com/diegosouzapw/OmniRoute/pull/11712))
- **docs(changelog):** add consolidated v3.8.50 stats, top-25 ranking and a .mailmap ([#11715](https://github.com/diegosouzapw/OmniRoute/pull/11715))
- **docs:** recommend gstack for AI-assisted workflows ([#11770](https://github.com/diegosouzapw/OmniRoute/pull/11770)) — thanks @santosraju99-hub
- **chore(quality):** tighten complexity/cognitive-complexity ratchets to the current tip; land the missed gateways.ts rebaseline ([#11771](https://github.com/diegosouzapw/OmniRoute/pull/11771)) — thanks @Rahulsharma0810 / @Rahulsharma0810
- **docs(i18n):** finish Freepik → Magnific rebrand in locale strings and README ([#11772](https://github.com/diegosouzapw/OmniRoute/pull/11772)) — thanks @turbolego
- **refactor(auto-combo):** fix divergent scoring in combo health reporting ([#11854](https://github.com/diegosouzapw/OmniRoute/pull/11854)) — thanks @jacobsparts
- **chore(changelog):** add missing fragments for #11919/#11918/#11916 ([#11938](https://github.com/diegosouzapw/OmniRoute/pull/11938))
- **test(check):** escape the runs-on fixture with JSON.stringify, not a quote-only replace ([#11942](https://github.com/diegosouzapw/OmniRoute/pull/11942)) (direct commit `fb7445eaa3`)
- **test(release):** align five suites with the contracts #11933, #11919 and #11876 shipped on release/v3.8.51 ([#11944](https://github.com/diegosouzapw/OmniRoute/pull/11944))
- **chore(quality):** remove unreachable code and restore test discovery ([#11950](https://github.com/diegosouzapw/OmniRoute/pull/11950)) — thanks @backryun
- **docs(agents):** sync-back landings are fast-forward, never squash ([#11964](https://github.com/diegosouzapw/OmniRoute/pull/11964))
- **test(infra):** retry recursive temp-dir removal instead of failing a shard on ENOTEMPTY (#11966) ([#11968](https://github.com/diegosouzapw/OmniRoute/pull/11968))
- **docs(ops):** the .113 heavy-build ceiling is one runner, not two ([#12048](https://github.com/diegosouzapw/OmniRoute/pull/12048))
- **test(ui):** correct inactive auto-fetch expectation ([#12098](https://github.com/diegosouzapw/OmniRoute/pull/12098)) — thanks @RaviTharuma
- **docs:** document native dependency check escape hatch ([#12101](https://github.com/diegosouzapw/OmniRoute/pull/12101)) — thanks @RaviTharuma
- **test(providers):** regenerate the translate-path golden for OrcaRouter (#11923) ([#12118](https://github.com/diegosouzapw/OmniRoute/pull/12118))
- **chore(quality):** velocity phase — loosen every numeric baseline by 20% until v4.0, monitor headroom nightly ([#12125](https://github.com/diegosouzapw/OmniRoute/pull/12125))
- **chore(lint):** batch 0 of #12146 — type the call-log-cap sqlite rows instead of 45 as-any casts ([#12157](https://github.com/diegosouzapw/OmniRoute/pull/12157))
- **chore(lint):** batch 4 of #12146 — shared/components react-hooks violations resolved ([#12159](https://github.com/diegosouzapw/OmniRoute/pull/12159))
- **chore(lint):** batch 1 of #12146 — dashboard/cli-code react-hooks violations resolved ([#12160](https://github.com/diegosouzapw/OmniRoute/pull/12160))
- **chore(lint):** batch 3 of #12146 — dashboard/settings react-hooks violations resolved ([#12162](https://github.com/diegosouzapw/OmniRoute/pull/12162))
- **chore(lint):** batch 2 of #12146 — resolve the react-hooks compiler violations in dashboard/providers ([#12163](https://github.com/diegosouzapw/OmniRoute/pull/12163))
- **chore(quality):** register search-432 cooldown test in stryker tap.testFiles ([#12170](https://github.com/diegosouzapw/OmniRoute/pull/12170))
- **chore(lint):** batch 5 of #12146 — combos, endpoint, provider-stats, api-manager and costs react-hooks violations resolved ([#12174](https://github.com/diegosouzapw/OmniRoute/pull/12174))
- **build:** add contributor fast profile ([#12192](https://github.com/diegosouzapw/OmniRoute/pull/12192)) — thanks @rafacpti23
- **docs(readme):** README touch-ups ([#12193](https://github.com/diegosouzapw/OmniRoute/pull/12193)) ([#12194](https://github.com/diegosouzapw/OmniRoute/pull/12194)) ([#12202](https://github.com/diegosouzapw/OmniRoute/pull/12202)) — thanks @rafacpti23
- **build:** force the webpack bundler in the contributor build profile ([#12197](https://github.com/diegosouzapw/OmniRoute/pull/12197)) — thanks @rafacpti23
- **build:** skip standalone packaging in the contributor profile ([#12198](https://github.com/diegosouzapw/OmniRoute/pull/12198)) — thanks @rafacpti23
- **docs(audit):** align every published number with the code and harden check:docs-counts ([#12200](https://github.com/diegosouzapw/OmniRoute/pull/12200))
- **build:** stub the instrumentation entrypoints in the contributor profile ([#12203](https://github.com/diegosouzapw/OmniRoute/pull/12203)) — thanks @rafacpti23
- **build:** omit the standalone output target for contributor builds ([#12204](https://github.com/diegosouzapw/OmniRoute/pull/12204)) — thanks @rafacpti23
- **chore(lint):** batch 6 of #12146 — memory, radar, audit, analytics, cache, usage, activity, home and RequestLoggerV2 react-hooks violations resolved ([#12208](https://github.com/diegosouzapw/OmniRoute/pull/12208))
- **docs(diagrams):** rename number-carrying diagram files to stable names ([#12210](https://github.com/diegosouzapw/OmniRoute/pull/12210))
- **docs(dashboard):** redraw onboarding tier-flow SVGs for the real 4-tier model ([#12211](https://github.com/diegosouzapw/OmniRoute/pull/12211))
- **docs(api):** document every implemented route in openapi.yaml (276 → 692 paths) ([#12212](https://github.com/diegosouzapw/OmniRoute/pull/12212))
- **chore(lint):** batch 7 of #12146 — final src tail: 37 react-hooks violations across 33 files resolved ([#12244](https://github.com/diegosouzapw/OmniRoute/pull/12244))
- Revert "docs: recommend gstack for AI-assisted workflows (#11770)" ([#12248](https://github.com/diegosouzapw/OmniRoute/pull/12248)) ([#12249](https://github.com/diegosouzapw/OmniRoute/pull/12249))
- **docs:** Chaos Mode setup guide + weighted strategy semantics ([#12250](https://github.com/diegosouzapw/OmniRoute/pull/12250))
- **docs(agents):** protected-surface merge rule — operator approval for agent-instruction files ([#12253](https://github.com/diegosouzapw/OmniRoute/pull/12253))
- **chore(quality):** register native-codex turn-pin tests in stryker tap.testFiles ([#12263](https://github.com/diegosouzapw/OmniRoute/pull/12263))
- **chore(quality):** dedupe tap.testFiles entries added by racing base-red fixes ([#12265](https://github.com/diegosouzapw/OmniRoute/pull/12265))
- **docs(readme):** introduce OmniRouteTray — the macOS menu-bar companion ([#12276](https://github.com/diegosouzapw/OmniRoute/pull/12276)) — thanks @ggiak
- **chore(proxy):** purge the legacy 1proxy residue — sync/rotator modules, dead DB exports, dead settings tab and flag, docs (#12091) ([#12290](https://github.com/diegosouzapw/OmniRoute/pull/12290))
- **chore(quality):** baseline-headroom skips generated and vendored files in the fileSize worst-file signal ([#12291](https://github.com/diegosouzapw/OmniRoute/pull/12291))
- **refactor(video):** unify the JPEG frame data-URI contract ([#12322](https://github.com/diegosouzapw/OmniRoute/pull/12322))
- **chore(deps):** freeze onnxruntime-node and eslint-plugin-react-hooks out of dependabot groups ([#12329](https://github.com/diegosouzapw/OmniRoute/pull/12329))
- **test(executors):** restore the #10986 reasoning-only fallback guards ([#12364](https://github.com/diegosouzapw/OmniRoute/pull/12364)) — thanks @pacocartones
- **chore(quality):** re-tighten the file-size ratchet to the real LOC (plan 3.8.52 task 0) ([#12411](https://github.com/diegosouzapw/OmniRoute/pull/12411))
- **test(grok):** format web executor suite ([#12412](https://github.com/diegosouzapw/OmniRoute/pull/12412))
- **chore(quality):** ratchet open-sse typecheck baseline to zero ([#12418](https://github.com/diegosouzapw/OmniRoute/pull/12418))
- **test(build):** guard the artifact path policy arrays against duplicates ([#12422](https://github.com/diegosouzapw/OmniRoute/pull/12422))
- **chore(lint):** adopt eslint-plugin-react-hooks 7.1.1 ([#12428](https://github.com/diegosouzapw/OmniRoute/pull/12428))
- **chore(providers):** remove a keyless provider integration at its operator's request ([#12440](https://github.com/diegosouzapw/OmniRoute/pull/12440))
- **docs(providers):** register providers removed at their operator's request and guard against reintroduction ([#12478](https://github.com/diegosouzapw/OmniRoute/pull/12478))
- **chore(quality):** base fixes — stryker tap.testFiles + node_modules cache key ([#12482](https://github.com/diegosouzapw/OmniRoute/pull/12482))
- **docs(arch):** one-process recipe for tens of long /v1/responses ([#12493](https://github.com/diegosouzapw/OmniRoute/pull/12493)) — thanks @RaviTharuma
- **chore(quality):** tighten the CodeQL ratchet baseline from 11 to 6 ([#12530](https://github.com/diegosouzapw/OmniRoute/pull/12530))
- **chore(deps):** pin browserslist override to ^4.28.8 ([#12592](https://github.com/diegosouzapw/OmniRoute/pull/12592)) — thanks @KooshaPari
- **chore(deps):** bump fast-uri to 3.1.7 in the electron lockfile ([#12601](https://github.com/diegosouzapw/OmniRoute/pull/12601))
- **docs(quality):** document how the CodeQL ratchet refreshes and how to tighten it ([#12611](https://github.com/diegosouzapw/OmniRoute/pull/12611))
- **test(ci):** pin the openapi-security-tiers two-arm contract with an executing gate test (#12581) ([#12652](https://github.com/diegosouzapw/OmniRoute/pull/12652))
- **docs(gamification):** align XP Rewards table with code (#12501) ([#12667](https://github.com/diegosouzapw/OmniRoute/pull/12667)) — thanks @KooshaPari
- **refactor(combo):** split executeTarget into gates, attempt, and loop ([#12746](https://github.com/diegosouzapw/OmniRoute/pull/12746)) — thanks @HouMinXi
- **docs(security):** document socket.yml scanner config + CI workflow link (#12575) ([#12764](https://github.com/diegosouzapw/OmniRoute/pull/12764)) — thanks @KooshaPari
- **chore(ci):** guard commit identity in pre-commit to stop author misattribution ([#12772](https://github.com/diegosouzapw/OmniRoute/pull/12772))
- **deps:** 17 Dependabot bumps — bump electron from 43.4.0 to 43.4.1 in /electron ([#11426](https://github.com/diegosouzapw/OmniRoute/pull/11426)); bump the development group across 1 directory with 8 updates ([#11428](https://github.com/diegosouzapw/OmniRoute/pull/11428)); bump the production group across 1 directory with 11 updates ([#11440](https://github.com/diegosouzapw/OmniRoute/pull/11440)); chore(deps): bump github/codeql-action/init from 4.37.7 to 4.37.8 ([#11925](https://github.com/diegosouzapw/OmniRoute/pull/11925)); chore(deps): bump github/codeql-action from 4.37.7 to 4.37.8 ([#11926](https://github.com/diegosouzapw/OmniRoute/pull/11926)); chore(deps): bump github/codeql-action/analyze from 4.37.7 to 4.37.8 ([#11927](https://github.com/diegosouzapw/OmniRoute/pull/11927)); build(deps): bump github/codeql-action/init from 4.37.8 to 4.37.9 ([#12345](https://github.com/diegosouzapw/OmniRoute/pull/12345)); build(deps): bump github/codeql-action/analyze from 4.37.8 to 4.37.9 ([#12346](https://github.com/diegosouzapw/OmniRoute/pull/12346)); bump the development group across 1 directory with 2 updates ([#12347](https://github.com/diegosouzapw/OmniRoute/pull/12347)); build(deps): bump github/codeql-action from 4.37.8 to 4.37.9 ([#12349](https://github.com/diegosouzapw/OmniRoute/pull/12349)); bump browserslist from 4.28.2 to 4.28.8 ([#12396](https://github.com/diegosouzapw/OmniRoute/pull/12396)); bump the production group across 1 directory with 4 updates ([#12399](https://github.com/diegosouzapw/OmniRoute/pull/12399)); bump @xmldom/xmldom ([#12500](https://github.com/diegosouzapw/OmniRoute/pull/12500)); bump qs from 6.15.2 to 6.16.0 ([#12512](https://github.com/diegosouzapw/OmniRoute/pull/12512)); bump @xmldom/xmldom from 0.9.10 to 0.9.12 ([#12513](https://github.com/diegosouzapw/OmniRoute/pull/12513)); bump fast-uri from 3.1.5 to 3.1.7 ([#12514](https://github.com/diegosouzapw/OmniRoute/pull/12514)); bump @humanfs/node from 0.16.7 to 0.16.8 ([#12515](https://github.com/diegosouzapw/OmniRoute/pull/12515))
### 🙌 Contributors
Thanks to everyone whose work landed in v3.8.51:
| Contributor | PRs / Issues |
| --- | --- |
| [@5dive-bot](https://github.com/5dive-bot) | #11852 |
| [@abhisheksharma2411](https://github.com/abhisheksharma2411) | #11547, #11684, #11959, #12209 |
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #11464 |
| [@adivekar-utexas](https://github.com/adivekar-utexas) | #12015, #12043, #12090, #12138 |
| [@AIB1TAL0S](https://github.com/AIB1TAL0S) | #12103 |
| [@alltomatos](https://github.com/alltomatos) | #11448 |
| [@alvinveroy](https://github.com/alvinveroy) | #11857, #12027, #12165 |
| [@amaleta](https://github.com/amaleta) | #11906, #11934 |
| [@amartinawi](https://github.com/amartinawi) | #11575 |
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #11083, #11830, #11834, #11844 |
| [@aniruddhaadak80](https://github.com/aniruddhaadak80) | #11760 |
| [@app](https://github.com/app) | #12554 |
| [@arminanton](https://github.com/arminanton) | #11461, #11513 |
| [@AStupidBear](https://github.com/AStupidBear) | #11584 |
| [@atescivitci-cmd](https://github.com/atescivitci-cmd) | #12105 |
| [@b3nw](https://github.com/b3nw) | #11970, #11971, #12147 |
| [@backryun](https://github.com/backryun) | #11259, #11950, #12075, #12076, #12078, #12079, #12081, #12082, #12181, #12228, #12239, #12255, #12258, #12277, #12367, #12423, #12524, #12538 |
| [@benjaminkitt](https://github.com/benjaminkitt) | #11747 |
| [@benzntech](https://github.com/benzntech) | #11614, #11615 |
| [@Bl0ck154](https://github.com/Bl0ck154) | #11948, #11951, #11952, #11953, #11954 |
| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #11752, #12242, #12262 |
| [@Chewji9875](https://github.com/Chewji9875) | #11455, #11875, #12028 |
| [@cheynetom](https://github.com/cheynetom) | #12058 |
| [@davidebaraldo](https://github.com/davidebaraldo) | #12222, #12304 |
| [@davidlinfr](https://github.com/davidlinfr) | #6390 |
| [@ddarkr](https://github.com/ddarkr) | #12124 |
| [@Deftera186](https://github.com/Deftera186) | #11809 |
| [@dpozimski](https://github.com/dpozimski) | #11945 |
| [@DW-MediaLab](https://github.com/DW-MediaLab) | #12229 |
| [@echel0nn](https://github.com/echel0nn) | #11923 |
| [@f9td56dbgh-hub](https://github.com/f9td56dbgh-hub) | #11660 |
| [@fabioluissilva](https://github.com/fabioluissilva) | #11991 |
| [@foreveryh](https://github.com/foreveryh) | #12177 |
| [@geek007git](https://github.com/geek007git) | #12115, #12116, #12117, #12120, #12122 |
| [@geekyNads](https://github.com/geekyNads) | #11784, #12266 |
| [@ggiak](https://github.com/ggiak) | #11561, #12276, #12350, #12402 |
| [@gonisulaimann](https://github.com/gonisulaimann) | #12368, #12369, #12371 |
| [@Gorillaz322](https://github.com/Gorillaz322) | #12207 |
| [@groovecityJO](https://github.com/groovecityJO) | #12682 |
| [@hartmark](https://github.com/hartmark) | #11434, #11452, #11473, #11499, #11703, #11983, #11984, #11985, #11986, #11988, #11989, #11990, #11994, #12221, #12293, #12445, #12446, #12447, #12448, #12460, #12461, #12623, #12646, #12650, #12680 |
| [@hizzt](https://github.com/hizzt) | #11894 |
| [@hongnoul](https://github.com/hongnoul) | #11484 |
| [@HouMinXi](https://github.com/HouMinXi) | #11411, #11414, #11512, #11518, #11520, #11641, #11642, #11643, #11687, #11779, #11849, #11850, #11851, #11915, #11916, #11918, #11919, #11920, #12013, #12017, #12033, #12042, #12106, #12139, #12166, #12169, #12171, #12205, #12213, #12312, #12325, #12487, #12488, #12495, #12504, #12557, #12566, #12590, #12591, #12624, #12626, #12632, #12637, #12678, #12696, #12697, #12711, #12733, #12746, #12767, #12770, #12789, #12803, #12805, #12811, #12866, #12868, #12899, #12926, #12934 |
| [@Hsia97](https://github.com/Hsia97) | #11624 |
| [@jacobsparts](https://github.com/jacobsparts) | #11854, #12155, #12167 |
| [@joglomedia](https://github.com/joglomedia) | #11980 |
| [@jonlwheat2-gif](https://github.com/jonlwheat2-gif) | #11443, #11450, #11549, #11567, #11608, #11633, #11635, #11640, #11644, #11671, #11672, #11673, #11674, #11675, #11676, #11677, #11706, #11783, #12051, #12052, #12053, #12055 |
| [@kanade-hoshino](https://github.com/kanade-hoshino) | #12180 |
| [@Karan825](https://github.com/Karan825) | #11861 |
| [@KaspaPulse](https://github.com/KaspaPulse) | #11389, #11469, #11628, #11666, #11910 |
| [@keeltrace](https://github.com/keeltrace) | #12080, #12223 |
| [@killer30001000](https://github.com/killer30001000) | #12178 |
| [@KooshaPari](https://github.com/KooshaPari) | #12470, #12592, #12667, #12699, #12703, #12706, #12764, #12769, #12771 |
| [@kriptoburak](https://github.com/kriptoburak) | #11370 |
| [@KrzysiekSko](https://github.com/KrzysiekSko) | #12425, #12432, #12673 |
| [@marcelokarval](https://github.com/marcelokarval) | direct commit / report |
| [@maxmad64bis](https://github.com/maxmad64bis) | #1622, #11435, #11437, #11441, #11537, #11550, #11553, #11555, #11557, #11812, #11842, #11843, #11903, #12151, #12214, #12215, #12218, #12226, #12314, #12316, #12317, #12318, #12319, #12320, #12321, #12870 |
| [@mdigitalbh81](https://github.com/mdigitalbh81) | #12240 |
| [@Meet6338-X](https://github.com/Meet6338-X) | #11598, #11609, #12110 |
| [@morpheus9393](https://github.com/morpheus9393) | #11943 |
| [@MumuTW](https://github.com/MumuTW) | #11492, #11502, #11506, #11507, #11626, #11675, #11685, #11728, #11746, #11888, #11889, #11890, #11892 |
| [@Neuron-Mr-White](https://github.com/Neuron-Mr-White) | #11622, #11801, #12256 |
| [@NightStalker-87](https://github.com/NightStalker-87) | #12183 |
| [@NoxzRCW](https://github.com/NoxzRCW) | #11879, #11880, #11881, #11882, #11883, #11935 |
| [@ntdat812](https://github.com/ntdat812) | #11585, #12095 |
| [@ntdatt812](https://github.com/ntdatt812) | #11368, #11573, #11574, #11576, #11577, #11580, #11582, #11583, #11588, #11589, #11590, #11591, #11592, #11593, #11672, #12177, #12180 |
| [@official-burak](https://github.com/official-burak) | #11542 |
| [@opensource-elearning](https://github.com/opensource-elearning) | #12179, #12189, #12278, #12286 |
| [@oyi77](https://github.com/oyi77) | #11408, #11409, #11421, #11505, #11677, #12036, #12110 |
| [@pacocartones](https://github.com/pacocartones) | #11521, #11522, #11527, #11528, #11529, #11530, #11531, #11532, #11533, #11534, #11595, #11599, #11603, #11604, #11605, #11607, #11610, #11676, #11714, #11716, #11718, #11767, #11838, #11860, #11862, #11869, #11871, #11872, #11873, #11903, #11906, #11921, #11934, #12359, #12360, #12361, #12362, #12364, #12365, #12373, #12374, #12375, #12376, #12377, #12379, #12380, #12381, #12386, #12387, #12389, #12390, #12394, #12395, #12397, #12401, #12403, #12404 |
| [@patrykkopycinski](https://github.com/patrykkopycinski) | #11936, #11937, #12224 |
| [@PauloFH](https://github.com/PauloFH) | #11509 |
| [@PauloHSOliveira](https://github.com/PauloHSOliveira) | #12241 |
| [@ponkcore](https://github.com/ponkcore) | #12054 |
| [@Prajeeth-12](https://github.com/Prajeeth-12) | #11634, #12046 |
| [@quiterunner-commits](https://github.com/quiterunner-commits) | #12131, #12143 |
| [@rafacpti23](https://github.com/rafacpti23) | #11554, #11558, #12192, #12197, #12198, #12202, #12203, #12204 |
| [@ragnar-claude](https://github.com/ragnar-claude) | #11564 |
| [@raheemuddin786](https://github.com/raheemuddin786) | #11491, #11839, #11840, #11841, #12003, #12230, #12231, #12232, #12233, #12234 |
| [@rahilmavani](https://github.com/rahilmavani) | #11761 |
| [@Rahulsharma0810](https://github.com/Rahulsharma0810) | #11771 |
| [@RaviTharuma](https://github.com/RaviTharuma) | #11710, #11727, #11797, #11798, #11802, #11805, #11806, #11811, #12098, #12099, #12101, #12449, #12452, #12472, #12473, #12493, #12533, #12607, #12628, #12631, #12636 |
| [@rezjalibd](https://github.com/rezjalibd) | #12186 |
| [@RhianB14](https://github.com/RhianB14) | #11692 |
| [@rifqiawl](https://github.com/rifqiawl) | #11517, #11519, #11969 |
| [@rqzbeh](https://github.com/rqzbeh) | #11390 |
| [@Sabeekhann](https://github.com/Sabeekhann) | #11699, #11705 |
| [@santosraju99-hub](https://github.com/santosraju99-hub) | #11755, #11770 |
| [@Seramicx](https://github.com/Seramicx) | #11568 |
| [@Siva010](https://github.com/Siva010) | #12191 |
| [@solstxce](https://github.com/solstxce) | #11597 |
| [@soroush5](https://github.com/soroush5) | #12691 |
| [@tenshiak](https://github.com/tenshiak) | #12279 |
| [@TheDemonTuan](https://github.com/TheDemonTuan) | #11468, #11470, #11471, #11482, #11548, #11758, #11775, #11814 |
| [@thomasmaerz](https://github.com/thomasmaerz) | #12834 |
| [@tuandinh0801](https://github.com/tuandinh0801) | #11454 |
| [@turbolego](https://github.com/turbolego) | #11621, #11762, #11772, #11774, #11781, #12216 |
| [@Tushar49](https://github.com/Tushar49) | #11392 |
| [@ujjawalkaushik1110](https://github.com/ujjawalkaushik1110) | #11863 |
| [@vermasomesh835](https://github.com/vermasomesh835) | #11794 |
| [@vsd2807](https://github.com/vsd2807) | #11565, #11619 |
| [@wahidsadik371-coder](https://github.com/wahidsadik371-coder) | #12126 |
| [@watchingdogs](https://github.com/watchingdogs) | #12031 |
| [@wildcard](https://github.com/wildcard) | #11620 |
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #11460, #11662, #12083 |
| [@Xxx91n](https://github.com/Xxx91n) | #11690 |
| [@yourspraveen](https://github.com/yourspraveen) | #11146 |
| [@yxyxy](https://github.com/yxyxy) | #11671 |
| [@ZaimMarzuki](https://github.com/ZaimMarzuki) | #11960 |
| [@Zartharas](https://github.com/Zartharas) | #11340 |
| [@zero-executioner](https://github.com/zero-executioner) | #11631 |
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
---
## [3.8.50] — 2026-08-25
_Living section — regenerated 2026-08-12 from all cycle commits (cycle open `ed2db6cb19` → tip). Bullets carry the merged PR and its author; direct pushes listed separately._

View File

@@ -227,6 +227,18 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
ENV DATA_DIR=/app/data
RUN mkdir -p /app/data
# #13679: default the PUBLISHED image to requiring an API key. A bare
# `docker run -p 20128:20128 … diegosouzapw/omniroute` (README/QUICK-START
# one-liners) does not pass `--env-file .env`, so without this default the
# anonymous /v1 LLM proxy would be both keyless AND world-reachable on the
# published container. This does NOT change the npm/CLI local-dev default
# (`REQUIRE_API_KEY` stays `"false"` in featureFlagDefinitions.ts) — only the
# shipped deployment artifact's posture. docker-compose.yml is unaffected: it
# loads the operator's own `.env` (env_file:) which overrides this ENV, and
# already binds loopback-only by default (#12568). Override with
# `-e REQUIRE_API_KEY=false` for an intentionally keyless deployment.
ENV REQUIRE_API_KEY=true
# `npm run build` (build-next-isolated → assembleStandalone) bundles ALL runtime
# files into .build/next/standalone/ — .next, node_modules, migrations, scripts,
# docs, and the previously hand-COPY'd modules below (@swc/helpers, pino-*, split2,
@@ -340,7 +352,7 @@ RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,targe
# build, not the floating `@latest`.
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
npm install -g --no-audit --no-fund \
@openai/codex@0.153.2 \
@openai/codex@0.153.4 \
@anthropic-ai/claude-code@2.1.260 \
droid@0.212.0 \
openclaw@2026.9.1

View File

@@ -7,7 +7,7 @@
# 🚀 OmniRoute — The Free AI Gateway
<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 1595% tokens (~89% avg) — never hit limits. 356 AI providers · 150+ free tiers · ~1.47B 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 → 359 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 1595% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
@@ -17,9 +17,9 @@
</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 **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`).
> 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 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.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."/>
<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.07B in the first month with signup credits, from 34 documented recurring pool keys covering 446 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)**.
>
@@ -133,7 +133,7 @@
</div>
<div align="center">
<b>🌐 In 42 languages</b>
<b>🌐 In 66 languages</b>
<br/><br/>
<a href="README.md"><img src="docs/assets/flags/us.svg" width="30" alt="English (en)" title="English (en)"></a>
<a href="docs/i18n/pt-BR/README.md"><img src="docs/assets/flags/br.svg" width="30" alt="Português — Brasil (pt-BR)" title="Português — Brasil (pt-BR)"></a>
@@ -177,6 +177,30 @@
<a href="docs/i18n/tr/README.md"><img src="docs/assets/flags/tr.svg" width="30" alt="Türkçe (tr)" title="Türkçe (tr)"></a>
<a href="docs/i18n/az/README.md"><img src="docs/assets/flags/az.svg" width="30" alt="Azərbaycan (az)" title="Azərbaycan (az)"></a>
<a href="docs/i18n/sw/README.md"><img src="docs/assets/flags/tz.svg" width="30" alt="Kiswahili (sw)" title="Kiswahili (sw)"></a>
<a href="docs/i18n/el/README.md"><img src="docs/assets/flags/gr.svg" width="30" alt="Ελληνικά (el)" title="Ελληνικά (el)"></a>
<a href="docs/i18n/hr/README.md"><img src="docs/assets/flags/hr.svg" width="30" alt="Hrvatski (hr)" title="Hrvatski (hr)"></a>
<a href="docs/i18n/sr/README.md"><img src="docs/assets/flags/rs.svg" width="30" alt="Српски (sr)" title="Српски (sr)"></a>
<a href="docs/i18n/lt/README.md"><img src="docs/assets/flags/lt.svg" width="30" alt="Lietuvių (lt)" title="Lietuvių (lt)"></a>
<a href="docs/i18n/et/README.md"><img src="docs/assets/flags/ee.svg" width="30" alt="Eesti (et)" title="Eesti (et)"></a>
<a href="docs/i18n/lv/README.md"><img src="docs/assets/flags/lv.svg" width="30" alt="Latviešu (lv)" title="Latviešu (lv)"></a>
<a href="docs/i18n/sl/README.md"><img src="docs/assets/flags/si.svg" width="30" alt="Slovenščina (sl)" title="Slovenščina (sl)"></a>
<a href="docs/i18n/mt/README.md"><img src="docs/assets/flags/mt.svg" width="30" alt="Malti (mt)" title="Malti (mt)"></a>
<a href="docs/i18n/ga/README.md"><img src="docs/assets/flags/ie.svg" width="30" alt="Gaeilge (ga)" title="Gaeilge (ga)"></a>
<a href="docs/i18n/kn/README.md"><img src="docs/assets/flags/in.svg" width="30" alt="ಕನ್ನಡ (kn)" title="ಕನ್ನಡ (kn)"></a>
<a href="docs/i18n/ml/README.md"><img src="docs/assets/flags/in.svg" width="30" alt="മലയാളം (ml)" title="മലയാളം (ml)"></a>
<a href="docs/i18n/or/README.md"><img src="docs/assets/flags/in.svg" width="30" alt="ଓଡ଼ିଆ (or)" title="ଓଡ଼ିଆ (or)"></a>
<a href="docs/i18n/pa/README.md"><img src="docs/assets/flags/in.svg" width="30" alt="ਪੰਜਾਬੀ (pa)" title="ਪੰਜਾਬੀ (pa)"></a>
<a href="docs/i18n/ne/README.md"><img src="docs/assets/flags/np.svg" width="30" alt="नेपाली (ne)" title="नेपाली (ne)"></a>
<a href="docs/i18n/si/README.md"><img src="docs/assets/flags/lk.svg" width="30" alt="සිංහල (si)" title="සිංහල (si)"></a>
<a href="docs/i18n/my/README.md"><img src="docs/assets/flags/mm.svg" width="30" alt="မြန်မာ (my)" title="မြန်မာ (my)"></a>
<a href="docs/i18n/km/README.md"><img src="docs/assets/flags/kh.svg" width="30" alt="ខ្មែរ (km)" title="ខ្មែរ (km)"></a>
<a href="docs/i18n/ha/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Hausa (ha)" title="Hausa (ha)"></a>
<a href="docs/i18n/yo/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Yorùbá (yo)" title="Yorùbá (yo)"></a>
<a href="docs/i18n/ig/README.md"><img src="docs/assets/flags/ng.svg" width="30" alt="Igbo (ig)" title="Igbo (ig)"></a>
<a href="docs/i18n/am/README.md"><img src="docs/assets/flags/et.svg" width="30" alt="አማርኛ (am)" title="አማርኛ (am)"></a>
<a href="docs/i18n/uz/README.md"><img src="docs/assets/flags/uz.svg" width="30" alt="Oʻzbekcha (uz)" title="Oʻzbekcha (uz)"></a>
<a href="docs/i18n/ka/README.md"><img src="docs/assets/flags/ge.svg" width="30" alt="ქართული (ka)" title="ქართული (ka)"></a>
<a href="docs/i18n/hy/README.md"><img src="docs/assets/flags/am.svg" width="30" alt="Հայերեն (hy)" title="Հայերեն (hy)"></a>
</div>
<br/>
@@ -209,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \
</div>
<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."/>
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 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."/>
<br/>
<br/>
@@ -462,7 +486,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: 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."/>
<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: 359 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 &amp; per-feature detail vs 9router, OpenRouter, CLIProxyAPI &amp; LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
@@ -520,7 +544,7 @@ Pix copia-e-cola:
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.10B**. Radar is an optional, signed catalog overlay for people who want fresher
month to **~2.07B**. 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.
@@ -648,7 +672,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 **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).
> **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 **443 per-model rows**, **34 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).
<div align="center">
@@ -1244,7 +1268,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>&gt;=22.22.2 &lt;23 || &gt;=24.0.0 &lt;27</code></td></tr>
<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>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 169 migrations</td></tr>
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 176 migrations</td></tr>
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
@@ -1307,7 +1331,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>16-factor scoring, mode packs, self-healing</td></tr>
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 34 documented recurring pools / 444 cataloged free-tier entries</td></tr>
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 34 documented recurring pools / 446 cataloged free-tier entries</td></tr>
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
</table>

View File

@@ -13,6 +13,7 @@ const OMNIROUTE_ENV_VARS = [
"OMNIROUTE_API_KEY",
"OMNIROUTE_BASE_URL",
"OMNIROUTE_HTTP_TIMEOUT_MS",
"OMNIROUTE_READY_TIMEOUT_MS",
];
const ENV_DEFAULTS = {

View File

@@ -9,6 +9,8 @@ function truncate(v, len = 60) {
return s.length > len ? s.slice(0, len - 1) + "…" : s;
}
const VALID_MCP_TRANSPORTS = ["stdio", "sse", "streamable-http"];
const mcpToolSchema = [
{ key: "name", header: "Tool", width: 36 },
{
@@ -43,6 +45,25 @@ export function registerMcp(program) {
if (exitCode !== 0) process.exit(exitCode);
});
mcp
.command("enable")
.description(t("mcp.enable.description"))
.option("--transport <transport>", t("mcp.enable.transport"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runMcpEnableCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
mcp
.command("disable")
.description(t("mcp.disable.description"))
.action(async (opts, cmd) => {
const globalOpts = cmd.parent.optsWithGlobals();
const exitCode = await runMcpDisableCommand({ ...opts, output: globalOpts.output });
if (exitCode !== 0) process.exit(exitCode);
});
// 5.1 — mcp call + mcp scopes
mcp
.command("call <tool> [argsJson]")
@@ -61,10 +82,15 @@ export function registerMcp(program) {
? JSON.parse(argsPositional)
: {};
const exitCode = await runMcpCallCommand(tool, args, {
...opts,
stream: opts.stream,
}, globalOpts);
const exitCode = await runMcpCallCommand(
tool,
args,
{
...opts,
stream: opts.stream,
},
globalOpts
);
if (exitCode !== 0) process.exit(exitCode);
});
@@ -127,7 +153,9 @@ async function mcpJsonRpcCall(tool, args, { stream = false, globalOpts = {} } =
if (!initRes.ok) {
const text = await initRes.text().catch(() => "");
process.stderr.write(`MCP initialize failed: HTTP ${initRes.status}${text ? `${text}` : ""}\n`);
process.stderr.write(
`MCP initialize failed: HTTP ${initRes.status}${text ? `${text}` : ""}\n`
);
return 1;
}
@@ -227,6 +255,7 @@ export async function runMcpStatusCommand(opts = {}) {
});
if (!res.ok) {
console.log(t("mcp.stopped"));
console.log(t("mcp.stoppedHint"));
return 0;
}
@@ -240,6 +269,9 @@ export async function runMcpStatusCommand(opts = {}) {
const transport = status.transport || "stdio";
const online = status.online ?? status.running;
console.log(online ? t("mcp.running", { transport }) : t("mcp.stopped"));
if (!online && status.enabled === false) {
console.log(t("mcp.stoppedHint"));
}
if (status.toolsCount !== undefined) console.log(` Tools: ${status.toolsCount}`);
if (status.scopes?.length) {
console.log(" Scopes:");
@@ -270,10 +302,76 @@ export async function runMcpRestartCommand(opts = {}) {
console.log(t("mcp.restarted"));
return 0;
}
console.error(t("common.error", { message: `HTTP ${res.status}` }));
const body = await res.json().catch(() => null);
const message = body?.error || `HTTP ${res.status}`;
console.error(t("common.error", { message }));
return 1;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runMcpEnableCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
if (opts.transport && !VALID_MCP_TRANSPORTS.includes(opts.transport)) {
console.error(
t("common.error", {
message: `Invalid transport '${opts.transport}'. Valid: ${VALID_MCP_TRANSPORTS.join(", ")}`,
})
);
return 1;
}
try {
const body = { mcpEnabled: true };
if (opts.transport) body.mcpTransport = opts.transport;
const res = await apiFetch("/api/settings", {
method: "PATCH",
body,
retry: false,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("mcp.enabled"));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}
export async function runMcpDisableCommand(opts = {}) {
const serverUp = await isServerUp();
if (!serverUp) {
console.error(t("common.serverOffline"));
return 1;
}
try {
const res = await apiFetch("/api/settings", {
method: "PATCH",
body: { mcpEnabled: false },
retry: false,
acceptNotOk: true,
});
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
}
console.log(t("mcp.disabled"));
return 0;
} catch (err) {
console.error(t("common.error", { message: err instanceof Error ? err.message : String(err) }));
return 1;
}
}

View File

@@ -54,6 +54,34 @@ async function openBrowser(url) {
}
}
// Mirrors src/lib/oauth/providers.ts::isLoopbackHostname — used here to detect
// when the redirect_uri the server resolved (and the authorize URL now
// advertises) points at a loopback address the CLI never binds a listener on
// (issue #12413). Returns false on an unparseable URI rather than throwing.
function isLoopbackHost(uri) {
try {
return /^(localhost|127\.0\.0\.1|\[::1\]|::1)$/i.test(new URL(uri).hostname);
} catch {
return false;
}
}
function printLoopbackRedirectWarning(providerId, redirectUri) {
process.stdout.write(
`Note: the authorize URL below advertises ${redirectUri}, but this CLI does not\n` +
"listen on that port. Right after you approve, the browser is expected to\n" +
"show a connection error (e.g. \"This site can't be reached\" / \n" +
"ERR_CONNECTION_REFUSED) — that is normal, not a failure. Copy the full URL\n" +
"from the address bar anyway and paste it below.\n"
);
if (providerId === "antigravity") {
process.stdout.write(
"Tip: `omniroute login antigravity` captures the code automatically and\n" +
"avoids that error page entirely.\n"
);
}
}
function targetApiOptions(opts = {}) {
return {
baseUrl: opts.baseUrl,
@@ -110,6 +138,10 @@ async function runBrowserFlow(def, opts) {
const { codeVerifier, state, redirectUri: returnedRedirectUri } = start;
const finalRedirectUri = returnedRedirectUri || redirectUri;
if (finalRedirectUri && isLoopbackHost(finalRedirectUri)) {
printLoopbackRedirectWarning(def.id, finalRedirectUri);
}
process.stdout.write(`\nOpen this URL to authorize:\n ${url}\n\n`);
if (opts.browser !== false) await openBrowser(url);
process.stdout.write(

View File

@@ -4,7 +4,7 @@ import { join, dirname } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { platform, totalmem } from "node:os";
import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import { writePidFile, cleanupPidFile, waitForServer, resolveReadyTimeoutMs } from "../utils/pid.mjs";
import {
ServerSupervisor,
detectMitmCrash,
@@ -58,6 +58,11 @@ export function registerServe(program) {
.option("--max-restarts <n>", t("serve.max_restarts"), parseInt, 2)
.option("--tray", t("serve.tray") || "Start in the system tray (desktop only)")
.option("--no-tray", t("serve.no_tray") || "Disable system tray icon")
.option(
"--ready-timeout <ms>",
t("serve.ready_timeout") ||
"Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)"
)
.option(
"--tls-cert <path>",
t("serve.tls_cert") ||
@@ -452,7 +457,8 @@ async function runWithSupervisor(
});
if (!showLog) {
waitForServer(dashboardPort, 60000).then(async (up) => {
const readyTimeoutMs = resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout });
waitForServer(dashboardPort, readyTimeoutMs).then(async (up) => {
if (up) {
if (useTray) {
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
@@ -489,10 +495,15 @@ async function runWithSupervisor(
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
// plus whatever stdout/stderr the child buffered instead of going silent.
export function reportReadinessTimeout(dashboardPort, supervisor) {
const readyTimeoutMs = resolveReadyTimeoutMs();
const seconds = Math.round(readyTimeoutMs / 1000);
console.error(
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
`\n\x1b[33m⚠ Server did not respond within ${seconds}s.\x1b[0m It may still be starting, or may` +
` have failed silently.`
);
console.error(
` Tip: set OMNIROUTE_READY_TIMEOUT_MS=${readyTimeoutMs * 2} or --ready-timeout ${readyTimeoutMs * 2} for slower cold starts.`
);
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);

View File

@@ -35,16 +35,24 @@ export function resolveOpencodeTarget(opts = {}) {
baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
}
// Precedence: explicit --api-key flag > OMNIROUTE_API_KEY env var > active
// context's management token. A context's accessToken/apiKey is a CLI
// management credential (oma_live_...) with no /v1/* inference scope — it
// must never silently outrank a real inference key the caller supplied
// either as a flag or via the ambient env var (mirrors the explicit >
// ambient-env > context precedence documented in bin/cli/api.mjs's
// buildHeaders()). Only fall back to the context token when neither an
// explicit flag nor the env var is set.
let apiKey = opts.apiKey ?? opts["api-key"];
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
if (!apiKey) {
try {
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
apiKey = c?.accessToken || c?.apiKey;
apiKey = c?.accessToken || c?.apiKey || "";
} catch {
/* no context auth */
}
}
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
return { baseUrl: baseUrl.replace(/\/+$/, ""), apiKey };
}
@@ -177,8 +185,17 @@ export function registerSetupOpencode(program) {
"--allow-container-write",
"Write even when the target is inside a container and not mounted from the host"
)
.action(async (opts) => {
const code = await runSetupOpencodeCommand(opts);
.action(async (opts, cmd) => {
// Commander parses the ancestor program's own global --api-key option
// (bin/cli/program.mjs, bound to .env("OMNIROUTE_API_KEY")) against any
// occurrence of the flag in argv, so it wins the value even when the
// user typed --api-key AFTER `setup-opencode` — this local option's own
// `opts.apiKey` never sees it. cmd.optsWithGlobals() resolves to the
// correct value either way ("globals overwrite locals" is exactly the
// outcome we want here, since the global option is where the value
// always actually lands).
const resolvedOpts = { ...opts, apiKey: cmd.optsWithGlobals().apiKey ?? opts.apiKey };
const code = await runSetupOpencodeCommand(resolvedOpts);
if (code !== 0) process.exit(code);
});
}

1340
bin/cli/locales/am.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/el.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -256,6 +256,7 @@
"max_restarts": "Max crash restarts within 30s before giving up (default: 2)",
"tray": "Start in the system tray (desktop only, opt-in)",
"no_tray": "Disable system tray icon",
"ready_timeout": "Readiness probe timeout in ms (also OMNIROUTE_READY_TIMEOUT_MS, default 60000)",
"tls_cert": "Path to a TLS certificate (PEM) to serve HTTPS (also OMNIROUTE_TLS_CERT)",
"tls_key": "Path to the TLS private key (PEM) to serve HTTPS (also OMNIROUTE_TLS_KEY)"
},
@@ -347,6 +348,16 @@
"running": "MCP server running ({transport})",
"stopped": "MCP server stopped.",
"restarted": "MCP server restarted.",
"stoppedHint": "Run `omniroute mcp enable` to turn it on.",
"enabled": "MCP server enabled.",
"disabled": "MCP server disabled.",
"enable": {
"description": "Enable the MCP server",
"transport": "Transport to use: stdio|sse|streamable-http"
},
"disable": {
"description": "Disable the MCP server"
},
"call": {
"description": "Invoke an MCP tool directly",
"args": "JSON arguments object (inline)",

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/et.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ga.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ha.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/hr.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/hy.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ig.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ka.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/km.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/kn.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/lt.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/lv.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ml.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/mt.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/my.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/ne.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/or.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/pa.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -30,7 +30,7 @@
"opencode": "Instala e configura o plugin @omniroute/opencode-plugin incluído para o OpenCode"
},
"doctor": {
"title": "OmniRoute Doctor",
"title": "Diagnóstico do OmniRoute",
"dbOk": "Banco de dados: OK ({path})",
"dbMissing": "Banco de dados: não inicializado — execute `omniroute setup`",
"portOk": "Porta {port}: disponível",
@@ -256,6 +256,7 @@
"max_restarts": "Máximo de reinícios em 30s antes de desistir (padrão: 2)",
"tray": "Mostrar ícone na bandeja do sistema (apenas desktop, opt-in)",
"no_tray": "Desabilitar ícone na bandeja do sistema",
"ready_timeout": "Tempo limite da verificação de prontidão em ms (também OMNIROUTE_READY_TIMEOUT_MS, padrão 60000)",
"tls_cert": "Caminho para um certificado TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_CERT)",
"tls_key": "Caminho para a chave privada TLS (PEM) para servir HTTPS (também OMNIROUTE_TLS_KEY)"
},
@@ -301,7 +302,7 @@
"noServer": "Servidor não está em execução. Inicie com: omniroute serve",
"title": "Saúde",
"status": "Status: {status}",
"uptime": "Uptime: {uptime}",
"uptime": "Tempo de atividade: {uptime}",
"requests": "Requisições (24h): {count}",
"cost": "Custo (24h): ${cost}"
},
@@ -344,6 +345,19 @@
},
"mcp": {
"title": "Servidor MCP",
"running": "Servidor MCP em execução ({transport})",
"stopped": "Servidor MCP parado.",
"restarted": "Servidor MCP reiniciado.",
"stoppedHint": "Execute `omniroute mcp enable` para ativá-lo.",
"enabled": "Servidor MCP ativado.",
"disabled": "Servidor MCP desativado.",
"enable": {
"description": "Ativar o servidor MCP",
"transport": "Transporte a ser usado: stdio|sse|streamable-http"
},
"disable": {
"description": "Desativar o servidor MCP"
},
"call": {
"description": "Invocar uma ferramenta MCP diretamente",
"args": "Objeto JSON de argumentos (inline)",
@@ -371,10 +385,7 @@
},
"audit": {
"description": "Log de auditoria MCP (alias para audit --source mcp)"
},
"running": "Servidor MCP em execução ({transport})",
"stopped": "Servidor MCP parado.",
"restarted": "Servidor MCP reiniciado."
}
},
"a2a": {
"skills": {
@@ -1093,7 +1104,7 @@
}
},
"combo": {
"title": "Combos",
"title": "Combinações",
"switched": "Combo ativo: {name}",
"created": "Combo criado: {name}",
"deleted": "Combo removido: {name}",
@@ -1268,7 +1279,7 @@
"description": "REPL interativo multi-turn com LLM",
"model": "Modelo a usar (padrão: auto)",
"combo": "Nome do combo a usar",
"system": "System prompt",
"system": "Prompt do sistema",
"resume": "Retomar sessão salva pelo nome"
},
"plugin": {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/si.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/sl.json Normal file

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/sr.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/uz.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1340
bin/cli/locales/yo.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -14,18 +14,6 @@
"jsonOpt": "以 JSON 格式输出",
"yesOpt": "跳过确认"
},
"program": {
"description": "OmniRoute — 具有自动故障转移的智能 AI 路由器",
"version": "打印版本并退出",
"output": "输出格式table, json, jsonl, csv",
"quiet": "禁止非必要输出",
"no_color": "禁用彩色输出",
"timeout": "HTTP 请求超时(毫秒)",
"api_key": "OmniRoute 服务器的 API 密钥",
"base_url": "OmniRoute 服务器的基础 URL",
"context": "此命令使用的服务器上下文/配置文件",
"lang": "设置 CLI 显示语言(覆盖 OMNIROUTE_LANG"
},
"setup": {
"title": "OmniRoute 设置",
"passwordPrompt": "管理员密码",
@@ -145,6 +133,20 @@
"listTitle": "{days} 天内过期的密钥:"
}
},
"authExport": {
"description": "导出已解密的提供者凭据(仅限本地,明文输出)",
"idOpt": "仅导出与此 id/名称/提供者匹配的连接",
"formatOpt": "输出格式json 或 env",
"outOpt": "将输出写入文件而非标准输出(以 0600 权限写入)",
"forceOpt": "确认你了解此操作会打印/写入明文密钥",
"warning": "⚠ 此操作会打印/写入已解密的明文 API 密钥和 OAuth 令牌。请确保你的屏幕、shell 历史记录以及任何输出文件保持私密。",
"confirmHeading": "⚠ 警告:此操作会以明文导出已解密的提供者凭据",
"confirmBody": "此命令会为所选连接解密并打印/写入 apiKey、accessToken、refreshToken 和\nidToken。请将输出视为机密。",
"confirmFooter": "如需确认,请运行:\n omniroute auth export --force",
"missingKey": "导出凭据需要 STORAGE_ENCRYPTION_KEY。",
"notFound": "未找到连接:{id}",
"invalidFormat": "无效格式:{format}。请使用 json 或 env。"
},
"stream": {
"description": "使用 SSE 检查模式流式传输聊天响应",
"file": "从文件读取提示",
@@ -254,6 +256,7 @@
"max_restarts": "30 秒内的最大崩溃重启次数默认2",
"tray": "显示系统托盘图标(仅桌面,选择加入)",
"no_tray": "禁用系统托盘图标",
"ready_timeout": "就绪探测超时(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS默认 60000",
"tls_cert": "用于提供 HTTPS 服务的 TLS 证书PEM路径也可用 OMNIROUTE_TLS_CERT",
"tls_key": "用于提供 HTTPS 服务的 TLS 私钥PEM路径也可用 OMNIROUTE_TLS_KEY"
},
@@ -301,7 +304,7 @@
"status": "状态:{status}",
"uptime": "运行时间:{uptime}",
"requests": "请求数24h{count}",
"cost": "成本24h"
"cost": "成本24h${cost}"
},
"quota": {
"description": "显示提供者配额使用情况",
@@ -345,6 +348,16 @@
"running": "MCP 服务器正在运行({transport}",
"stopped": "MCP 服务器已停止。",
"restarted": "MCP 服务器已重启。",
"stoppedHint": "运行 `omniroute mcp enable` 以启用它。",
"enabled": "MCP 服务器已启用。",
"disabled": "MCP 服务器已禁用。",
"enable": {
"description": "启用 MCP 服务器",
"transport": "要使用的传输方式stdio|sse|streamable-http"
},
"disable": {
"description": "禁用 MCP 服务器"
},
"call": {
"description": "直接调用 MCP 工具",
"args": "JSON 参数对象(内联)",
@@ -807,6 +820,18 @@
"event": "要模拟的事件类型默认request.completed"
}
},
"program": {
"description": "OmniRoute — 具有自动故障转移的智能 AI 路由器",
"version": "打印版本并退出",
"output": "输出格式table, json, jsonl, csv",
"quiet": "禁止非必要输出",
"no_color": "禁用彩色输出",
"timeout": "HTTP 请求超时(毫秒)",
"api_key": "OmniRoute 服务器的 API 密钥",
"base_url": "OmniRoute 服务器的基础 URL",
"context": "此命令使用的服务器上下文/配置文件",
"lang": "设置 CLI 显示语言(覆盖 OMNIROUTE_LANG"
},
"files": {
"description": "管理文件(上传、列出、获取、下载、删除)",
"list": {
@@ -907,6 +932,11 @@
"model": "按模型筛选"
}
},
"radar": {
"description": "检查并同步本地 Radar 目录订阅源",
"status": "显示本地 Radar 设置和订阅源缓存状态",
"sync": "通过本地服务器同步目录、推荐、优惠和 Intel"
},
"resilience": {
"description": "检查和管理弹性机制",
"status": {
@@ -1234,8 +1264,8 @@
"description": "管理 OmniRoute 开机自启Linuxsystemd 用户服务)",
"enable": "启用开机自启",
"disable": "禁用开机自启",
"status": "显示自启状态",
"toggle": "切换开机自启"
"toggle": "切换开机自启",
"status": "显示自启状态"
},
"runtime": {
"description": "管理本地运行时依赖",
@@ -1262,25 +1292,6 @@
"update": "更新已安装的插件",
"scaffold": "搭建新的插件模板"
},
"authExport": {
"description": "导出已解密的提供者凭据(仅限本地,明文输出)",
"idOpt": "仅导出与此 id/名称/提供者匹配的连接",
"formatOpt": "输出格式json 或 env",
"outOpt": "将输出写入文件而非标准输出(以 0600 权限写入)",
"forceOpt": "确认你了解此操作会打印/写入明文密钥",
"warning": "⚠ 此操作会打印/写入已解密的明文 API 密钥和 OAuth 令牌。请确保你的屏幕、shell 历史记录以及任何输出文件保持私密。",
"confirmHeading": "⚠ 警告:此操作会以明文导出已解密的提供者凭据",
"confirmBody": "此命令会为所选连接解密并打印/写入 apiKey、accessToken、refreshToken 和\nidToken。请将输出视为机密。",
"confirmFooter": "如需确认,请运行:\n omniroute auth export --force",
"missingKey": "导出凭据需要 STORAGE_ENCRYPTION_KEY。",
"notFound": "未找到连接:{id}",
"invalidFormat": "无效格式:{format}。请使用 json 或 env。"
},
"radar": {
"description": "检查并同步本地 Radar 目录订阅源",
"status": "显示本地 Radar 设置和订阅源缓存状态",
"sync": "通过本地服务器同步目录、推荐、优惠和 Intel"
},
"launch": {
"description": "启动指向 OmniRoute 的 Claude Code本地或远程使用 --profile",
"token": "Claude 客户端应发送的令牌ANTHROPIC_AUTH_TOKEN",

View File

@@ -14,18 +14,6 @@
"jsonOpt": "以 JSON 格式輸出",
"yesOpt": "跳過確認"
},
"program": {
"description": "OmniRoute — 具有自動故障轉移的智慧 AI 路由器",
"version": "列印版本並退出",
"output": "輸出格式table, json, jsonl, csv",
"quiet": "禁止非必要輸出",
"no_color": "停用彩色輸出",
"timeout": "HTTP 請求超時(毫秒)",
"api_key": "OmniRoute 伺服器的 API 金鑰",
"base_url": "OmniRoute 伺服器的基礎 URL",
"context": "此命令使用的伺服器上下文/配置檔案",
"lang": "設定 CLI 顯示語言(覆蓋 OMNIROUTE_LANG"
},
"setup": {
"title": "OmniRoute 設定",
"passwordPrompt": "管理員密碼",
@@ -145,6 +133,20 @@
"listTitle": "{days} 天內過期的金鑰:"
}
},
"authExport": {
"description": "匯出已解密的提供者憑據(僅限本機,明文輸出)",
"idOpt": "僅匯出與此 id/名稱/提供者相符的連線",
"formatOpt": "輸出格式json 或 env",
"outOpt": "將輸出寫入檔案而非標準輸出(以 0600 權限寫入)",
"forceOpt": "確認你了解此操作會列印/寫入明文密鑰",
"warning": "⚠ 此操作會列印/寫入已解密的明文 API 金鑰和 OAuth 令牌。請確保你的螢幕、shell 歷史記錄以及任何輸出檔案保持私密。",
"confirmHeading": "⚠ 警告:此操作會以明文匯出已解密的提供者憑據",
"confirmBody": "此命令會為所選連線解密並列印/寫入 apiKey、accessToken、refreshToken 和\nidToken。請將輸出視為機密。",
"confirmFooter": "如需確認,請執行:\n omniroute auth export --force",
"missingKey": "匯出憑據需要 STORAGE_ENCRYPTION_KEY。",
"notFound": "找不到連線:{id}",
"invalidFormat": "無效格式:{format}。請使用 json 或 env。"
},
"stream": {
"description": "使用 SSE 檢查模式流式傳輸聊天響應",
"file": "從檔案讀取提示",
@@ -254,6 +256,7 @@
"max_restarts": "30 秒內的最大崩潰重啟次數預設2",
"tray": "顯示系統托盤圖示(僅桌面,選擇加入)",
"no_tray": "停用系統托盤圖示",
"ready_timeout": "就緒探測逾時(毫秒)(也可用 OMNIROUTE_READY_TIMEOUT_MS預設 60000",
"tls_cert": "用於提供 HTTPS 服務的 TLS 憑證PEM路徑也可用 OMNIROUTE_TLS_CERT",
"tls_key": "用於提供 HTTPS 服務的 TLS 私鑰PEM路徑也可用 OMNIROUTE_TLS_KEY"
},
@@ -301,7 +304,7 @@
"status": "狀態:{status}",
"uptime": "執行時間:{uptime}",
"requests": "請求數24h{count}",
"cost": "成本24h"
"cost": "成本24h${cost}"
},
"quota": {
"description": "顯示提供者配額使用情況",
@@ -345,6 +348,16 @@
"running": "MCP 伺服器正在執行({transport}",
"stopped": "MCP 伺服器已停止。",
"restarted": "MCP 伺服器已重啟。",
"stoppedHint": "執行 `omniroute mcp enable` 以啟用它。",
"enabled": "MCP 伺服器已啟用。",
"disabled": "MCP 伺服器已停用。",
"enable": {
"description": "啟用 MCP 伺服器",
"transport": "要使用的傳輸方式stdio|sse|streamable-http"
},
"disable": {
"description": "停用 MCP 伺服器"
},
"call": {
"description": "直接呼叫 MCP 工具",
"args": "JSON 引數物件(內聯)",
@@ -807,6 +820,18 @@
"event": "要模擬的事件型別預設request.completed"
}
},
"program": {
"description": "OmniRoute — 具有自動故障轉移的智慧 AI 路由器",
"version": "列印版本並退出",
"output": "輸出格式table, json, jsonl, csv",
"quiet": "禁止非必要輸出",
"no_color": "停用彩色輸出",
"timeout": "HTTP 請求超時(毫秒)",
"api_key": "OmniRoute 伺服器的 API 金鑰",
"base_url": "OmniRoute 伺服器的基礎 URL",
"context": "此命令使用的伺服器上下文/配置檔案",
"lang": "設定 CLI 顯示語言(覆蓋 OMNIROUTE_LANG"
},
"files": {
"description": "管理檔案(上傳、列出、獲取、下載、刪除)",
"list": {
@@ -907,6 +932,11 @@
"model": "按模型篩選"
}
},
"radar": {
"description": "檢查並同步本機 Radar 目錄訂閱來源",
"status": "顯示本機 Radar 設定和訂閱來源快取狀態",
"sync": "透過本機伺服器同步目錄、推薦、優惠和 Intel"
},
"resilience": {
"description": "檢查和管理彈性機制",
"status": {
@@ -1234,8 +1264,8 @@
"description": "管理 OmniRoute 開機自啟Linuxsystemd 使用者服務)",
"enable": "啟用開機自啟",
"disable": "停用開機自啟",
"status": "顯示自啟狀態",
"toggle": "切換開機自啟"
"toggle": "切換開機自啟",
"status": "顯示自啟狀態"
},
"runtime": {
"description": "管理本地執行時依賴",
@@ -1262,25 +1292,6 @@
"update": "更新已安裝的外掛",
"scaffold": "搭建新的外掛模板"
},
"authExport": {
"description": "匯出已解密的提供者憑據(僅限本機,明文輸出)",
"idOpt": "僅匯出與此 id/名稱/提供者相符的連線",
"formatOpt": "輸出格式json 或 env",
"outOpt": "將輸出寫入檔案而非標準輸出(以 0600 權限寫入)",
"forceOpt": "確認你了解此操作會列印/寫入明文密鑰",
"warning": "⚠ 此操作會列印/寫入已解密的明文 API 金鑰和 OAuth 令牌。請確保你的螢幕、shell 歷史記錄以及任何輸出檔案保持私密。",
"confirmHeading": "⚠ 警告:此操作會以明文匯出已解密的提供者憑據",
"confirmBody": "此命令會為所選連線解密並列印/寫入 apiKey、accessToken、refreshToken 和\nidToken。請將輸出視為機密。",
"confirmFooter": "如需確認,請執行:\n omniroute auth export --force",
"missingKey": "匯出憑據需要 STORAGE_ENCRYPTION_KEY。",
"notFound": "找不到連線:{id}",
"invalidFormat": "無效格式:{format}。請使用 json 或 env。"
},
"radar": {
"description": "檢查並同步本機 Radar 目錄訂閱來源",
"status": "顯示本機 Radar 設定和訂閱來源快取狀態",
"sync": "透過本機伺服器同步目錄、推薦、優惠和 Intel"
},
"launch": {
"description": "啟動指向 OmniRoute 的 Claude Code本機或遠端使用 --profile",
"token": "Claude 用戶端應傳送的令牌ANTHROPIC_AUTH_TOKEN",

View File

@@ -14,6 +14,7 @@ import { stopProcessGracefully } from "../../../src/shared/platform/windowsProce
import {
isFatalInstrumentationHookFailure,
formatAndroidInstrumentationFailureHint,
isFatalStartupDiagnostic,
} from "../utils/ensureAndroidCacheDir.mjs";
const CRASH_LOG_LINES = 50;
@@ -55,12 +56,14 @@ export class ServerSupervisor {
this.child = null;
this.isShuttingDown = false;
this.instrumentationFailureHintPrinted = false;
this.fatalStartupDiagnosticPrinted = false;
}
start() {
this.startedAt = Date.now();
this.crashLog = [];
this.instrumentationFailureHintPrinted = false;
this.fatalStartupDiagnosticPrinted = false;
const showLog = process.env.OMNIROUTE_SHOW_LOG === "1";
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
@@ -99,6 +102,15 @@ export class ServerSupervisor {
)
);
}
// #13314: surface any `[STARTUP] Fatal:`-guarded boot diagnostic
// immediately, even without --log — otherwise it is only buffered and
// reaches the operator on exit/crash, which never happens when the
// HTTP listener still comes up after the fatal failure (every route
// then 500s with zero visible diagnostic anywhere).
if (!this.fatalStartupDiagnosticPrinted && isFatalStartupDiagnostic(text)) {
this.fatalStartupDiagnosticPrinted = true;
process.stderr.write(text.endsWith("\n") ? text : `${text}\n`);
}
};
if (this.child.stdout) {

Some files were not shown because too many files have changed in this diff Show More