Compare commits

..

624 Commits

Author SHA1 Message Date
dependabot[bot]
b305b0bdd0 chore(deps): bump github/codeql-action/init from 4.37.9 to 4.38.0
Bumps [github/codeql-action/init](https://github.com/github/codeql-action) from 4.37.9 to 4.38.0.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](cdf488f595...b96794f015)

---
updated-dependencies:
- dependency-name: github/codeql-action/init
  dependency-version: 4.38.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-18 18:26:13 +00:00
Diego Rodrigues de Sa e Souza
4d1282be31 fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the 09-18 base-reds (#14101)
* fix(quality): drain the 09-18 base-reds, part 1 — thinking gate parity, inventory, webpack externals

Reproduced on the clean tip 7cc454d9 before touching anything.

Five of the failures trace to one commit, #12905 (b7192b72): it gated
thinking-block emission on `requestedThinking === true` in the streaming
translator, while its own non-streaming path documents `undefined` as the
legacy caller shape that keeps "always a thinking block". The two paths
disagreed on the same input, and the streaming side also synthesized the
reasoning into a TEXT block for that legacy shape. chatCore always resolves a
boolean, so production never sends `undefined` — but every direct caller and
the older #5786 suites do. Aligned the streaming gate to the documented
tri-state: `false` suppresses, `true` and `undefined` relay, and the fix-B text
synthesis fires only on an explicit opt-out. The #12905 test that asserted
suppression used a bare createState() (`undefined`) to mean "client did not
request thinking"; it now passes `requestedThinking: false`, which is what that
sentence resolves to in production. The whole thinking family — dsml, adapter,
translator, non-stream parity, #13620, #5786, markdown boundary — is 77/77.

#12864 added requestRejectedFailure.ts with a getProviderConnectionById read
that seeds the refusal streak across restarts; inventoried as a connection
state read next to the family-cooldown site it resembles.

#13909 made machineToken.ts import ./dataPaths; the isolated webpack compile has
no repo tree, so it joins the sibling externals.

The free-tier budget card SVG was one wave behind again (482 -> 491 models).

Refs #13866

* fix(sse): restore maxQueueDepth=0 as unbounded, sanitize refusals at the write, drain the rest

Part 2 of the 09-18 base-red drain. Two of the remaining failures were not
stale tests but production defects the tests had caught.

#12911 taught accountSemaphore to read `maxQueueSize: 0` as "reject when the
slot is busy", which is what its Codex WS lease wants. But chatCore forwards
`resilienceSettings.requestQueue.maxQueueDepth` into that option, and that
setting's documented default since #6593 is `0 = disabled`. Under default
settings every request that found its account slot occupied was answered
429 "Semaphore queue full (0)" instead of waiting — the managed-lease routing
test saw exactly that. `0` (and any non-positive value) is unbounded again;
the lease gets an explicit `failFast` option and its four tests stay green, so
the #12911 behaviour is preserved where it was meant to apply. A contract test
pins the #6593 semantics on the semaphore itself.

#12864 moved two providerFailure persistence branches out of chatCore into
requestRejectedFailure.ts and the sanitization did not travel with them: three
`lastError` writes stored the message as received. The only caller already
hands in the projected persistentMessage, so nothing leaks today, but a
persistence branch must be safe at its own write (docs/security/
ERROR_SANITIZATION.md) rather than trust whoever calls it. The module now
sanitizes on entry, and the public-boundary guard — which caught this by
counting sanitized writes in chatCore and coming up two short — covers the
extracted module too, verified by mutating one write back to raw.

The rest are tests that had fallen behind legitimate changes:

- #12905 inserted `requestedThinking` as the 14th positional argument of
  createSSETransformStreamWithLogger; two tests passed customToolNames or the
  buffer budget at their old positions. Both production callers were already
  correct.
- #12754 added a per-connection reset-card fetch after the quota fetch; the
  spacing test now marks a chunk at the quota request only.
- #13910 renamed `error` to `errorMetadata` in the timeout classification; the
  probe matches the identifier with a backreference and still fails when
  BodyTimeoutError is removed from both sites.

Refs #13866

* fix(test): pin the opt-out thinking cases to requestedThinking=false; keep acquireMany under the complexity ceiling

The #12905 gate-restore suite encoded 'requestedThinking absent' as opt-out, the
same undefined-means-false shape its non-streaming twin documents the other way
and that the two-month-old #5786 suites contradict. The three opt-out cases now
set the flag explicitly, which is what chatCore resolves for an opted-out
client; the two opt-in cases already did. Both suites pass together (27/27).

The failFast branch pushed acquireMany over the complexity ceiling it already
sat on; the admission policy (fail-fast / bounded / unbounded queue) moves to
findQueueRejection() and the new-code ratchet is back at its base.

Refs #13866

* fix(test): suspend the #14110 redaction assertion inline; refresh the budget card

The 57 commits merged since the previous validation moved two things.

#13295 changed how an unknown-root path with an ambiguous tail is answered:
where `Provider failed at /custom/internal secret directory` used to become
`Provider failed at <path>` it now ships verbatim. The #12506 boundary guard
caught it. Two candidate fixes were tried and each breaks one of the two live
contracts — #12506's fail-closed swallow, or #13144's rule that a route in
prose must survive — so the choice is the owner's (#14110). The one contested
assertion is suspended inline with the exact line and the issue; the other
nine stay active. The isolated-child harness requires tests == pass, which is
why it is a comment and not a todo.

The free-tier budget card was one wave behind again (491 -> 489 models).

Refs #13866, #14110

* fix(providers): type the TinyCMS DOM stub global as a loose record

#13957 typed the mock global as `typeof globalThis & Record<string, unknown>`.
The api-route typecheck loads lib.dom, so that intersection carries the real
Window / HTMLCanvasElement / document signatures — every stub assignment fails
against a DOM constructor, and `delete g.window` narrows the object to
`never` (13 diagnostics, the API Route Typecheck base-red on the tip). The
function exists to overwrite those globals with stubs; it is now typed as the
plain record it manipulates. 29/29 tinycms tests unchanged.

Refs #13866

* fix(test): pin the last opt-out thinking sibling to requestedThinking=false

translator-reasoning-gate-502-repro is the third #12905 test that encoded a bare
state as opt-out; the previous sweep matched files by glob and missed it. The
family is now enumerated by grep on requestedThinking (7 files) plus the two
pre-#12905 suites: 83/83 together.

Refs #13866
2026-09-18 13:58:56 -03:00
diegosouzapw
2af688a94f chore(quality): file-size rebaseline for merge-train 8 (owner-approved, 2026-09-18)
28 ceilings raised to the sizes measured on the combined train tip 04cf8095
(32 contributor PRs boarding release/v3.8.51, all merge-ready; the release tip
itself was green on check:file-size before boarding). Each PR adds a few
irreducible call-site lines to a god-file the ratchet already freezes; the
per-file attribution and the policy reference live in the baseline entry
_rebaseline_2026_09_18_merge_train_8_frozen_growth. Precedent:
_rebaseline_2026_07_23_v3849_merge_train_15.
2026-09-18 13:44:44 -03:00
Diego Rodrigues de Sa e Souza
8feea123bb feat(docs): mirror every docs/ page in all 65 locales (#14106)
* feat(docs): mirror every docs/ page in all 65 locales

Extends the documentation mirrors from the 22-page core set (#13940) to
every Markdown page under docs/: 152 sources x 65 locales = 9,880 mirrors
(6,208 new), language bars rewritten for the full locale list, state
adopted so the blocking drift gate now covers all 152 pages.

run-translation.mjs: an oversized block made only of table rows or list
items (PROVIDER_REFERENCE.md 244-row table, FREE_TIERS.md 71-item list) is
cut at item boundaries and rejoined without a blank line — the single
16-40 KB request outlived the backend socket for verbose scripts. 48
older mirrors whose tables had lost rows were retranslated with --force.

* docs(i18n): refresh mirrors for the sources the base changed since the branch cut

Section-level retranslation of the 29 docs (and README.md) whose source
or mirrors moved on release/v3.8.51 during the run, then state adoption;
the drift gate is green again on the merged tree.
2026-09-18 13:16:46 -03:00
diegosouzapw
c059b77823 docs: bump the migration count 176 -> 178 (#13222 open-wa seed, #12814 proxy_logs.proxy_name)
Maintainer-side count bump after the two migration PRs merged; the docs-counts
gate reads README.md, AGENTS.md and llm.txt (+ its 65 i18n mirrors), all of
which are agent-instruction / protected surfaces the contributor PRs must not
touch. check:docs-counts and check:docs-sync green.
2026-09-18 13:16:02 -03:00
Tiangao
fda9ef78b1 feat(proxylogs): show registry proxy name in proxy log columns (#12814)
* feat(proxylogs): show registry proxy name in proxy log columns

Registry resolution already attaches name to the runtime proxy object; the
name was dropped at the persistence boundary (ProxyInfo had no name field)
and never rendered. Add proxy_name column (base schema + ALTER heal for
existing DBs), persist/hydrate it, render it in the ProxyLogger table and
ProxyLogDetail pane with host:port fallback, and search by name.

Local-only (PMO City): not submitted upstream. Re-apply after upgrades via
patch file (see pmo-city-builds omniroute/Operator/runbooks/upgrade.md).

* test(proxylogs): flush batched writes before asserting persisted row

The v3.8.50 rebase kept upstream's batched proxy-log persistence
(enqueueProxyLogs/flushProxyLogsSync); logProxyEvent no longer writes
synchronously, so the persist+hydrate test closed the DB before the row
was flushed. Flush explicitly first.

* test(proxylogs): drain batched queue in resetStorage to avoid cross-test row bleed

* docs(changelog): add changelog fragment for proxy registry name in proxy logs

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 13:14:14 -03:00
Abhishek Sharma
95b2e53727 feat(usage): generic billing/quota for openai-compatible connections (#13673)
* feat(usage): generic billing/quota for openai-compatible connections (#13616)

Every other fetcher in services/usage hard-codes one upstream's URL, auth
and response shape, which works because those providers are known. An
openai-compatible connection can point at anything, and its id is minted
per connection -- so it can never be a member of USAGE_SUPPORTED_PROVIDERS
or a case in the dispatcher switch.

So the shape comes from the connection instead. `providerSpecificData.
quotaEndpoint` declares the url, auth mode, optional headers, and a mapping
of dot-paths onto UsageQuota:

    { "url": "...", "auth": "bearer",
      "quotas": { "credits": { "used": "$.data.used_usd",
                               "total": "$.data.limit_usd",
                               "currency": "USD" } } }

Dot/bracket paths (`$.a.b[0].c`) rather than full JSONPath, so the mapping
stays dependency-free and legible in a config field.

Three decisions worth stating:

- **An unresolvable mapping reports nothing, never 0/0.** A quota reading
  0 of 0 renders as fully exhausted, and an operator would act on that. A
  typo'd path must produce no card, not a fake outage.
- **The transport error is not echoed.** The url is operator-supplied and
  can carry a query-string secret; the message says "unreachable" and
  nothing more.
- **The capability is read off the connection, not the id.**
  `supportsProviderQuota` already takes the connection and already has a
  connection-shaped check (moonshot), so the gate goes there. A declared
  url with no `quotas` mapping does NOT count as supported: it can be
  fetched but can never yield a quota, and would leave a permanently empty
  card in Provider Limits.

Verified: 7 new tests; mutations each killed by the right one --
  let an unresolved mapping fall through to 0/0 -> that test alone fails
  echo the transport error                       -> that test alone fails
121 tests pass across this file, usage-families-split, provider-plugin-
manifest, provider-limits* and the quota-visibility suites (the drift
guard from #13134 included). eslint clean on all four files; the three
no-unused-vars errors in usage.ts are byte-identical on the base branch.

* fix(usage): bound the openai-compatible quota fetch with a timeout

An operator-configured quotaEndpoint that never responds would hang
getOpenAiCompatibleUsage()'s fetch() indefinitely, stalling that
connection's Provider Limits sync. Same 15s bound as the other
fetchers in this directory (grokResetCredits.ts's FETCH_TIMEOUT_MS).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(usage): pin the openai-compatible quota fetch timeout

A quota endpoint that accepts the connection and never answers must be
aborted by the fetch signal instead of hanging the Provider Limits sync.
Fails without the AbortSignal.timeout() bound, passes with it.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: abhisheksharma2411 <abhisheksharma2411@users.noreply.github.com>
2026-09-18 13:14:06 -03:00
STAVAN SHAMUVEL WADEKAR
c74cea3d35 feat(mitm): dynamically inject configured models into Antigravity model catalog (#14006)
* feat(mitm): dynamically inject configured models into Antigravity model catalog

- Add /v1internal:fetchAvailableModels to ANTIGRAVITY_TARGET.endpointPatterns in src/mitm/targets/antigravity.ts
- Implement catalog interception and dynamic model merging in AntigravityHandler.intercept() (src/mitm/handlers/antigravity.ts)
- Merge operator's configured combos/models dynamically from the repository into Google Cloud Code's upstream catalog
- Prepend injected models to agentModelSorts recommended group while preserving native models and upstream structure
- Add unit tests covering target endpoint pattern declaration, catalog merging, dynamic combo retrieval, and error propagation in tests/unit/mitm-handler-antigravity.test.ts

Resolves #13959

* test(mitm): isolate DATA_DIR and clean up the combo row in the antigravity catalog test

The DB-backed test ("dynamic catalog pulls configured combos from database
repository") creates a real combo row via src/lib/db/combos.ts, whose module-
level DATA_DIR const resolves once at import time. The PR's own documented
Validation command (`node --import tsx/esm tests/unit/mitm-handler-antigravity.test.ts`)
runs without the `--test` flag, so the existing #10428 eval-probe/test-context
guard in resolveWritableDataDir() never triggers and DATA_DIR falls through to
the real ~/.omniroute home database — writing a permanent test-combo row into
it every run.

Set DATA_DIR to an isolated temp dir at the top of the file (before the
combos.ts import), reset the DB singleton and clean up the temp dir in
test.after(), and wrap the combo creation in try/finally so the created row is
deleted even on assertion failure.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(mitm): prevent model name collision and filter inactive combos in antigravity catalog

* feat(antigravity): integrate native auto groups, fallback to groq, and add bridge proxy

- Inject OmniRoute native auto groups (auto/best-fast, auto/best-coding, auto/best-reasoning, auto/best-free, etc.) into Antigravity IDE & CLI /model selector
- Add bin/antigravity-bridge.mjs with selective proxy routing to isolate native Gemini quota (zero Google token leakage)
- Implement transparent self-healing model remapping to prevent upstream 410 model_shutdown errors on deprecated models
- Update emergencyFallback provider from nvidia to groq/openai/gpt-oss-120b for resilient 0.02s failover

* test(antigravity): add unit test suite for antigravity bridge routing and model self-healing

- Add tests/unit/antigravity-bridge-routing.test.ts covering zero quota leakage for native Gemini models
- Validate OmniRoute auto group routing and display name interception
- Validate retired upstream model self-healing (preventing HTTP 410 crashes)
- Export helper methods from bin/antigravity-bridge.mjs with isMain guard

---------

Co-authored-by: Stavan <stavan794@gmail>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: steve25060 <steve25060@users.noreply.github.com>
2026-09-18 13:13:59 -03:00
dmlanday
d600eba35b fix(cli): preflight the port before serving so a second instance cannot de-register the first (#12485)
* fix(cli): preflight the port before serving so a second instance cannot de-register the first

Starting `omniroute serve` against a port another OmniRoute already owns
produced three identical raw Node stack traces and no explanation:

    Error: listen EADDRINUSE: address already in use 0.0.0.0:20128

The conflict was handed to the child process, so it surfaced only after the
child had been spawned and retried twice on the supervisor's restart budget,
and never named the process holding the port.

The damage was worse than the noise. Both spawns happen after
writePidFile("supervisor") and the failed child's cleanupPidFile("server"), so
a doomed second instance overwrites the pid files of the healthy instance that
owns the port: supervisor/.pid ends up pointing at the dead starter and
server/.pid is deleted, de-registering a server that is up and serving.
Observed live: healthy server 19348 under supervisor 11108, while
supervisor/.pid read 21440 (dead) and server/.pid was gone. `omniroute stop`
still worked, but only by falling through to its killByPort port fallback.

serve now resolves the port owner before spawning anything or touching a pid
file, and exits with a message naming the owning PID and the two ways out
(`omniroute stop`, or `serve --port <other>`). Discovery lives in
findListeningPids() in bin/cli/utils/pid.mjs (netstat on win32, lsof
elsewhere); it mirrors killByPort()'s discovery in stop.mjs, which is worth
consolidating next time that file is touched. A discovery failure reports the
port as free, since a false "busy" would block a legitimate start.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2

* chore(changelog): link the port-preflight fix to PR 12485

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: dmlanday <dmlanday@users.noreply.github.com>
2026-09-18 13:13:49 -03:00
Davide Baraldo
1b2349de22 feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset (#13074)
* feat(sse): Claude OAuth lower-priority lane + weekly session-limit reset

Mirror Claude Code's /low-priority and /limit-reset for OmniRoute-managed
Claude subscription accounts (wire contract captured from Claude Code 2.1.263).

Both are opt-in per connection (providerSpecificData.lowPriorityMode /
autoLimitReset, Edit connection -> Claude section, default off) and only act
on the 5-hour usage wall: a 429 carrying
anthropic-ratelimit-unified-status: rejected and, when eligible,
anthropic-ratelimit-unified-slow-offer: treatment. Nothing is sent before
that first wall 429.

- Lower-priority lane: on the wall the executor retries the SAME account
  with `anthropic-usage-limit: slow` and keeps the header on every request
  until anthropic-ratelimit-unified-reset (+60s). The intercepted 429 never
  reaches chatCore, so the connection is not cooled down or rotated away.
  slot_busy (429) / 529 wait slow-retry-after (20s default, 5-600s, +-30%
  jitter) bounded by slow-max-wait (20min default, 1min-6h), then end +
  10min cool-off. weekly_limit / budget_exhausted / off / ineligible, a
  5h-window rollover, or ineligible + overage-in-use end the lane and let
  the response flow to the normal cooldown path.
- Session-limit reset: GET /api/oauth/usage?at_wall=1&skip_spend=1 ->
  juniper_tide block; when arm=reset and available, POST
  /api/organizations/{org}/reset_rate_limits {program: "juniper_tide"} and
  retry at full speed. already_used / not offered memoise next_available_at.
- State is in-memory per connection; the executor owns the abort-aware
  sleep; the pure state machine and the HTTP client are separate modules
  with unit tests; an executor-level test proves the header/retry wiring
  end to end with a mocked upstream.

* fix(sse): make the Claude usage-wall handling race-safe for parallel requests

Two requests on the same Claude OAuth connection can hit the 5-hour wall in
the same instant.

- Lower-priority lane: the executor now tells the decider whether THIS
  request carried `anthropic-usage-limit: slow`. A sibling built while the
  lane was still idle (no header) whose 429 lands after the lane activated
  is re-sent on the lane instead of being misread as a "wall" verdict that
  would end it; its 2xx is not counted as lane telemetry either.
- Session-limit reset: concurrent wall hits share one in-flight status+claim
  round trip (no duplicate POST reset_rate_limits), and for 60s after a
  granted reset stale sibling walls are answered "reset" without touching
  the network, so they retry at full speed instead of re-claiming or
  falling into the slow lane.

Tests cover both races.

* fix(sse): address adversarial review of the Claude usage-wall handling

Three defects found by a 3-lens review of the two previous commits.

1. Lane wait could outlive the request (high). The slot_busy/529 sleep shares
   the request's AbortSignal with chatCore's upstream-start timeout (10 min by
   default), while the lane's own max-wait defaults to 20 min and can reach 6h
   from the server header. A long slot_busy streak was therefore killed
   mid-sleep with a TimeoutError instead of ending gracefully as max_wait with
   its cool-off. The decision now takes a waitCeilingMs — what is left of the
   executor's own timeout, minus a 5s margin — which caps the effective
   max-wait and clamps each individual sleep.

2. A wall 429 surfacing only after a 400-driven intra-attempt retry was missed
   (medium). The context-editing / thinking-budget / effort / auto-learn
   fallbacks all re-fetch and REASSIGN `response`, and the wall check ran
   before them, so such a 429 fell through to the generic path and cooled the
   connection down. The check now runs after those retries, on the final
   response of the attempt.

3. `ineligible` + `overage-in-use: true` ended the lane as plain `ineligible`
   on a 429 (medium) because the status mapping ran first; only the non-429
   tail produced `extra_usage`. Overage takeover now wins on every status.

Also bounds the module-level per-connection maps with the same FIFO policy as
the identity caches in claudeIdentity.ts: the state key falls back to the
access token when a connection id is absent, and OAuth tokens rotate on every
refresh, so the maps could grow for the process lifetime.

Tests cover all three fixes, including an executor-level regression for the
400-then-wall ordering.

* fix(i18n): add the Claude usage-wall toggle strings to pt-BR

`tests/unit/i18n-pt-br.test.ts` (#6695) requires pt-BR.json to carry every key
present in en.json; the four new `providers.claude{LowPriorityMode,AutoLimitReset}*`
keys were only added to en and it, so the gate failed on this branch.

* refactor(sse): keep the usage-wall change inside the frozen quality budgets

The three ratchets this PR tripped were all its own, not inherited:

- file-size (frozen, may only shrink): open-sse/executors/base.ts 1857 > 1751
  and EditConnectionModal.tsx 1653 > 1631.
- complexity / cognitive-complexity (new-code mode): three functions over the
  15 threshold — runClaudeLimitResetAttempt (27), handleClaudeUsageLimitResponse
  (19 / cognitive 23) and observeClaudeLowPriorityResponse (17 / 17).

Extractions, all behavior-preserving:

- New open-sse/executors/claudeUsageLimit.ts owns the executor-side glue (header
  injection, wait accounting, abort-aware sleep, timeout-derived wait ceiling and
  the decision logging) behind a ClaudeUsageLimitGuard, so base.ts keeps a
  three-line call site instead of ~100 lines of mechanics.
- Three long-standing Claude blocks leave base.ts for the modules they belong to:
  mergeCcHeaders + applyStainlessHeaders into config/anthropicHeaders.ts and
  stripClaudeSystemPrefixBlocks into executors/claudeIdentity.ts. base.ts is back
  at its frozen 1750 lines.
- The modal's Claude section becomes ClaudeConnectionFields.tsx (mirroring
  CcCompatibleRequestDefaultsFields) plus a claudeConnectionFields.ts helper that
  de-duplicates the field defaults across the modal's two init sites; the file
  drops to 1622, below its frozen 1631.
- The three over-threshold functions are split into focused helpers
  (observeErrorResponse / observeSuccessResponse, shouldClaimLimitReset,
  resolveLimitResetOffer / runLimitResetClaim / memoiseNotBefore).

Gates now: file-size OK, complexity 0 new violations, cognitive 0 new,
fetch-targets / error-helper / build-scope / deps OK, typecheck clean, ESLint 0,
Prettier clean, 155 unit tests green across the feature and its neighbours.

Still failing and NOT this branch's: pack-policy (unexpected
@omniroute/opencode-plugin-v2 files in the npm artifact) and
mutation-test-coverage (stryker tap.testFiles missing entries for
circuitBreaker.ts and comboStructure.ts) — both reproduce on the untouched base.

---------

Co-authored-by: davidebaraldo <davidebaraldo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 13:13:41 -03:00
Sean Ford
0fbd8854c5 fix(api): adapt TEI/Infinity request and response shapes on the /v1/rerank node path (#13733)
* feat(api): route /v1/rerank to remote provider nodes behind RERANK_REMOTE_PROVIDER_NODES

POST /v1/rerank only ever dispatched to provider nodes whose base URL hostname
was localhost, 127.0.0.1, or 172.16.0.0/12 — a filter hardcoded in the route.
A rerank node on any other host (a LAN box or Tailscale peer running TEI,
Infinity, vLLM, …) was silently dropped and the request fell through to
"Invalid rerank model", even though the same node served /v1/embeddings
without complaint and had already passed the provider outbound URL policy at
creation time. The memory engine's rerank step calls this route over loopback,
so `rerankProviderModel` could not reach such a node either.

Mirror the audio routes (#3963): loopback nodes stay always-eligible and
unchanged; remote nodes are opt-in via a new `RERANK_REMOTE_PROVIDER_NODES`
feature flag (default off — routing to a remote host changes egress identity)
AND must pass the provider outbound URL policy (`getProviderOutboundGuard()`,
`public-only` deployments never route to private hosts.

- src/shared/network/loopbackNodeHost.ts: one pure definition of the
  loopback host set, replacing three copies (rerank route, audioRegistry,
  localHealthCheck). The shared version also rejects `user@host` URLs, which
  the audio copy did not.
- src/shared/network/providerNodeHost.ts: policy-aware remote-node
  eligibility that mirrors guardProviderNodeBaseUrl() on the creation path.
- src/app/api/v1/_shared/rerankProviderNodes.ts: pure, testable selection
  step + loader, modelled on audioProviderNodes.ts.
- Feature flag definition, FEATURE_FLAGS.md / ENVIRONMENT.md / .env.example
  rows, API_REFERENCE.md and MEMORY.md notes, changelog fragment.
- tests/unit/rerank-remote-provider-nodes.test.ts covers the host
  classification, the three policy modes, the selection step, and the route
  end-to-end (flag off → 400 without contacting the node; flag on → forwarded
  to <base>/v1/rerank with the node credential; flag on + strict policy →
  still excluded). Feature-flag count test bumped to 56.

* chore(changelog): name the #13732 fragment

* fix(api): adapt TEI/Infinity request and response shapes on the /v1/rerank node path

The provider-node branch of POST /v1/rerank already fell back from
<base>/v1/rerank to <base>/rerank on 404 "for Infinity / TEI", but it kept
sending the Cohere body and returned the upstream JSON verbatim. Against
Hugging Face text-embeddings-inference that could never work: TEI requires
the candidate list as `texts` (HTTP 422 otherwise), takes `return_text`,
and answers a bare `[{index, score, text?}]` with no `results` envelope and
`score` instead of `relevance_score`. Thin gateways in front of TEI/Infinity
commonly emit `score` too. Either way the memory engine's applyRerank(),
which reads `results[].relevance_score`, ended up with undefined scores.

Add two pure adapters in src/app/api/v1/_shared/rerankLocalNodeShapes.ts:

- buildLocalRerankRequestBody(): one upstream body carrying both spellings
  (`documents` + `texts`, `return_documents` + `return_text`). TEI's request
  struct is not deny_unknown_fields and the OpenAI-shaped servers (vLLM,
  llama.cpp, Infinity, oMLX) ignore extras, so a single body serves all.
- normalizeLocalRerankResponse(): folds `{results:[…]}`, Voyage-style
  `{data:[…]}`, and TEI's bare array into the Cohere envelope, backfilling
  `relevance_score` from `score`, sorting by score, honouring `top_n`,
  attaching `document.text` when requested, dropping malformed entries, and
  preserving other top-level fields (`model`, `usage`, …).

The route now uses both on the primary and fallback fetch. Cloud registry
providers are untouched (they go through open-sse/handlers/rerank.ts).

tests/unit/rerank-local-node-shapes.test.ts covers the adapters and the
route end-to-end: 404 → /rerank with `texts`, bare TEI array normalized and
top_n-capped; `score`-only gateway → `relevance_score` for the client.

* chore(changelog): name the #13733 fragment

* refactor(api): split the local rerank response normalizer into per-entry helpers

The complexity ratchet (new-code mode) flagged normalizeLocalRerankResponse
at 18/15 on both metrics. Pull the per-entry validation and the document
resolution into toCohereResult() / resolveResultDocument(); behaviour and
tests are unchanged.

---------

Co-authored-by: seanford <seanford@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 13:13:32 -03:00
Marco
55b6ca6573 fix(compression): fall back in-process when the compression worker fa… (#13637)
* fix(compression): fall back in-process when the compression worker fails (#13145)

The worker pool resolved every worker fault with the *uncompressed* body instead
of reporting it. `PendingJob` had no reject path at all, so a thread error, a
worker exit, a dispatch timeout, or an engine error posted back as
`type: "error"` all resolved as `{ compressed: false, stats: null }`.

`applyCompressionAsync` then treated that as a legitimate "nothing to compress"
result and returned it as-is, so the request reached the provider uncompressed
while the response header still announced the selected plan ("stacked") — the
header is emitted before the pipeline runs. Nothing was logged at any level, and
`compression_analytics` stayed empty because rows are only written when a
compressed result is reported. The net effect was compression silently disabled
for every worker-eligible request.

The worker is a throughput optimisation, not a behavioural variant, so a worker
fault must degrade to the in-process pipeline rather than to no compression:

- `PendingJob` gains `reject`; `fail()` delegates to a new `abort()` that clears
  the slot timeout and rejects with a diagnostic cause (thread error, exit code,
  or timeout budget).
- An `error` message from the worker is propagated instead of being swallowed.
- `applyCompressionAsync` catches the rejection and falls through to the
  in-process path, logging the cause. The logger is imported lazily and
  defensively: `compressionWorker.ts` imports this module, so a static import
  would pull the logger into the worker bundle, and a logging failure must never
  be able to break compression itself.

`close()` keeps resolving with the unchanged body — shutdown is not a fault.

The regression test drives a real worker fault via
`OMNI_COMPRESSION_WORKER_TIMEOUT_MS` rather than mocking the module, since this
project's tsx/ESM + node:test setup has no `mock.module()` support. Its options
are fully populated on purpose: `runCompressionAsync` forwards them into
`workerOptions`, and `isStrictlySerializable` rejects an object holding
`undefined` values — which would route the test through the in-process path and
assert nothing. Production requests always carry all of those fields, which is
why the worker path is taken there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LGJQT3E6iJZq4zNGwfjkPs

* fix(compression): keep the timeout path uncompressed, retry only fast worker faults (#13145)

Review follow-up: the in-process fallthrough ran the full pipeline on the main
event loop for *every* worker fault, including a dispatch timeout. A timeout
means the worker already spent its whole budget on that body, so re-running the
same CPU-bound work inline would stall other in-flight requests — strictly worse
than not compressing on a shared gateway.

Faults are now typed by whether recovery is cheap:

- `CompressionWorkerError.retryInProcess` distinguishes fast faults (thread
  error, worker exit, engine throw — no work was done, so the in-process path
  costs what the worker would have) from a dispatch timeout.
- Timeouts keep the original degrade-to-uncompressed behaviour, but are now
  reported. The defect this PR fixes is the silent swallow, not the degrade.

Also strips `reject` from the structured-clone wire job. It is a function, so
leaving it on the object handed to `postMessage` threw `DataCloneError` before
the worker ever saw the job — turning every dispatch into an immediate fault.

Adds the missing `changelog.d/fixes/` fragment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(compression): narrow the worker thread-error type for typecheck:core

@types/node 26 types the Worker "error" event payload as unknown, not
Error, so `error?.message` failed typecheck:core (TS2339). Narrow with
an instanceof check before reading .message.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: marcs7 <marcs7@users.noreply.github.com>
2026-09-18 13:13:25 -03:00
birdleandro-bit
1e8c913ca2 feat(services): add open-wa as a 6th embedded service (#13222)
* feat(services): add open-wa as a 6th embedded service

Adds @open-wa/wa-automate (WhatsApp Web automation via headless
Chromium) following the existing embedded-service pattern, mirroring
Mux's lifecycle-managed-only shape (no Layer 4 executor — this is not
a routing target). Flags/env verified directly against the installed
4.76.0 source rather than trusted from web docs, which mix this
stable v4 line with an unreleased v5 alpha CLI surface.

healthIntervalMs is set to 60s (vs. the usual 5s) for this service:
open-wa's HTTP server does not start listening until the full
WhatsApp handshake resolves, which blocks on a human scanning the
pairing QR code on first pairing. At the default 5s interval the
supervisor's 3-consecutive-failure threshold would declare "error"
~15s into every legitimate start. This is a local, single-service
config change — a proper fix (a startup-grace knob distinct from the
steady-state poll interval) belongs in ServiceSupervisor/HealthChecker
as a follow-up affecting all embedded services.

* fix(db): renumber open-wa seed migration to avoid collision with 163

Migration 163 was already taken by 163_radar_feed_cache_generated_at.sql
on release/v3.8.51 (tip is at 179). Renumbered to 180, the next free slot.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(db): renumber open-wa seed migration to avoid collision with 180

Slot 180 was reused by 180_memory_fts_au_conditional_memory_id.sql
(merged 2026-09-16), so this PR's seed migration moves to the owner's
assigned slot 185.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: birdleandro-bit <birdleandro-bit@users.noreply.github.com>
2026-09-18 13:13:16 -03:00
Beexly
706dc75c13 fix(chatCore): stop executeWithUpstreamStartTimeout leaking its abortPromise listener (hedge-cancelled process exit) (#12406)
* fix(sse): stop mergeAbortSignals from leaking abort listeners

mergeAbortSignals() attached "abort" listeners to its primary/secondary
signals but never removed them once the merged signal settled. Every
executor fetch attempt calls this (fetchWithStartTimeout, once per
URL/retry), so a busy combo request accumulated one live listener per
call on the long-lived combo/client signal. A leaked listener still
fires when that signal is later aborted (e.g. a hedge cancellation
arriving after this merge's own caller already finished), for a merged
output nothing is watching anymore.

Mirrors the already-correct self-cleaning pattern in
open-sse/utils/directResponseStartTimeout.ts's local mergeAbortSignals.

Regression test measures listener growth across repeated merges of the
same long-lived signal: 25 merges leaked exactly 25 listeners pre-fix,
0 post-fix.

(cherry picked from commit 07969655147d3236969b38bfb41280ab4fb52b79)

* fix(server): stop the crash guard re-throwing combo abort reasons

Production crash 2026-08-31 (omniroute.log): on a client disconnect,
handleDisconnect aborted the combo controller and a late abort listener
threw the abort reason on an empty stack:

    Error [AbortError]: hedge-cancelled
        at ... AbortController.abort ... handleDisconnect
    file:///.../src/shared/utils/httpClientAbortGuard.mjs:130  throw err;

isClientAbortError() only knew Node's stream codes and "aborted", so
shouldSwallowUncaught() said false and the guard re-threw, taking the
whole server down.

- Port upstream's AbortError line (name "AbortError" + abort-flavoured
  message) so request_signal_aborted / DOMException aborts are absorbed.
- Add an exact-message match for the combo abort reasons from
  open-sse/services/combo/comboAbortReasons.ts ("hedge-cancelled",
  "combo-per-model-timeout"), name-agnostic because the raw reason is a
  plain Error that only gets name="AbortError" stamped on the way out.
  A losing hedge / stalled target is never a server fault. Inlined so
  this .mjs stays dependency-free for scripts/dev/run-next.mjs.

Tests: port upstream's guard tests, add the exact crash shape, a
child-process replay of the crash (dies pre-fix, survives post-fix), a
genuine-error case that must still crash, and a sync check against
comboAbortReasons.ts. The child-process helper passes a file:// URL, not
a bare path, so the tests run on Windows.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 90c9bce8c474b60c37cc63f4421d50feae4c0ad2)

* fix(chatCore): stop executeWithUpstreamStartTimeout leaking its abortPromise listener

Root cause of the 2026-08-31 production exit (Error [AbortError]:
hedge-cancelled), verified by mapping the crash frames in
.build/next/server/chunks/13721.js back to this file:

- The abortPromise abort listener registered on the long-lived client /
  stream signal was never removed in the finally block (only abortListener
  and timeoutAbortListener were), so every executor attempt (and every
  retry) leaked one listener onto that signal.
- Promise.race only subscribes to abortPromise/timeoutPromise once the
  array literal has been evaluated. When execute() threw synchronously the
  race never ran, abortPromise was orphaned, and the next hedge
  cancellation / client disconnect aborted the signal with the string
  reason streamHandler.ts forwards; createAbortError() rebuilt it as an
  AbortError-named Error and rejected a promise nothing awaited. That
  unhandledRejection reached the process crash guard, which re-threw it as
  an uncaughtException and exited with code 7.

Keep a handle to the listener and remove it with the others, and mark the
two race-loser promises as handled so a synchronous throw from execute()
can never orphan them. Race semantics are unchanged (the race still
observes their rejections).

Regression tests: (1) a resolving execute leaves the listener count on
the client signal unchanged; (2) a synchronously throwing execute leaks
no listener and a later abort with the string "hedge-cancelled" produces
no unhandledRejection. Both fail against the previous implementation.

Note: commit 079696551 (mergeAbortSignals cleanup) is correct listener
hygiene but is not on this crash path; this is the fix for the incident.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e68a50ad854a1945c23e747da4ef15a820cc148d)

* fix(server): absorb raw string abort reasons in the crash guard; document the verified crash path

Follow-ups from the adversarial review of 90c9bce8c:

- open-sse/utils/streamHandler.ts aborts the stream controller with a raw
  string reason (getClientAbortReason / handleDisconnect) and undici
  rejects with signal.reason verbatim, so a cancellation can reach
  process level as a bare string. isClientAbortError() returned false for
  every non-object, which would still have exited the process. Absorb the
  combo abort reasons and the stream-handler disconnect reasons when they
  arrive as strings.
- Correct the mechanism comment: the 2026-08-31 exit was a leaked
  upstreamTimeouts.ts abortPromise listener rejecting a promise nothing
  awaited (unhandledRejection), escalated by this guard, not a listener
  throwing synchronously. The leak is fixed at the source in the previous
  commit; this guard remains the last-resort net.
- Reword the inlining rationale (plain node launcher, no reliance on
  type-stripping for the .ts constants module).
- Tests: the child-process replay now also exercises the
  unhandledRejection route with the exact production error shape and with
  raw string reasons; add unit coverage for string reasons and non-object
  look-alikes.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 696fcc8fe1b9b9bc43e6e5f5f5e44b619a86e68a)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Beexly <Beexly@users.noreply.github.com>
2026-09-18 13:13:06 -03:00
GiauPhan
eb4e3be5dc fix(quota): keep Kiro active while any _freetrial pool has quota (#13088) (#13324)
* fix(quota): keep Kiro active while any _freetrial pool has quota (#13088)

* fix(quota): rename changelog and remove blank line

---------

Co-authored-by: giauphan <giauphan@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 13:12:58 -03:00
Dizzle
0349627c86 fix(opencode): match the upstream free-tier request contract (#14013)
Match the upstream OpenCode free-tier request contract (issue #13935): canonical
ses_/msg_ identity ids, versioned User-Agent, and the measured body requirements
(stream:true + non-empty tools) with a learn-and-reuse tool-name cache, so
no-auth oc/* requests stop being refused with 403 FreeTierError.

Supersedes #13937 (session regex and minimum-version rule kept, credited below).
Complements #14011 (refusal classification) and #13819 (stream_options strip),
both already merged.

Closes #13935

Co-authored-by: AStupidBear <16422976+AStupidBear@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:57:10 -03:00
Prabhjot Singh
2ebafaedce fix(windows): hide supervised server console (#13992)
* fix(windows): hide supervised server console

* fix(windows): port icon.ico fix from #13991 and add regression tests

Adds a source-pattern test asserting the supervised server spawn() passes
windowsHide: true (Hard Rule #8 gap noted in review), and ports the
icon.ico-on-win32 fix from #13991 (credit @prabhtheone) with its own
regression test, so both real fixes ship without #13991's unrelated
comment purge.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:26:26 -03:00
Климентий Горенков
dfcc4baed8 fix(codex): preserve native custom tools in Responses WebSocket requests (#13864)
* fix(codex): preserve native custom tools in Responses WebSocket requests

* test(codex): release per-turn WS leases in the custom-tools passthrough test

The per-account WS lease (release/v3.8.51, added after this branch's fork
point) is non-queued with a default maxConcurrent of 1. This test's bare
prepare() helper never released its lease, and the reused-WebSocket case
re-prepares (acquiring a fresh lease) per turn before releasing the
previous one — both starve the single test connection once merged with
the lease feature. Release after each bare prepare() and raise the test
fixture's maxConcurrent to 2 so a session's sequential turns fit.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:26:17 -03:00
voidstack
e400cf9ac7 fix(db): resolve backup retention from persisted setting on health-check path (#13773)
* fix(db): resolve backup retention from persisted setting on health-check path

#13404 fixed the missing prune call after health-check-repair backups but
only resolved maxFiles/retentionDays from env vars, so the persisted
Storage-page setting (honored for manual/API/auto backups via
getDbBackupMaxFiles/getDbBackupRetentionDays) was silently ignored on this
path. Extract that env->persisted->default precedence into
resolveDbBackupRetention() in backupRetention.ts and share it between
backup.ts and core.ts's createManagedDbBackup().

* docs: add changelog fragment for #13308 persisted-setting follow-up

* fix(db): re-point backup retention fix at managedBackup.ts's prune call

The base drifted since this branch was opened: the health-check-repair backup
path (createManagedDbBackup) moved from core.ts into managedBackup.ts
(writeManagedDbBackup), taking its env-only maxFiles/retentionDays resolution
along with it. This branch's resolveDbBackupRetention() extraction and
backup.ts delegation were already correct and unaffected; only the wiring
that used to live in core.ts needed to move to managedBackup.ts's prune call
so the persisted Storage-page setting is honored on this path too (#13308).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:26:09 -03:00
小妍儿 ✨
27e0d9b5b7 fix(dashboard): refresh per-connection proxy badges after a proxy save (#13711)
* fix(dashboard): refresh per-connection proxy badges after a proxy save

ProxyConfigModal persists an assignment through
`PUT /api/settings/proxies/assignments`, but the provider page bound its
`onSaved` callback to `fetchProxyConfig()`, which only refetches
`GET /api/settings/proxy` into `proxyConfig`.

The per-account proxy badges read `connProxyMap`, which is filled from a
different endpoint (`GET /api/settings/proxy?resolve=<connectionId>`) by an
effect keyed on `[loading, connections]`. A proxy save changes neither key,
so the effect never re-ran and the saved (or cleared) proxy stayed invisible
until a manual page reload. Account- and combo-level saves refreshed nothing
visible at all; a provider-level save refreshed only the toolbar chip while
the rows that inherit that proxy stayed stale.

Adds `refreshProxyState()`, which re-reads both sources together, and binds
the modal's `onSaved` to it. The callback reads the latest connections from a
ref so it stays referentially stable and does not re-render consumers on every
connections fetch.

Fixes #13710

Also removes the now-unused `no-unused-vars` suppression entry for
useProviderConnections.ts. That entry was already stale on the base branch
(the same eslint invocation reports it on an unmodified tree), and the
pre-commit ratchet refuses to pass while a touched file carries one. Only
that single exact entry was pruned; no baseline was widened.

* docs(changelog): add fragment for #13711

* test(providers): raise proxySaveRefresh test timeout to 30s

The it() case dynamically imports ProviderModalsPanel, the first test in
the repo to pull in that module's ~15 modal components, which alone
consumes most of Vitest's default 5000ms testTimeout and made the test
flake under CI/runner load.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:26:01 -03:00
keii-2596
80ea176022 fix: feat(providers): add kimi/qwen/deepseek/gpt auto-routing families (#13214) (#13709) 2026-09-18 12:25:53 -03:00
Paijo
2374bbf1ee fix(mitm): bound SSE transcript retention and cancel abandoned upstream reads (#13702)
Handler-side collected strings grew without bound before the inspector
clamp; abandoned streams kept the reader alive for the full upstream
lifetime. createBoundedCollector caps retention at 1 MiB while keeping
true responseSize; pipeSSE and server.cjs cancel on downstream close.
Fixes #13395.

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
2026-09-18 12:25:45 -03:00
Kha Tran
994245a476 fix(translator): prevent schema property name collision in Gemini sanitizer (#13057, #13477) (#13690)
* fix(translator): prevent schema property name collision in Gemini sanitizer (#13057, #13477)

* refactor(translator): reuse SCHEMA_MAP_KEYS in forEachSubschema and add changelog fragment

* test(translator): avoid explicit any in gemini schema collision regression test

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: zcrew0x <zcrew0x@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:25:38 -03:00
dependabot[bot]
44e32a1995 deps: bump electron from 44.0.0 to 44.3.0 in /electron (#13664)
Bumps [electron](https://github.com/electron/electron) from 44.0.0 to 44.3.0.
- [Release notes](https://github.com/electron/electron/releases)
- [Commits](https://github.com/electron/electron/compare/v44.0.0...v44.3.0)

---
updated-dependencies:
- dependency-name: electron
  dependency-version: 44.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-18 12:25:30 -03:00
hummern
cc7d19daa9 fix(routing): skip redundant parseAutoPrefix for recognized built-in auto variants (#13647)
* Change hasFree from true to false for pioneer.ai

Pioneer.ai removed the free tier.

Before:

https://web.archive.org/web/20260516140358/https://pioneer.ai/pricing

After:

https://pioneer.ai/pricing

* fix(sse): skip parseAutoPrefix invalid-prefix warning for recognized built-in auto variants

resolveAutoRoutingState() already classifies auto/best-* variants correctly via
classifyAutoModel() before applyAutoPrefix() runs, and the old early-return
preserved that state — so the routing variant was never broken. The real,
observable defect was the spurious 'Invalid auto prefix format' warning logged
on every auto/best-* request, because parseAutoPrefix() only knows the short
aliases (VALID_VARIANTS) and returns valid:false for the best-* built-ins that
AUTO_TEMPLATE_VARIANTS recognizes.

Skip the warning (and the pointless early-return) for any model already present
in AUTO_TEMPLATE_VARIANTS. Add a regression test asserting the warning no
longer fires for auto/best-coding while an genuinely unknown auto/* variant
still warns (proving the log probe detects the message).

* fix(providers): drop out-of-scope Pioneer AI hasFree change from this PR

The Pioneer AI hasFree=false commit (c70e425) leaked into this branch
via a merge and is unrelated/stale vs. the release tip's current
hasFree=true value for this unrelated PR (#13647 is about auto-routing
prefix warnings). Reverting to keep the diff scoped to the actual fix.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Tobias Andersen <turbolego@gmail.com>
Co-authored-by: hummern <hummern@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:25:23 -03:00
Moseyuh333
31ea46934e fix(sse): recognize reasoning_effort in the reactive 400 field-strip retry (#13642)
* fix(sse): recognize reasoning_effort in the reactive 400 field-strip retry

Strict OpenAI-compatible gateways that don't implement the reasoning-effort
knob reject requests with 400 "Unsupported parameter: reasoning_effort".
findOffendingField() did not list it in KNOWN_OFFENDING_FIELDS, so the
generic strip-and-retry in base.ts never fired and the 400 surfaced to the
client — the request died instead of being retried once without the field.

Add "reasoning_effort" to KNOWN_OFFENDING_FIELDS (sibling of the existing
reasoning_budget entry, same FCC/NIM-style recovery) and pin the new match
in provider-field-strips.test.ts.

* chore(changelog): add fix fragment for the reasoning_effort field-strip retry (#13642)
2026-09-18 12:25:15 -03:00
luyuehm
1f8bfe52c5 feat(docker): add self-host compose + 5-minute deploy doc (RIC-739) (#13639)
KISS self-host carrier for the 零月费 + 自托管 product form. One command
brings up the published image + Redis on loopback — no profile choice, no
build step, no multi-tenant anything.

- docker-compose.selfhost.yml: pulls diegosouzapw/omniroute:latest + redis,
  all app ports 127.0.0.1-only by default, Redis not published to host,
  depends_on healthy, healthcheck wired.
- .env.selfhost.example: minimal env (2 EDIT ME lines), no secrets baked in.
- docs/getting-started/SELF_HOST_GUIDE.md: 5-minute deploy, sizing, exposing,
  data/backups, common issues, security checklist, what-it-is-NOT.
- meta.json + DOCKER_GUIDE cross-link.

Graduates to the full docker-compose.yml profiles when the user needs CLI
tools / web-cookie Chromium / sidecars.

Co-authored-by: Ant Rich <ant@richants.com>
2026-09-18 12:25:08 -03:00
Lukas
639d7dac44 docs(resilience): describe the queue-wait and execution deadlines as they are (#13624)
* docs(resilience): describe the queue-wait and execution deadlines as they are

Two docs still describe `requestQueue.maxWaitMs` the way it behaved before
the split, in two different and both incorrect ways:

  RESILIENCE_GUIDE.md  "a legacy persisted name for execution expiration
                        ... bounds limiter-managed execution, not time
                        spent in the local queue ... Queue residence has
                        no time deadline"
  ENVIRONMENT.md       "Max time to wait on a 429 before failing the
                        request"

`rateLimitManager.ts` does the opposite of the first and has nothing to do
with the second. `maxWaitMs` is the queue-wait budget: it covers the slot
wait plus QUEUED residence, and its timer is cleared the moment the job
starts executing (`wrappedFn`). Bottleneck's `expiration` is fed by
`executionMaxWaitMs` (default 600000ms), "never by the queue-wait budget"
in the source's own words, and is raised to the executor's fetch-start
timeout when that is longer.

Issue #13592 is that misreading in practice: the reporter concluded the
deadline "measures execution time after dispatch rather than queue wait"
-- which is what the guide says.

Also corrected while here:

  * The guide's "1-30000ms UI ceiling" is a different setting's bound
    (`comboCooldownWaitSettings`). `requestQueueSettingsSchema.maxWaitMs`
    is `min(1)` with no max, normalised to 1ms-24h.
  * Precedence is documented for the first time: the env var supplies the
    DEFAULT only, a persisted `resilienceSettings.requestQueue` value wins
    over it, and a per-connection `rateLimitOverrides` value wins over
    that. #13592 reports exactly this surprise -- setting
    `RATE_LIMIT_MAX_WAIT_MS` on a deployment that already has a persisted
    value changes nothing.

Documentation only; no behaviour change.

Refs #13592

* docs(changelog): add fragment for the resilience-deadline doc correction
2026-09-18 12:25:00 -03:00
jbovard2016
d574826f37 fix(usage): detach completed request previews (#13623)
* fix(usage): bound completed request retention

Completed request previews used V8 sliced strings that kept multi-megabyte request backing stores alive. Detach and byte-bound cached details, and add a credential-free profiler with cleanup and physical-retention assertions.

* fix(usage): give the JON-562 memory-profile canary realistic timeouts

The 100k-token worker step alone takes ~230s (tsx/esm boot of the full
route/handler module graph plus the real request lifecycle), well past
the driver's hardcoded 180s spawnSync timeout — the resulting SIGKILL
surfaces as `worker.status === null`, indistinguishable from a real
crash. Bump the worker timeout to 300s and the test's own outer/inner
timeouts to match the measured ~230-330s real runtime.

Also make git-branch provenance detached-HEAD safe: `git branch
--show-current` is empty on a detached HEAD (the normal state for a CI
PR checkout, and for this fix worktree itself), which made the canary
throw "git branch is empty" deterministically outside a regular branch
checkout.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:24:52 -03:00
Omkar Prabhu
9b84531e4c fix(cli): pass --legacy-peer-deps to npm install -g in omniroute update (#13579)
* fix(cli): pass --legacy-peer-deps to npm install -g in omniroute update

Problem
-------
Running \
pm install -g omniroute\ prints a wall of ERESOLVE / peer-dependency
warnings because marked-terminal@7.3.0 declares a peer range of marked>=1 <16,
while omniroute ships marked@18. npm's strict peer-resolution mode (the default
since npm 7) flags this mismatch loudly even though the packages work correctly
together at runtime.

Fix
---
Pass --legacy-peer-deps to the npm install -g call that \omniroute update\
issues so that every user who upgrades through the built-in updater gets a
clean, warning-free output. The dry-run log line is updated to match.

Why --legacy-peer-deps is safe here
------------------------------------
The repo already ships .npmrc with legacy-peer-deps=true (added in #11544) so
the published package documents this as its supported install mode. This commit
simply applies the same flag programmatically in the updater so the flag is
always honoured regardless of the caller's local npm config.

Changes
-------
- bin/cli/commands/update.mjs: append --legacy-peer-deps to execSync npm call
  and to the dry-run console.log so output matches the real command
- docs/guides/TROUBLESHOOTING.md: add a supported install snippet and clarify
  that residual deprecation notices come from third-party transitive packages
- tests/unit/cli-update-npm-win32-11335.test.ts: regression test asserting both
  the dry-run string and execSync call carry --legacy-peer-deps
- changelog.d/fixes/: add fragment (number updated after PR is opened)

* chore(changelog): rename fragment to PR #13579
2026-09-18 12:24:43 -03:00
mdigitalbh81
f24c3665df fix(proxy): skip bare TCP health probe for SOCKS5 data plane (#13571)
* fix(proxy): skip bare TCP health probe for SOCKS5 data plane

* docs(changelog): add fragment for SOCKS5 bare TCP probe skip fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:24:35 -03:00
Ryan
70eebe9adb fix(arena+analytics): atomic ELO sync (fetch-first) + flatRateAsZero in compression writer (#13446)
* fix(analytics+arena): flatRateAsZero in compression writer; atomic arena sync redesign

* docs(changelog): add fragments for arena ELO sync and compression flat-rate fixes

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: CrashCartCapital <crashcartcapital@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:24:28 -03:00
이창섭
a9c62ba83b fix(playground): improve Compare response scrolling and copy actions (#13317)
* feat(playground): copy individual compare responses

* docs(changelog): add fragment for Compare column copy-to-clipboard

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:24:19 -03:00
easypathuni
4dc73fb36a chore: add Windows helpers to run OmniRoute and Claude Code from a source checkout (#13312)
* chore: add Windows helpers to run OmniRoute and Claude Code from a source checkout

Adds contrib/windows/ with two double-clickable scripts for Windows users who
cloned the repo instead of installing the npm package:

- start-omniroute.bat  -> npm run dev (resolves the repo root from its own path)
- launch-claude.bat    -> node bin/omniroute.mjs launch, forwarding extra args

The README documents a fresh-clone gotcha on Windows with npm >= 11: the
optional better-sqlite3 dependency is silently skipped, the server falls
back to node:sqlite and logs "Module not found: Can't resolve
'better-sqlite3'". Since better-sqlite3@13 ships win32-x64 prebuilds inside
the package, extracting the npm pack into node_modules fixes it without a
compiler. Verified on Windows 11, Node 24.14.0, npm 11.6.1.

* chore(contrib): start Claude Code in the project folder, not the OmniRoute checkout

launch-claude.bat used to cd into the repo root before exec'ing
`omniroute launch`, so Claude Code always started inside the OmniRoute
checkout and loaded this repo's CLAUDE.md/AGENTS.md (~60k chars) into the
user's own coding session. Take the project folder as the first argument
(or prompt for it on double-click) and resolve bin/omniroute.mjs from the
script's own path instead. Remaining args are still forwarded to
`omniroute launch`.
2026-09-18 12:24:11 -03:00
Domenico Massafra
fbe195d69e fix(opencode): preserve catalog display names (#13168)
* fix(opencode): preserve catalog display names

* test(cli): cover OpenCode catalog display-name precedence

Adds the automated unit test the PR body's manual smoke check
(Auto Chat / DeepSeek V4 Pro) was standing in for, covering all four
name-precedence branches: existing custom name, catalog display_name,
native catalog name with owned_by prefix stripped, and the auto/* readable
fallback. Also adds the changelog.d/fixes/ fragment.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:24:03 -03:00
Anh Tran
93be9d544c fix(responses): count input tokens locally for Codex OAuth (#13167)
The ChatGPT subscription backend does not serve
/backend-api/codex/responses/input_tokens for the affected account. Native
requests to that path are intercepted by an OpenAI Cloudflare managed challenge,
while the same path over the bundled Chrome transport returns 404 Not Found.
Forwarding the client preflight can therefore never return a useful count and,
before the companion classifier fix, permanently disabled the healthy Codex
connection on the first challenge.

Add a static /v1/responses/input_tokens route that shadows the generic
Responses passthrough, uses the existing offline o200k_base token counter, and
returns the standard response.input_tokens contract without issuing any upstream
request. Count instructions, structured input, tool definitions and config;
apply a conservative five-percent margin so the failure mode is earlier client
compaction rather than a context-window overflow.

Preserve the API-key and model-policy boundary from the catch-all Responses path.
Tests pin the public schema, prove fetch is never called, cover text,
instructions, structured input, tools, non-text parts, server-held context ids,
invalid JSON, OPTIONS, and the conservative lower bound.

Co-authored-by: anhth2 <anhth2@vng.com.vn>
2026-09-18 12:23:55 -03:00
marioschoenert-code
6614780c36 fix(deps): declare remark-gfm as a direct dependency (#13162)
MarkdownMessage.tsx and other client components import remark-gfm
directly, but it was only present transitively via fumadocs-mdx (a
devDependency). npm's hoisting happens to resolve it, masking the
issue, but pnpm's strict node_modules isolation fails with
"Module not found: Can't resolve 'remark-gfm'" since the package was
never declared as a direct dependency of the app.

Add it explicitly to package.json/package-lock.json and the
supply-chain dependency allowlist.

Co-authored-by: marioschoenert-code <291579285+marioschoenert-code@users.noreply.github.com>
2026-09-18 12:23:48 -03:00
Ozeas Souza
b71f466f1c fix(authz): preserve zed-hosted native-app callback through root middleware redirect (#13140)
* fix(release): let the Electron workflow start again — grant actions:read to the npm leg (#11973)

v3.8.50 shipped with zero desktop assets. The tag push did trigger electron-release.yml
(run 33005490476) but GitHub refused the run at startup:

  Error calling workflow 'npm-publish.yml@5458026'. The nested job 'publish' is
  requesting 'actions: read', but is only allowed 'actions: none'.

npm-publish.yml's `publish` job gained `actions: read` (it downloads the next-build
artefact) and the caller job here never widened its grant — a reusable workflow may not
request more than its caller allows, and the refusal is a startup failure of the WHOLE
run, so the `release` job that attaches the installers, the source archives and the
SBOM never ran either. Nothing about it is visible through the API (no jobs, no
check-runs); only the run page shows the annotation.

- publish-npm: `actions: read` added, with the rule written down (keep the block a
  superset of every job in npm-publish.yml).
- workflow_dispatch: new boolean input `publish_npm` (default true) and the npm leg
  is gated on it, so re-attaching assets to a release whose package already shipped
  does not try to publish the same version twice.
- web-build / build / release checkouts pin `ref: needs.validate.outputs.version`:
  a dispatch builds the tag it names, not the dispatching branch (a tag push resolves
  to the same commit, so nothing changes on the normal path).

actionlint clean; electron-release-desktop-channel-8949, electron-release-efficiency,
build-next-isolated-windows-home-2402, electron-release-latest-yml.repro and
check-workflows suites pass. Next step: dispatch on main with version=v3.8.50 and
publish_npm=false to attach the missing assets.

* fix(ci): stop a stalled Codecov upload from cancelling the Coverage job and the main run (main twin of #11972) (#11978)

Same change as #11972 on release/v3.8.51: the Coverage job had timeout-minutes: 20,
the c8 merge across 8 shards takes ~10 min and the informational Codecov upload hung
for the rest of the budget on two consecutive main runs (33207760653, 33215115341),
ending the job cancelled and turning the run's conclusion cancelled with every
blocking job green. Codecov step: 5-minute ceiling + continue-on-error; job: 30 min.

* fix(release): resync the electron lockfile and let a dispatch build from a repaired ref (#11982)

* fix(release): resync the electron lockfile and let a dispatch build from a repaired ref

The v3.8.50 desktop re-dispatch (run 33238093090) lost its Linux leg at
`npm ci` in electron/: "Missing: electron-builder-squirrel-windows@26.15.3 from
lock file" plus its 12 transitive entries — the optional Windows-installer subtree of
electron-builder had been dropped when the lock was last regenerated, and no CI ran
the desktop legs between then and the tag (v3.8.49 never ran them; v3.8.50 died at
startup, #11973). `npm install --package-lock-only` restores the 13 entries; a clean
`npm ci --ignore-scripts` on the result adds 284 packages with no complaint.

The tag itself carries the broken lock, and the workflow now checks out the tag on
dispatch (#11973), so a dispatch input `build_ref` (default: the version tag) lets the
operator name the repaired line — the v3.8.50 assets will be rebuilt from main, which
is 3.8.50 plus its post-release fixes. Push-triggered runs are unaffected.

actionlint clean; electron-release-desktop-channel-8949, electron-release-efficiency,
electron-release-latest-yml.repro and check-workflows suites pass.

* fix(release): do not regenerate release notes on a re-attach dispatch

`generate_release_notes: true` on an existing release APPENDS GitHub's auto-generated
"What's Changed" block to the curated body — the v3.8.50 re-dispatch (run 33238093090)
added 1,416 chars to the 121 KB notes. Only the tag push should generate notes.

* fix(release): attach the SBOM to the GitHub Release on dispatch publishes too (#12020)

The step was gated on github.event_name == 'release'. v3.8.50's package shipped
through a workflow_dispatch (the staged publish, 11 attempts) and the step was
skipped, so the GitHub Release carried no SBOM — it was attached by hand from the
run's sbom-npm artifact (5.0 MB, 1,886 components). Now it attaches on release or
workflow_dispatch whenever a release for the published tag exists, and says so
when it does not (the workflow artifact remains the durable copy either way).

actionlint and prettier clean; npm-publish-artifact-provenance and
check-workflows-provenance-runner suites pass.

* fix(release): drop the build_ref input — a dispatch builds the ref it is dispatched on (#12032)

Twin of #12022 on main: CodeQL flagged the same input-controlled checkout + npm cache pattern (cache-poisoning/poisonable-step) on main since it's the default branch. Checkouts go back to github.ref; dispatch still works via --ref (documented in the workflow's own on: contract).

Also fixes the packaged-app smoke: it now waits on /api/monitoring/health (which touches the DB) instead of /login (which doesn't), so the smoke can actually distinguish "native driver selected" from "database never opened." electron-smoke-script.test.ts 9/9 (2 new cases).

* fix(ci): accept CVE-2025-68121 in the prebuilt tls-client .so, auto-close base-red issues, guard Scorecard on the default branch (main twin) (#12086)

* fix(ci): accept CVE-2025-68121 in the prebuilt tls-client .so, auto-close base-red issues, guard Scorecard on the default branch

- .trivyignore: CVE-2025-68121 (Go stdlib crypto/tls inside bogdanfinn/tls-client
  v1.15.1, built with go 1.24.1) with justification, expiry and tracker #12084.
  No upstream rebuild exists; the blocking Trivy gate now also names the ignore
  file explicitly.
- nightly-release-green: close the "not green" issue when the validation passes
  again (the workflow only ever opened/commented it, so stale issues outlived
  the fix and stamped new PRs as base-red inherited).
- scorecard: the action only accepts the DEFAULT branch (the active release
  branch, not main) - guard the job on it so pushes to main stop failing.

Refs #12084

(cherry picked from commit 8adf34bada)

* fix(release): never let the tag-push Create Release append auto notes to the curated body

Twin of the release/v3.8.51 commit (see #12085).

Refs #12084

* fix(docker): bump Bun image to 1.4.0 with Turbopack and port the node image's build memory guards (#11719)

Validated in an isolated worktree against main: typecheck:core clean, 15/15 focused tests pass (docker-build-memory-budget, bun-support, resolve-next-build-bundler-flag). Root cause confirmed against the current workflow config (docker-publish.yml triggers on push to both main and release/v*, so this genuinely needed to target main). One out-of-scope change dropped before merging: config/alibaba-free-tier-allowlist.json's validUntil bump (2026-08-27 -> 2027-12-31) was unrelated to the Docker/Bun fix — reverted to the current value, keeping only the Docker/Bun/memory-guard changes this PR is actually about. Thanks for the thorough root-cause writeup and the worker-pool math.

* test(infra): retry recursive temp-dir removal on main (main twin of #11968) (#12246)

* test(infra): retry recursive temp-dir removal on main (main twin of #11968)

`main` has been red since b342c1a361 on the vitest and integration gates:

  ✖ tests/unit/autoCombo/provider-family-combos.test.ts > auto/<family>
  ✖ chat pipeline applies Codex OAuth fingerprint and priority tier inside combos

Both call resetStorage() from beforeEach, which does an fs.rmSync(TEST_DATA_DIR,
{recursive: true, force: true}) with no retry, and intermittently loses the race
with a not-yet-released SQLite handle (ENOTEMPTY).

release/v3.8.51 fixed this in #11968 with a mechanical codemod adding
maxRetries/retryDelay to every recursive rm/rmSync/rmdirSync under tests/, but
that PR landed only on the release branch. Because main only receives work at
the release squash, it stayed broken for the whole cycle — and repo-wide gates
then turn every open PR into main red on checks unrelated to their diff.

This is the --base main twin: re-runs the same codemod that already shipped on
the release branch (scripts/ad-hoc/codemod-rm-maxretries.mjs), so the two
branches converge on identical test-teardown semantics. Test-only; no product
logic is touched.

The remaining three failures reported on #12133 (unit full suite exceeding its
4800s ceiling, package-artifact exceeding 1200s, and the boot-smoke that is
skipped as a consequence) are runner-contention timeouts, not code defects —
validate-release-green.mjs runs those heavy gates concurrently on one shared
hosted runner. There is no fix to port for those.

* chore(scripts): carry the rm-maxretries codemod onto main alongside its output

The codemod that generated the previous commit lives in the repo on
release/v3.8.51 (added by #11968) but was never on main. Bringing it over keeps
the tool next to the change it produced, so the transformation stays
reproducible and auditable from either branch.

* fix(ci): port the release-green ESLint gate fix to main (base-red #12363) (#12618)

Porta para `main` o fix do gate de ESLint que só havia entrado na branch de release — o padrão de PR-companheiro que `_shared/merge-gates.md` §8 prescreve.

As 12 falhas de CI foram discriminadas como o **outro** base-red do main, não deste diff. Todas descendem de um único ponto: `Package Artifact` falha e os 9 shards de E2E mais os 2 Electron Package Smoke consomem esse artefato. A própria issue #12363 lista os dois separadamente:

- ` ESLint: could not parse eslint json` — que é justamente o que este PR conserta;
- ` Package artifact (npm pack policy): gate exceeded its 1200s ceiling` — a raiz da cascata.

O PR toca apenas `scripts/quality/validate-release-green.mjs` e seu teste, então não tem caminho para afetar o build do pacote. Teste portado primeiro e falhando no script atual do main (TDD).

* fix(authz): preserve zed-hosted native-app callback through root middleware redirect

The root middleware intercepts `pathname === "/"` and redirects to
`/dashboard` using `new URL(basePath+"/dashboard", url)`, which drops
the query string entirely.

Zed's native-app sign-in always redirects the browser to the loopback
root — `http://127.0.0.1:<port>/?user_id=...&access_token=...` — ignoring
any path. When the dashboard's own loopback port is reused as
`native_app_port` (see `src/lib/oauth/providers/zed-hosted.ts`'s
`resolveDashboardLoopbackPort`), that redirect lands on `/` of the running
OmniRoute instance. The root page (`src/app/page.tsx`) was already written
to forward `user_id`+`access_token` to `/callback`, but this middleware
runs first and silently discards the payload — making page.tsx's forward
dead code and breaking the entire zed-hosted sign-in flow.

Fix: detect `user_id` + `access_token` in `searchParams` and, when
present, redirect to `/callback${search}` (preserving the query string)
instead of `/dashboard`. Regular root visits (no native callback params)
continue to redirect to `/dashboard` unchanged.

This approach mirrors what `src/app/page.tsx` already does and is
provider-agnostic: any future provider whose native loopback callback lands
on `/` with `user_id`+`access_token` params benefits automatically.

* test(authz): cover zed-hosted native-app callback root redirect (#13140)

Adds automated coverage for the new pathname === "/" branch: with
user_id+access_token both present the redirect now forwards to
/callback preserving the query string; with only one of the two
present, behavior is unchanged (redirect to /dashboard).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Rouzbeh† <78313022+rqzbeh@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:23:39 -03:00
Bl0ck
62d14a7fc7 feat(audio): expand Fish Audio S2.1 and voice cloning (#13090)
* feat(audio): expand Fish Audio S2.1 and voice cloning

* fix(audio): type Node streaming request init

* test(audio): align Fish Audio CI expectations

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:23:31 -03:00
Tuan Dinh
45fa62a18e feat(build): add build:fast and start:fast to bypass standalone tracing (#13021)
* feat(build): add build:fast and OMNIROUTE_SKIP_STANDALONE to bypass standalone tracing

* feat(build): add start:fast script to run non-standalone builds locally

* docs(changelog): add fragment for #13021
2026-09-18 12:23:23 -03:00
Tuan Dinh
e188da6399 fix(dev): allow Ctrl+C to promptly kill dev server by closing active connections (#13020)
* fix(dev): allow Ctrl+C to promptly kill dev server by closing active connections and adding force-exit timeout

* test(dev): add regression assertions for prompt dev server exit on Ctrl+C

* docs(changelog): add fragment for #13020
2026-09-18 12:23:15 -03:00
Tuan Dinh
4587c7ec3a fix(mitm): add catch-all (*) model mapping fallback for Agent Bridge (#12140) (#13013)
* fix(mitm): add catch-all (*) model mapping fallback for Agent Bridge (#12140)

* docs(changelog): add fragment for #13013
2026-09-18 12:23:07 -03:00
Zach Frederich
80a2bb1230 fix(cli): skip POSIX path lookup on Windows autostart (#12993)
* fix(cli): skip POSIX path lookup on Windows autostart

* docs(changelog): record Windows autostart path fix
2026-09-18 12:22:59 -03:00
Nguyễn Viết Tuấn
d5452d03e7 feat(codex): safely discover compatible models (#12933)
* feat(codex): safely discover compatible models

* docs(changelog): add fragment for #12933

* fix(codex): drop the duplicate GPT-6 Astra registry entries from the merge

release/v3.8.51 had already landed the seven gpt-6-astra* models, and the
merge kept both copies, so the Codex registry listed every Astra id twice.
Keep the release's entries (same ids, capabilities and timeouts) and drop
this branch's copies.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: TheDemonTuan <nguyenviettuanbp@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:22:50 -03:00
ZaimMarzuki
7f1b4a5eb7 feat(dashboard): add sidebar pinned items shortcut section (#12891)
* feat(dashboard): add sidebar pinned items shortcut section

* docs(changelog): add fragment for sidebar pinned items feature

* docs(changelog): update pr number in changelog fragment

* fix(dashboard): scale down sidebar pinned item icons to match text proportions

* fix(dashboard): remove redundant pin icon from PINNED category header

---------

Co-authored-by: ZaimMarzuki <ZaimMarzuki@users.noreply.github.com>
2026-09-18 12:22:42 -03:00
botea
9eb7c2f17e feat(compression): add Hungarian Caveman language pack (#12825)
* feat(compression): add Hungarian Caveman language pack

* docs(changelog): add Hungarian Caveman entry

* chore(changelog): make the fragment a well-formed bullet

changelog.d/ fragments must start with "- " (scripts/release/aggregate-changelog.mjs).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: botii16 <botii16@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:22:34 -03:00
KeelTrace
95d3b164a5 fix(models): preserve free-model metadata from discovery (#12763)
* fix(models): preserve live free economics in synced discovery

* docs(changelog): add fragment for free-model metadata discovery fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:22:26 -03:00
Goni Sulaiman
5c305680ac fix(sse): strip internal markers from the dario and 9router request bodies (#12729) (#12735)
`dario` and `9router` both override transformRequest() without calling the base
implementation, so the internal-marker strip that runs inside
BaseExecutor.transformRequest() never applied to their bodies, and the second
strip before dispatch in BaseExecutor.execute() is not on their path either.
Whatever the routing layer left on the request — including the context-relay /
universal-handoff markers — went upstream verbatim, and strict
OpenAI-compatible gateways reject unknown top-level keys with HTTP 400.

glm and gitlab look like the same class of bypass but are not: glm's
transformRequest() calls super, so the shared strip already covers it, and
gitlab rebuilds its payload field by field, so no extra key can survive to the
wire.

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
2026-09-18 12:22:17 -03:00
Juri
db5ae3c33d docs(dependencies): clarify socket.yml is registry-side scan, not CI gate (#12664)
* docs(dependencies): clarify socket.yml is registry-side scan, not CI gate

* docs(dependencies): add changelog fragment for socket.yml scope note

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:22:09 -03:00
wofiporia
6e74739607 fix(pricing): accept sync-written fields on PATCH and surface actionable save errors (#12629)
* fix(pricing): accept sync-written fields on PATCH and surface actionable save errors

* docs(changelog): add fragment for #12629

---------

Co-authored-by: wofiporia <172453170+wofiporia@users.noreply.github.com>
2026-09-18 12:22:00 -03:00
ZaimMarzuki
060f70ed18 feat(dashboard): show exact token counts on hover in usage analytics cards and tables (#12553)
* feat(dashboard): show exact token counts on hover in usage analytics cards and tables

* fix(dashboard): lock tooltip position to prevent top-left slide animation

---------

Co-authored-by: ZaimMarzuki <ZaimMarzuki@users.noreply.github.com>
2026-09-18 12:21:52 -03:00
Gaul Samuyenga
43758c7885 fix(cli): support dashboard API key ids (#12520)
* fix(cli): support dashboard API key ids

* docs(changelog): add fragment for CLI API key route drift fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:21:44 -03:00
Mr White
5ba3eee2b0 fix(devin): treat Devin CLI model ids as literal — never strip or synthesize effort suffixes (#12492)
The Devin CLI providers (devin-cli, devin-cli-agentic, devin-desktop; aliases
dv/dva) serve a catalog whose model ids EMBED the reasoning tier:
claude-opus-5-low, claude-opus-5-medium, … and gpt-5-6-sol-max/-low are
distinct upstream models (see registry/devin/catalog.ts).

applyClaudeEffortVariant stripped the trailing -{low,medium,high,xhigh,max}
from any id whose base is a known Claude model, regardless of provider. For
Devin lanes this dispatched a base id that does not exist upstream, e.g.

  dva/claude-opus-5-low  ->  claude-opus-5  ->  400
  'Model is not present in the current Devin catalog: claude-opus-5'

Only accidental double-suffixed ids (dva/claude-opus-5-max-low) survived,
because stripping the outer -low left the real claude-opus-5-max. Symmetrically,
the catalog synthesized -<level> variants on top of tier-embedded ids,
advertising phantom ids (dva/gpt-5-6-sol-max-low, dva/kimi-k3-*) that 400 when
called.

Three gates now treat Devin ids as literal:
- applyClaudeEffortVariant: early return for Devin providers (ids/aliases)
- appendClaudeEffortVariants: no -<level> variants for devin-prefixed ids
- appendSyncedEffortVariants: isSkippedEffortProvider now covers Devin
  providers (they own their suffix mechanism — the tier IS the id)

Validated live on a self-hosted v3.8.51 deployment: dva/claude-opus-5-low,
dva/claude-5-fable-low and the whole tier-embedded catalog now dispatch; the
phantom variant ids disappear from /v1/models. Claude-lane stripping
(claude/cc, e.g. cc/claude-opus-5-high -> claude-opus-5 + reasoning_effort) is
unchanged and covered by existing + new characterization tests.

Co-authored-by: Neuron Mr White <whiteneuron@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:21:36 -03:00
dmlanday
b36c81d3f0 fix(cli): escalate the readiness probe timeout so a slow health response is not a phantom boot failure (#12484)
* fix(cli): escalate the readiness probe timeout so a slow health response is not a phantom boot failure

`omniroute serve` reported "Server did not respond within 60s" over servers that
were up and serving traffic. Every probe of /api/monitoring/health was aborted at
a fixed 2s, and a timed-out probe is classified "hanging", which never counts
toward readiness (#6800). So whenever the first health response takes longer than
2s the poll can never succeed: each abort discards the in-flight request before
the route finishes (its own 1s payload cache is never populated either), and
500ms later the next probe restarts the same work into the same ceiling, for the
whole 60s budget. Reproduced by the new test: against a health route that answers
200 in 3.2s, the old poller ran 12 probes over 30s and reported ready=false every
time.

The per-probe timeout now escalates after each hang (2s, 4s, 8s, 15s), clamped to
the time left in the budget so the caller's total timeout still holds. Only a hang
escalates, so #6800's guarantee is unchanged: a socket that accepts TCP and never
answers still resolves false. waitForServer also reports each probe outcome to an
optional onOutcome callback, and the readiness-timeout diagnostic uses it to say
whether the port was accepting connections, which separates "up and still warming"
from "never bound the port".

Same failure family as #10508, which fixed it by taking a DNS lookup out of the 2s
budget rather than by widening it. The heavy /api/monitoring/health route is what
makes that budget tight in the first place (its own docstring points high-frequency
pollers at /api/health/ping, which is what the Electron readiness poller uses);
switching the CLI probe route is a larger change, left as a follow-up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2

* chore(changelog): link the readiness-probe fix to PR 12484

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CJf2dxEpiwZqyZujWk57T2

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:21:27 -03:00
killer30001000
d71d0f76e5 fix(usage): render OpenRouter PAYG credit pool with real denominator (#12468)
* fix(usage) handle OpenRouter PAYG credit percentage

OpenRouter PAYG accounts without a per-key limit previously rendered
the credits row as 'total: 0, remainingPercentage: 100, unlimited: true',
treating /credits balance as unlimited even when a real credit pool was
present. Route the credit pool through the credits renderer with the
real denominator: total = totalCredits when positive, used = total -
creditBalance, remaining = creditBalance, remainingPercentage =
round(balance / total * 100), isCredits: true, unlimited: false. Per-key
limit still wins. A non-positive pool surfaces the row but never invents
a 100% bar.

Tests cover: explicit key limit, PAYG account credits without key limit,
key limit taking priority over account credits, and a balance without a
positive denominator.

* fix(usage) render OpenRouter PAYG quota as a metered percentage bar

The frontend parser was routing every OpenRouter 'credits' quota through
buildCreditsQuota(), which sets isCredits: true. QuotaCardExpanded
short-circuits on that flag and shows only the USD balance as a bare
number, so a real PAYG payload (used: 7.33, total: 10, remaining: 2.67,
remainingPercentage: 27) was rendered as '$2.67' instead of the '27% left
/ 7.33 / 10' bar the backend already computed.

Drop isCredits: true for any payload whose total is a positive finite
number - the row then goes through the normal normalizeQuotaEntry() path
with currency preserved as an extra. The balance-only fallback (total 0
or non-finite denominator, used by legacy /credits responses) still uses
buildCreditsQuota() so the row stays renderable, and never invents a
100% percentage.

The frontend test now asserts:
- PAYG positive denominator -> total: 10, remainingPercentage: 27,
  currency: 'USD', isCredits !== true.
- Balance-only payload -> isCredits === true, creditCount === 2.67,
  total: 0, no fabricated 100%.
- NaN denominator -> balance-only fallback.
- Non-credits keys -> unchanged normalizeQuotaEntry() path.
- Mixed payload -> normal quota row + PAYG row, both kept.

* docs(changelog): add OpenRouter PAYG fix fragment

* docs(changelog): remove self credit
2026-09-18 12:21:17 -03:00
Kareem Jalal
ad633c8440 fix(memory): word/sentence-boundary aware fact truncation (#12383)
* fix(memory): word/sentence-boundary aware truncation in extraction

sanitizeMatch() and capExtractionText() previously did raw character-offset
slices (slice(0, MAX_FACT_LENGTH) / slice(-MAX_EXTRACTION_TEXT_LENGTH)) with
no boundary awareness, producing garbled mid-word/mid-clause fragments that
get injected into LLM context as memory facts.

- sanitizeMatch() now backs the cut off to the nearest sentence-ending
  punctuation (. ! ?) within a lookback window, falling back to a plain
  whitespace boundary, falling back to the original hard cut only when no
  boundary exists nearby.
- capExtractionText() applies the equivalent boundary-aware trim on the
  front edge of the kept tail.

Mirrors the boundary-aware truncation pattern already used by
open-sse/services/compression/lite.ts (#8169) for tool-result truncation.

Adds tests/unit/memory-extraction-boundary-truncation.test.ts covering
word-boundary cuts, sentence-boundary preference, short-string passthrough,
the no-boundary-available fallback, and capExtractionText's tail behavior.

* docs(changelog): add fragment for word/sentence-boundary fact truncation

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:21:10 -03:00
Amirreza Kimiyaei
b84059213f fix(gemini): preserve response-schema nullability across union flattening (#12310)
* fix(gemini): preserve response-schema nullability across union flattening

cleanJSONSchemaForAntigravity flattens every union spelling of nullable before
the schema reaches Gemini: flattenTypeArrays turns ["string","null"] into
"string" and flattenAnyOfOneOf drops the {"type":"null"} branch. Correct for
tool parameters, wrong for response schemas — a model with nothing to say can
no longer answer null, so it returns the string "null" or fabricates a value,
and either reaches the client as schema-conformant data. Pydantic emits the
anyOf spelling for Optional[str], so the fabricating path is the common one.

A Phase 1b walk now records Gemini's sibling-key spelling, nullable: true, on
any node whose union carries null — before Phase 2 destroys the evidence. The
key is absent from GEMINI_UNSUPPORTED_SCHEMA_KEYS so it survives sanitizing,
and flattenAnyOfOneOf's Object.assign cannot clobber a key the surviving
branch lacks. Opt-in via { preserveNullable: true }, passed only by the
responseSchema call site; the three tool-parameter call sites keep the default.

Closes #12308

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changelog): add fragment for #12308 gemini nullable schema fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:21:02 -03:00
Paijo
023a57476f feat(chat-admission): expose admission tunables via dashboard settings (#12038)
* feat(chat-admission): add settings store for admission tunables

* fix(chat-admission): extract parseEnvNumber to reduce cyclomatic complexity

* fix(chat-admission): repair settings store write path and add coverage

The settings store could not persist anything: `updateChatAdmissionSettings`
targeted an `updated_at` column that `key_value` does not have (the schema is
namespace/key/value — src/lib/db/core.ts), so every write threw
`table key_value has no column named updated_at`.

Also fixes, found while adding the tests:

- `getChatAdmissionSettingsSource` returned a partial map (only the keys whose
  layer differed from the default) and dropped the unset keys entirely, so a
  dashboard reading it could not render a complete row.
- env parsing used `parseFloat` for the shed ratio, so `"0.5x"` was silently
  accepted as 0.5 while `chatBodyAdmission.ts` rejects that same input — both
  paths now share one per-field predicate table.
- DB reads validated `typeof === "number"` but not integrality/range, so a
  hand-edited row could serve `2.5` or `-1` to the admission controller.
- writes persisted unvalidated input.
- malformed, non-object, and partial rows are now tolerated per field.

Adds tests/unit/db-chat-admission-settings.test.ts (17 cases) covering CRUD
round-trips, namespace isolation, reset, env parsing/validation boundaries,
env-over-DB precedence, provenance, normalization on write, and malformed-row
tolerance, per Hard Rule #8.

Verification: eslint clean; `npm run typecheck:core` clean; the new suite plus
the two sibling settings suites pass 63/63; check-complexity-ratchets reports
complexityNewCode=0; check-db-rules OK; check-env-doc-sync OK (all three vars
are already documented in .env.example).

---------

Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
2026-09-18 12:20:53 -03:00
Diego Rodrigues de Sa e Souza
d8be3b1a77 feat(sse): reserve the Antigravity account for the request's stream lifecycle (re-land of #10011) (#13929)
* feat(sse): reserve the Antigravity account for the request's stream lifecycle

Re-land of the account-lease half of #10011 on the current release branch.
Its exact-model-scoping half had already shipped in #8050 and its quota half
lost to the tip's aggregate-family design (selectAntigravityQuotaWindowNames /
antigravityQuotaFamily.ts); none of that is reintroduced here. The lease is a
concurrency reservation only and never reads or writes quota state.

The Antigravity account selected for a request is reserved for the whole
streaming lifecycle of that request, so a concurrent retry — or the credential
handoff inside getProviderCredentialsWithQuotaPreflight — cannot re-pick an
account already committed to an in-flight upstream stream. The reservation is
scoped to (connection, callable upstream model) rather than the whole account,
so one account can still serve two different models at once; catalog ids that
resolve to the same upstream id (the gemini-3.7-flash tiers, all
gemini-3.7-flash-tiered) share one lease. When every eligible account is leased
for that model the request returns a structured 503 antigravity_pool_busy with
a bounded Retry-After instead of piling onto a busy account.

Opt-in behind ANTIGRAVITY_ACCOUNT_LEASE_ENABLED (runtime, default false). With
the flag off no reservation is taken, credentials carry no routing descriptor,
every release/hold is a no-op on an undefined lease id, and account selection
and dispatch behave exactly as before.

#10011's original test suite asserted family semantics for a lease that was
exact-model scoped and failed deterministically on its own head; the model ids
it used (gemini-3.5-flash / gemini-3-flash-agent) no longer exist in the
catalog. The contradiction is resolved in favour of one coherent semantic —
exact callable upstream model — and the tests assert it against the alias
tables as they are on this branch.

Co-authored-by: Ardem2025 <openclaw-auto@example.invalid>

* fix(sse): widen the Antigravity lease reservation result so auth.ts narrows it

The discriminated-union form of reserveAntigravityLeaseForSelection's return type
did not narrow under tsconfig.typecheck-api.json, so reading `reserved.lease`
after the `reserved.busy` early return raised TS2339 in the API Route Typecheck
gate. A single optional-property shape carries the same information and type-checks
everywhere.

Co-authored-by: Ardem2025 <openclaw-auto@example.invalid>

---------

Co-authored-by: Ardem2025 <openclaw-auto@example.invalid>
2026-09-18 12:09:02 -03:00
voidstack
c07cebbab7 fix(db): close failed initialization connections (#13342)
* fix(db): close failed initialization connections

* docs: add changelog fragment for #13303 db handle-leak fix

---------

Co-authored-by: voidstackloop <voidstackloop@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:00:16 -03:00
chatchawan-simplewish
04eba4dc05 fix(mcp): load audit sqlite via runtime helper (#13223)
* fix(mcp): load audit sqlite via runtime helper

* docs(changelog): add fragment for MCP audit sqlite runtime-require fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Codex <codex@openai.com>
Co-authored-by: chatchawan-simplewish <chatchawan-simplewish@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 12:00:07 -03:00
Aref Alapour
e780da3578 fix(catalog): advertise input_modalities on vision-capable combos (#12799)
* fix(catalog): advertise input_modalities on vision-capable combos

A combo whose merged capabilities carry vision:true (e.g. an
operator-flagged #9195 vision head, or canonical vision with no synced
modality data) advertised the boolean with an empty modality set, so
models.dev-shaped clients that key off input_modalities still saw a
text-only entry. buildComboCatalogMetadata now derives the modalities
from the vision verdict it already advertises via
visionDerivedModalities() in catalogHelpers; synced modality
intersections keep precedence and nothing is derived for unknown or
text-only verdicts (fail-closed, same discipline as #4071/#4072).

catalog.ts stays at its frozen LOC (spreads collapsed into the helper
call). Regression-tested in models-catalog-combo-metadata.test.ts.

Refs #12798

* changelog: fragment for #12799

---------

Co-authored-by: aref-alapour <aref-alapour@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:59:58 -03:00
Amirreza Kimiyaei
04cc8aab67 fix(cache): fold the response output contract into the semantic cache signature (#12309)
* fix(cache): fold the response output contract into the semantic cache signature

The signature hashed only {model, messages, temperature, top_p}, so two temp=0
requests with identical messages but different response_format shared a cache
key: the second was served the first's stored body under a 200, violating the
schema it asked for. tools/tool_choice had the same exposure.

generateSignature now takes an optional output contract — response_format,
text.format, tools, tool_choice, collected by outputContractOf() — and folds it
into the digest only when present, so plain-chat signatures (and every cache
entry already written for them) are unchanged. All three call sites pass it;
read/write symmetry is preserved because bodyForCacheWrite snapshots the same
body object the read path hashed (#cache-signature-asymmetry).

Closes #12307

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(changelog): add fragment for #12307 semantic-cache output-contract fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(cache): populate both constraint spellings in outputContractOf

The merge with #12734 left generateSignature reading the camelCase
constraints (toolChoice/responseFormat) with a snake_case fallback, but
outputContractOf only filled the snake_case keys, so the #12734
"signature is called with tool_choice/tools/response_format from body"
store tests failed on the merged branch. Set both spellings so either
caller shape reads the value it expects.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: amirrezakm <amirrezakm@users.noreply.github.com>
2026-09-18 11:59:50 -03:00
Jasmin Sehic
aecd50369b fix(sse): treat "length" stop_reason as legitimate in detectMalformedNonStream for Claude messages (#12935)
* fix(sse): treat "length" stop_reason as legitimate in detectMalformedNonStream for Claude messages

* docs(changelog): add fragment for length stop_reason fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: jasminsehic <jasminsehic@users.noreply.github.com>
2026-09-18 11:59:41 -03:00
AnhLead
6788de8ef9 feat(providers): update Openference free models and add Deyin to compatible agents (#13378)
* feat(providers): update Openference free models and add Deyin to compatible agents

* docs(providers): regenerate PROVIDER_REFERENCE.md against the current tip

Post-merge regeneration so the diff only reflects the Openference free-model
addition, not stale eurouter/greenpt/count churn from an out-of-date local
generation.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(providers): soften unconfirmed Openference free-forever claim

The Openference pricing page (openference.com/pricing) currently lists five
paid plans ($15-$120/mo) and no $0 tier in its structured pricing data, so
neither the old "3-day trial" note nor a "free forever" claim can be verified
against the source. Point readers to the pricing page instead of asserting a
specific duration.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: AnhLead <AnhLead@users.noreply.github.com>
2026-09-18 11:59:33 -03:00
ducphamtien-fonos
128f06d645 fix(translator): support Responses custom tool choice (#13128)
* fix(translator): support Responses custom tool choice

* fix(translator): preserve custom tools across response paths

* docs(changelog): add fragment for Responses custom tool choice fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Pham Tien Duc <phamtienduceng@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: ducphamtien-fonos <ducphamtien-fonos@users.noreply.github.com>
2026-09-18 11:59:24 -03:00
luyuehm
5a82da7084 feat(routing): deterministic routing strategies for self-hosted entry (RIC-740) (#13611)
* feat(routing): self-hosted unified OpenAI-compatible entry (RIC-738)

Divert /v1/chat/completions through the self-hosted provider adapters when
OMNIROUTE_SELF_HOSTED_PROVIDERS / OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE is set:
one OpenAI-compatible contract in, auto-route to the selected provider
(x-omniroute-provider header, provider/model prefix, or first provider),
standard OpenAI error shape out. Optional OMNIROUTE_SELF_HOSTED_API_KEY guards
the entry (D5 reserved); unset = open loopback route. Upstream credentials stay
runtime-only and are stripped from echoed responses.

Brings in the provider-adapters baseline from sibling branch (RIC-737) that
this entry depends on. Includes 21 passing unit tests (provider selection,
model-prefix forwarding, header hygiene, auth, error normalization, SSE
passthrough, fall-through/misconfig), docs, env example, changelog fragment.

* feat(routing): deterministic routing strategies for self-hosted entry (RIC-740)

Add the M2 deterministic routing strategy engine (D3 可审计路由) to the
self-hosted unified entry: a declarative `strategy:` block expressing five
explainable, non-predictive policies — blacklist/whitelist hard filters,
cooldown circuit breaker, cost-priority, latency-aware ordering, and an
explicit fallback chain. The ordered candidate list is the fallback chain:
a failed primary (network or non-2xx) falls through to the next candidate and
each failure feeds the breaker. Every response carries an
x-omniroute-route-decision header answering "why this model / why not that
one". A pinned provider rejected by a hard filter returns 400 (never a silent
re-route); no eligible providers returns 503 with the full explainable
decision. No ML/predict dependency.

Covers the RIC-740 acceptance: 5 strategy types with unit tests + HTTP
fault-injection tests (primary down -> fallback works), config matching docs,
and no predict/ML deps. Adds docs, .env.example entries, and a changelog
fragment.

* refactor(routing): reduce complexity-ratchet violations in new self-hosted routing files

Extract cost/id validation, pin-blocked resolution, ordering, and env/file
source resolution into small helpers so routingStrategies.ts and
selfHostedEntry.ts stay under the complexity-ratchets cap. No behavior
change — the same 51 routing-strategies/self-hosted-entry/provider-adapters
tests pass unmodified.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(routing): document the 5 self-hosted env vars in ENVIRONMENT.md

check:env-doc-sync failed because OMNIROUTE_SELF_HOSTED_PROVIDERS(_FILE),
OMNIROUTE_SELF_HOSTED_API_KEY and OMNIROUTE_SELF_HOSTED_STRATEGY(_FILE)
were present in .env.example but missing from
docs/reference/ENVIRONMENT.md. Add them under "6. Tool & Routing Policies".

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Ant Rich <ant@richants.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: luyuehm <luyuehm@users.noreply.github.com>
2026-09-18 11:59:15 -03:00
James
3ebea07278 fix(sse): replay reasoning for Responses-API targets on plain turns and Anthropic clients (#13031)
* fix(sse): replay reasoning for Responses-API targets on plain turns and Anthropic clients

DeepSeek thinking mode requires the reasoning of every prior assistant turn
to be passed back once the request carries `tools`, including turns that
made no tool call. Since #10540 routed opencode-go/deepseek-v4-* to
`/responses`, the reasoning replay cache had two gaps on Responses-API
targets, and clients that drop `reasoning_content` hit intermittent
`400 The reasoning_text in the thinking mode must be passed back`.

1. Plain (non-tool-call) turns are keyed on a digest of the normalized
   OpenAI transcript. Both capture sites used `translatedBody.messages` as
   the history, which a Responses body (`input`) does not carry, so the
   write-time digest never matched the read side. translateRequest now
   reports the pivot transcript it digested via `onReasoningReplayHistory`,
   and the streaming / non-streaming capture sites digest that transcript.
2. The Responses replay pass was gated on `sourceFormat === "openai"`, so
   Anthropic Messages clients (Claude -> OpenAI -> Responses) got no replay
   at all. The pass now runs on the OpenAI pivot for every source format,
   right before the Responses conversion discards `messages`.

The reported transcript is a shallow snapshot of the digested fields only
and travels through a callback, not the body, so nothing new reaches the
upstream payload.

* docs(changelog): add fragment for #13031

* fix(sse): guard the Responses capture sites and skip plain-turn writes with no history

Review follow-ups for #13031:

- Add tests/unit/chatcore-reasoning-cache-write-guard-responses.test.ts:
  runs the real handleChatCore against a mocked opencode-go/deepseek-v4-flash
  Responses upstream (JSON and SSE), then asserts the next turn's upstream
  body carries the replayed `reasoning` input item. Removing either capture
  site fallback turns both cases red.
- Project the reported transcript down to the digested fields only
  (tool_calls keep type/name/arguments, ids are dropped) and document that
  `content` is shared by reference.
- Skip the plain-turn cache write when the history is empty: a real request
  always has a prior user turn, so an empty history means the transcript
  could not be recovered and a one-message digest can never match.
- Changelog wording: the pre-fix write digested only the assistant message.

* test(sse): select the /responses dispatch by URL in the Responses replay guard

Review follow-ups for #13031: the guard picks the upstream body by URL
(`/responses`) and asserts exactly one such dispatch per turn instead of
taking the last fetch, the streaming case asserts the same body shape as the
non-streaming one, and the `historyMessages` doc on
NonStreamingClientTranslateInput names the Responses-shaped fallback.

* docs(routing): name the replay-history hand-off without tripping the hook heuristic

The fabricated-docs gate treats any `onXxx` token in prose as a plugin hook
name and flagged `onReasoningReplayHistory` (a translateRequest option, not a
hook). Point at the option's home file instead.

* chore(quality): freeze chatCore.ts at 6159 for the Responses replay wiring

check:file-size in PR mode caps a frozen file at max(frozen, base). The rebase onto
the v3.8.51 tip (cde49c937) leaves chatCore.ts at 6159 lines against a 6146 ceiling:
the onReasoningReplayHistory callback on both Responses-capable translateRequest call
sites, reasoningReplayHistory on both non-streaming leg inputs, and the historyMessages
fallback at the streaming cache write. Record the growth with a justification key, as
#13033 did for the same file.

---------

Co-authored-by: jmche <jmche@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:59:06 -03:00
initguru
6768b14b54 fix(chatcore): block duplicate turn execution with 409 turn_in_progress (#12912)
* fix(chatcore): block duplicate turn execution with 409 turn_in_progress

* test(sse): align turn-execution-guard 409 body expectation with buildErrorBody reason field

* fix(errors): preserve duplicate turn classification

* test(turn-execution-guard): assert ageMs range instead of exact 0

Comparing ageMs to an exact 0 was flaky under real scheduling
latency between the two synchronous calls in the test.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(changelog): add fragment for turn execution guard fix (#12912)

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): rebaseline chatCore.ts file-size for the turn-execution guard

The guard logic lives in the new open-sse/handlers/chatCore/turnExecutionGuard.ts
leaf; what grows chatCore.ts is the irreducible call-site wiring at the single
execution chokepoint (acquire, the 409 turn_in_progress early return, the
release/handoff bookkeeping and the try wrapper that scopes it).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(sse): keep endpointPath outside the turn-guard try so the failure-usage closure can reach it

The try/finally that scopes the duplicate-turn guard block-scoped the
resolveChatCoreRequestFormat destructuring, but persistFailureUsage is defined
above the try and closes over endpointPath — every failure-usage write would
have thrown ReferenceError. Moved the destructuring above the guard (it is a
pure derivation from the request, so nothing else changes) and narrowed the
acquire result with an explicit === false, which the workspace tsconfig
(strict: false) needs to see the non-acquired arm's retryCount/ageMs.

check:open-sse-typecheck goes from 4 errors to 0; typecheck:core, eslint,
prettier and the PR's 4 turn-execution-guard tests stay green.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: initguru <initguru@users.noreply.github.com>
2026-09-18 11:58:57 -03:00
Tuan Dinh
6fec29ca2d fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider (#13705)
* fix(copilot): fallback to copilot-chat on 403 identity denial for standard provider

* fix(copilot): document COPILOT_INTEGRATION_ID, extract identity fallback, add changelog

Adds the missing COPILOT_INTEGRATION_ID entry to .env.example (fixes
tests/unit/issue-7793-env-doc-sync-repro.test.ts), extracts the GitHub
Copilot 403 identity fallback out of open-sse/executors/base.ts into its
own module (open-sse/executors/copilotIdentityFallback.ts) to bring the
file back under the frozen file-size ratchet, and adds a changelog.d/fixes
fragment for the PR.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: tuandinh0801 <tuandinh0801@users.noreply.github.com>
2026-09-18 11:58:48 -03:00
Alvin T. Veroy
24706705fa fix(streaming): per-provider fetch-start timeout cap override (#11526 follow-up) (#13002)
* fix(streaming): per-provider fetch-start timeout cap override (#11526 follow-up)

Buffered gateways (opencode-go / command-code Console Go tiers) legitimately
buffer a whole reasoning generation before the first upstream byte, so their
streaming requests can exceed the default 110s headers-wait cap. #11526 capped
every streaming request at that ceiling, so these long generations died at
exactly 'Fetch timeout after 110000ms' (504) before any bytes arrived.

Add a per-provider fetchStartTimeoutCapMs registry knob (600s for opencode-go
and command-code) and project it into the executor's LegacyProvider so
resolveFetchStartTimeout caps only genuinely unbounded providers.

* docs(changelog): add fragment for fetch-start cap per-provider override

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: alvinveroy <alvinveroy@users.noreply.github.com>
2026-09-18 11:58:39 -03:00
Wu Shuwen
10bb627576 fix(evals): mark eval-runner requests as self-managed so cases measure the model (#13139) (#13206)
* fix(evals): mark eval-runner requests as self-managed so cases measure the model

executeEvalCase() built its request with only Content-Type and Authorization, so
every graded case picked up the chat path's contextual injections: a selected
output style was prepended as a system message (gated on
`x-omniroute-compression`) and, once the request carried an API key, retrieved
memory plus the built-in `memory_*` tools were appended (gated on
`x-omniroute-no-memory`). An evaluation therefore measured the operator's
injected context as much as the model, and passing an API key to a run made its
score worse, because the key is what gives the request a memory owner (Refs #13139).

Both are documented request-header opt-outs, so the runner now sets them on every
case. Request construction moves to an exported buildEvalCaseRequest() so the
header contract is testable without invoking the chat route.

* docs(changelog): add the eval-runner self-managed-context fragment (#13206)

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:58:30 -03:00
Tuan Dinh
4a5f1cd771 fix(providers): Antigravity connection Retest probes Cloud Code envelope (#13010) (#13015)
* fix(providers): Antigravity connection Retest probes Cloud Code envelope (#13010)

* fix(providers): ensure correct argument order for Antigravity discovery and add test

* fix(providers): resolve connection.projectId and surface upstream 400 error message

* docs(changelog): add fragment for #13015

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:58:22 -03:00
IAMBOBJIM
d1b26bcb62 fix(sse): aggregate findInsensitive collision warning into one line per build (#12972)
modelMetadataRegistry's findInsensitive() warned once per colliding key while
building its lowercase index. On a real catalog that is hundreds of lines per
rebuild: a production log carried 27,296 of these in a single file — 40% of all
lines, in ~500/sec bursts — driving 52 MB log rotations and ~466 MB of logs on
disk.

The warning itself is worth keeping: a case-insensitive collision is a genuine
upstream data-quality signal (models.dev returning both "OpenAI" and "openai"
as distinct provider keys), and first-match-wins silently discards the later
value. Only the volume was wrong.

Collisions are now collected during the index build and reported as a single
line carrying the total count plus the first 5 keys, so the diagnostic survives
at 1/N the volume. No behavior change: the index, the first-match-wins
resolution, and the WeakMap identity cache are untouched.

Validated by TDD (Hard Rule #18): tests/unit/model-metadata-registry-collision-log.test.ts
fails on the old implementation (3 collisions -> 3 warnings, 50 -> 50) and
passes after (always 1). Also covers the no-collision case emitting nothing,
and asserts the aggregated line still names colliding keys.

Note for reviewers: the test fixture deliberately spells the provider key
"OpenAI" rather than "openai". findInsensitive short-circuits on
`if (key in obj) return obj[key]` before the index is ever built, so a fixture
containing the literal lookup key produces zero warnings and proves nothing.

Gates: eslint clean on both changed files. typecheck:core reports 9 pre-existing
errors in open-sse/services/compression/omniglyph* — unrelated to this change
(those files are byte-identical to origin/release/v3.8.50) and caused by a local
stale node_modules carrying omniglyph 1.3.1 against the required ^1.4.0.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:58:14 -03:00
Pixma
01b2467d61 feat(sse): add LLM Gateway DevPass quota tracking (#12462)
* feat(sse): add LLM Gateway DevPass quota tracking

Surface the LLM Gateway DevPass allowance (GET /v1/key) in OmniRoute's
quota telemetry, mirroring the OpenRouter API-key fetcher pattern.

- llmgatewayQuotaFetcher.ts: fetch + parse the DevPass /v1/key response
  (decimal-string USD values), exposing two windows — monthly plan
  credits and the 7-day premium-model window — with a 45s TTL cache.
  Pay-as-you-go keys (devPlan "none") and 401/403 fail open (no quota).
- Register in chat.ts before registerGenericQuotaFetchers + register the
  named windows for the dashboard cutoff modal.
- usage/llmgateway.ts leaf + usage.ts dispatch case so the Limits page
  renders the monthly + weekly premium rows.
- Add "llmgateway" to USAGE_FETCHER_PROVIDERS, USAGE_SUPPORTED_PROVIDERS,
  PROVIDER_LIMITS_APIKEY_PROVIDERS, and the dashboard label/order map.
- tests: 21 cases covering the parser, auth fail-open, cache TTL, window
  exhaustion, preflight proceed/block, registration, and the usage leaf.

* docs(sse): add changelog fragment + codebase-doc entry for llmgateway quota

* refactor(sse): register llmgateway quota via quotaTrackersBatch

Move the LLM Gateway fetcher registration out of chat.ts (a frozen
file-size-baseline chokepoint) into quotaTrackersBatch.ts, the dedicated
side-effect module that exists precisely so new fetchers don't grow
chat.ts. The batch import runs at module load, before
registerGenericQuotaFetchers(), so the bespoke fetcher still wins over
the generic path. Fixes the file-size gate (chat.ts must not grow).

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:58:05 -03:00
Diego Rodrigues de Sa e Souza
f1eabd8885 fix(sse): stop direct fetch retry reusing pooled flat response-start budget (#13703) (#14047)
resolveDirectHeadersTimeoutMs() (open-sse/utils/directResponseStartTimeout.ts)
now bounds only the pooled dispatcher attempt (attempt 0) with the flat
OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS watchdog. The fresh-socket retry (attempt
1) is by construction a brand-new socket with no zombie-socket risk (#10214's
rationale only applies to the pooled attempt), so when the caller already
attached its own deadline signal it now defers to a generous, configurable
backstop (OMNIROUTE_DIRECT_RESPONSE_RETRY_TIMEOUT_MS, default 600s) instead of
reusing the identical short flat window — fixing spurious 504s on healthy
slow-TTFB reasoning models that need well over 60s total for both attempts.

Regression test: tests/unit/proxyfetch-direct-response-start-flat-retry-budget-13703.test.ts
(RED against unmodified code: retry cut at 81ms against an 80ms flat budget
with ~1920ms of caller deadline unused; GREEN after the fix).

New env var documented in .env.example and docs/reference/ENVIRONMENT.md.

tlsProfileForProvider's return type in proxyFetch.ts is pulled into a named
alias so its signature stays on one line under prettier's canonical
formatting -- otherwise prettier's mandatory lint-staged reformat of this
frozen file grows it past the check:file-size baseline on every future touch.

base-red inherited: #14004 (docs env/docs contract, fixed separately in
#14022; chatHelpers file-size drift)
2026-09-18 11:57:51 -03:00
Diego Rodrigues de Sa e Souza
1b82b2f982 fix(combo): stop chars/4 overestimate demoting a verified context override (#13870) (#14046)
filterTargetsByRequestCompatibility ranked combo targets solely on the
chars/4 estimateTokens() heuristic. On a repetitive agent-session body the
estimate overstates real usage several-fold, so a manually-overridden
primary sized correctly for the real request got marked context-incompatible
and was reordered behind an unconfirmed catalog "emergency" member with a
large but unverified limit_context.

Fix: when the reorder branch promotes known-context-compatible targets,
split them by whether their pass came from an operator-set
model_context_override (trusted) or bare catalog metadata (advisory), and
also trust a near-boundary override rejection (required tokens within 5x
the override — covering the ~3.7x overestimate the issue measured) over a
catalog-only pass. An override target keeps or regains priority over an
unconfirmed catalog-only "known compatible" target; two override targets or
two catalog-only targets keep resolving purely on their own fit as before.

Regression test: tests/unit/combo-13870-chars4-overdrops-override-primary.test.ts
(RED before the fix — emergency member promoted to position 0 ahead of the
override primary; GREEN after).

⚠️ base-red inherited: #14004 — docs env/docs contract (fixed separately in
#14022), chatHelpers file-size drift. Not touched by this branch.
2026-09-18 11:57:40 -03:00
Diego Rodrigues de Sa e Souza
f3ab24b8c7 fix(db): rotate proxy pools on the chat path like the registry does (#13575) (#14044)
resolveProxyForConnection cached a scope pool's first resolution result for the
life of the per-connection cache, so a chat-path request never saw the pool's
round-robin/sticky/random strategy advance again — only the narrow #13578
set-aside escape hatch could break the freeze. resolveProxyForScopeFromRegistry
(used directly by every existing rotation test) always re-ran the strategy and
rotated correctly.

The cache now treats a registry-sourced pool result as due for re-resolution on
every call (falling through to the same cascade the direct registry callers
use), except for the two populations that need a stable egress across
requests: EGRESS_BUCKETED_LOCK_PROVIDERS (opencode's quota is bucketed by
egress IP) and grok-web (its cf_clearance cookie is pinned to the IP/UA/TLS
fingerprint that earned it).

Regression test: tests/unit/proxy-pool-chat-path-rotation-13575.test.ts, RED
before the fix (resolveProxyForConnection returned the same host 6/6 times for
a 3-member pool), GREEN after. Updated tests/unit/proxy-pool-skips-refused-member.test.ts's
three assertions that encoded the frozen-cache contract to the corrected
always-rotates-except-pinned contract; all other cases in that file and in
tests/unit/proxy-pool-rotation-6365.test.ts pass unchanged.
2026-09-18 11:57:32 -03:00
Diego Rodrigues de Sa e Souza
b0955042dc fix(providers): map agentrouter GLM thinking.type adaptive to enabled (#13696) (#14043)
AgentRouter routes GLM models through the generic DefaultExecutor, which has
no GLM-specific handling. When the connection's OpenAI-compatible alternate
format is used, a Claude-style thinking:{type:"adaptive"} field survived
stripUnsupportedParams untouched and reached AgentRouter's upstream GLM
endpoint verbatim, which 400s (thinking.type "adaptive" is not supported by
glm models; must be one of enabled, disabled).

Add a mapThinkingType mechanism to paramSupport.ts's STRIP_RULES (in addition
to the existing drop/clamp mechanisms) and scope a rule to provider
"agentrouter" + model matching /glm-/i that remaps thinking.type from
"adaptive" to "enabled", preserving any other thinking fields (e.g.
budget_tokens) — mirroring the same mapping GlmExecutor already performs for
its own provider.
2026-09-18 11:57:25 -03:00
Diego Rodrigues de Sa e Souza
0d31fd3d24 fix(quota): resolve plan from pool's primary connection (#13876) (#14042)
Multi-connection Quota Sharing pools resolve a DIFFERENT provider plan
depending on which member connection actually served a request (write
path, enforceQuotaShare/recordConsumption) vs. the pool's primary
connection (dashboard read path, /api/quota/pools/[id]/usage). The
wizard's "Limite" step PUTs a manual plan override only to the primary
connection, so any other pool member fell back to a different
(catalog/empty) plan shape. Since the quota_consumption dimension key
is poolId:unit:window, a different unit/window meant recordConsumption
wrote to a bucket the dashboard never read, so real traffic served via
a non-primary connection never appeared as "consumed".

Fix: resolve the plan from the pool's canonical primary connection
(pool.connectionId) in both enforceQuotaShare and recordConsumption,
matching the dashboard's read path. recordConsumption now keeps the
matched pool object (not just its id) so it can reach connectionId.
getSaturation(input.connectionId, ...) is untouched — that signal is
legitimately per-connection.
2026-09-18 11:57:17 -03:00
Diego Rodrigues de Sa e Souza
460c6075b1 fix(providers): convert agent_message input items for non-Codex-native Responses upstreams (#13698) (#14041) 2026-09-18 11:57:08 -03:00
Diego Rodrigues de Sa e Souza
a96c8381f7 fix(db): serve getPricingForModel() from the pricing cache (#13891) (#14040)
getPricingForModel() called the uncached getPricing() on every
invocation instead of the existing getCachedPricing() helper
(30s TTL, readCache.ts), so usageStats.getUsageStats() re-ran a
3-SELECT + JSON.parse + merge cycle against key_value once per
GROUP BY row -- up to 531 times on a large usage_history table --
blocking the event loop for several seconds on /api/usage/history.

Every known pricing writer (updatePricing, LiteLLM/models.dev sync)
already invalidates this cache via touchPricing()/invalidateDbCache,
so a write remains immediately visible; added a regression test that
proves both the cache hit path and the invalidation path.
2026-09-18 11:57:00 -03:00
Diego Rodrigues de Sa e Souza
e94752fa53 fix(tests): make ReDoS guard assert cost scaling, not wall-clock (#13907) (#14039)
The property test asserted an absolute 250ms ceiling on
sanitizeErrorMessage() for adversarial inputs. That ceiling had no
margin over the pipeline's real fixed cost (3x redact + 2x
normalize passes added by #12506), so it failed on cost under any
machine load, not on backtracking. Replace it with a check that the
sanitizer's cost does not scale with input length beyond a generous
noise allowance, which is what a bounded-backtracking guarantee
actually claims; keep a coarse absolute hang ceiling as a backstop.
2026-09-18 11:56:50 -03:00
Diego Rodrigues de Sa e Souza
b14ef5c7e5 fix(guardrails): resolve nested combo-ref hops before vision-bridge decision (#13927) (#14038)
getComboVisionBridgeDecision() treated any top-level combo-ref step as an
unconditional "process", without ever resolving the referenced combo's real
leaf models. A pass-through combo whose only member is a combo-ref to an
all-vision-capable inner combo was wrongly routed through the
describe-and-replace path, and with no describer model configured every image
was replaced with the literal stub text.

Recursively resolve combo-ref steps to their real leaf models (depth-guarded
by the same MAX_COMBO_DEPTH used by the flatten dispatch path, plus a
visited-set cycle guard) and fold their vision capability into the same
accumulation used for direct model steps. An unresolvable combo-ref (not
found / empty / circular / depth-exceeded) is conservatively treated as a
single non-vision-capable leaf instead of forcing the whole combo to
"process".
2026-09-18 11:56:42 -03:00
Diego Rodrigues de Sa e Souza
5fce23c957 fix(sse): recognize MCP-gateway-namespaced CCR retrieve tool names (#13781, #13897) (#14028)
callerSupportsCcrRetrieve() matched the omniroute_ccr_retrieve tool by exact
string equality. MCP aggregators/gateways (Docker MCP Toolkit, Claude Code)
rename re-exposed tools with a namespace prefix (e.g.
mcp__docker__omniroute__omniroute_ccr_retrieve, or a dotted/slashed prefix),
so a fully MCP-capable caller reachable only under such a name was treated as
unable to retrieve at all -- skipping both the protocol-instruction injection
and, per the #7746 safety guarantee, the entire CCR compression engine for
that request.

Add matchesCcrRetrieveToolName(), a separator-bounded trailing-segment match
(__, ., /, :) that accepts a namespaced form of the tool name while still
rejecting a near-miss like omniroute_ccr_retrieve_v2 -- never a bare
substring/endsWith check.
2026-09-18 11:56:32 -03:00
Diego Rodrigues de Sa e Souza
25d35179fd fix(security): scope /api/files and /api/batches to caller's tenant (#13882) (#14027)
/api/files, /api/files/[id]/content, /api/batches and /api/batches/[id]
only gated on requireManagementAuth(request), which returns null
unconditionally when settings.requireLogin===false, and never applied
any per-record ownership check. On an instance with login disabled, an
unauthenticated caller could enumerate/download every tenant's files
and batches — the hardened /api/v1/files and /api/v1/batches siblings
already scope via getApiKeyRequestScope()/resolveListScope()/
canAccessOwnedRecord() from the GHSA-2jm2-mpx8-6523 and
GHSA-m3hp-hq9g-fpmv fixes.

Port that exact scoping onto the 4 management routes: an API key sees
only its own files/batches, a dashboard session keeps instance-wide
access, and any other caller is rejected instead of falling through to
an unscoped read.
2026-09-18 11:56:24 -03:00
Diego Rodrigues de Sa e Souza
5b61937f17 fix(sse): declare deepseek 1M default context window (#13922) (#14026) 2026-09-18 11:56:15 -03:00
Diego Rodrigues de Sa e Souza
1c5612c760 fix(api): fail closed on revoked/expired/banned API keys in getApiKeyRequestScope (#13881) (#14024)
getApiKeyRequestScope() resolved apiKeyId purely from getApiKeyMetadata(),
which does a row-existence lookup with no lifecycle filtering. Only
validateApiKey() checks is_active/revoked_at/is_banned/expires_at, and none
of the six /v1/files and /v1/batches route handlers called it directly, so a
revoked, expired or banned key kept a live apiKeyId and canAccessOwnedRecord()
/resolveListScope() kept granting it access to its own records after
revocation (CWE-613).

Fold validateApiKey() into getApiKeyRequestScope() itself: a key that fails
that lifecycle gate is now collapsed into the same { apiKeyId: null,
apiKeyMetadata: null } shape as an unresolved/anonymous caller, so every
consumer of this scope (list reads, per-record ownership checks) fails
closed without each route re-implementing the check.
2026-09-18 11:56:06 -03:00
Diego Rodrigues de Sa e Souza
3b535968c4 fix(providers): detect Lemonade labels[] vision capability (#13918) (#14023)
detectVisionInput() only recognized supportsVision, architecture.input_modalities,
top-level input_modalities, and architecture/modality string shapes. Lemonade
Server's GET /v1/models exposes capabilities only through a labels[] string
array (e.g. ["chat", "vision", "reasoning", "tool-calling"]), so a
vision-labelled Lemonade model imported with supportsVision unset and was
advertised as text-only.

Add a fifth branch that does a case-insensitive, trimmed EXACT membership
test for "vision" in record.labels[] (not a substring match, per the prior
false-positive lesson with bare gemma id-fragment matching). Purely additive
- all four existing shapes stay byte-identical, proven by a new regression
test that exercises the architecture.modality path unchanged.
2026-09-18 11:55:54 -03:00
Diego Rodrigues de Sa e Souza
9c9ad6bbde fix: land #13161 and #13304 by cherry-pick (locked organization fork) (#14090)
* fix(resilience): treat a Cloudflare managed challenge as a fingerprint rejection, not a ban

A Cloudflare managed/JS challenge served in front of an upstream provider is the
same class of block as a Cloudflare 1010 — the edge refused the CLIENT's
signature — but it is a different product surface and carries none of the 1010
markers isCloudflareFingerprintRejection() looks for.

It therefore fell through the entire 403 ladder in classifyProviderError() to the
terminal default FORBIDDEN, which chatCore persists via writeTerminalStatus() as
testStatus=banned / isActive=false. That state never auto-recovers, so a single
challenge takes the whole provider offline until an operator reconnects in the
dashboard.

Observed on POST chatgpt.com/backend-api/codex/responses/input_tokens for a
healthy Codex OAuth account: the response carried cf-mitigated: challenge,
server: cloudflare and a ~12KB text/html interstitial with
window._cf_chl_opt = {... cType: 'managed', cZone: 'chatgpt.com' ...}. The same
connection refreshed its OAuth token successfully in the same second and served
normal /responses traffic seconds before and after, so the account was never
banned upstream.

Classify the interstitial as FINGERPRINT_REJECTION, reusing the existing
non-terminal precedent from #9929: authTerminalStatus already treats that type as
non-terminal, so the request falls through to the next combo target and the
account state stays untouched.

The markers are matched as full, distinctive Cloudflare-internal strings
(_cf_chl_opt, cdn-cgi/challenge-platform, the challenge-error-text span id
including its escaped-quote nested form) and never as the loose word
"challenge", so provider bodies discussing a challenge in prose are unaffected.

Tests cover the full interstitial, the gateway-nested error.message form, each
marker individually, prose false-positive guards, and regression guards proving
a genuine permission 403 and the ChatGPT Web Sentinel/Turnstile 403 (#8813) both
still classify as FORBIDDEN.

(cherry picked from commit 8204da668a)

* fix(translator): skip replayed web_search_call metadata in Responses-to-Chat

OmniRoute's web-search fallback emits a native web_search_call output item
alongside function_call/function_call_output. Responses clients keep that item
in conversation history and replay it in the next request's input. When the
follow-up turn routes to a Chat Completions target (Claude), the translator hit
its default unsupported-feature branch and returned a deterministic HTTP 400:

  Unsupported Responses API feature: input item type 'web_search_call'
  cannot be represented in Chat Completions

Skip the replayed metadata next to tool_search_call/tool_search_result. The
paired function_call_output still carries the search results, so no context is
lost and the sources are not duplicated into assistant history.

(cherry picked from commit 2b3e63a470)

* chore(changelog): credit the locked-fork landings of #13161 and #13304

Both PRs come from an organization fork (azox-ai) that refuses maintainer pushes
even with maintainerCanModify=true, so they cannot be re-synced in place and are
landed here by cherry-pick with the contributor's authorship preserved
(merge-gates §6). GitHub may not mark the PRs Merged, hence the explicit
credit in the fragments.

---------

Co-authored-by: anhth2 <anhth2@vng.com.vn>
2026-09-18 11:35:50 -03:00
Hakarioz
217c93d081 chore(deps): bump better-sqlite3 to ^13.0.3 (#14049)
Co-authored-by: Hakarioz <lucaspapoute@gmail.com>
2026-09-18 11:35:41 -03:00
Paco Cartones
57f31105f9 test(dashboard): reactivate logs modal coverage (#14048)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-18 11:35:33 -03:00
Dizzle
de77213b0d fix(build): drop orphaned httpClientAbortGuard.mjs pack-artifact entries (#14029)
The #13636 crash-guard wiring was removed, leaving no producer or
consumer for the dist file. Refs #12732

Co-authored-by: Max <maxmad64@gmail.com>
2026-09-18 11:35:24 -03:00
Dizzle
fec8dc2be9 fix(opencode): record the free-tier refusal instead of counting it as success (#14011)
An OpenCode Zen free-tier 403 ("free tier can only be used from within
OpenCode") reached the end of the executor loop unrecognized: nothing was
persisted about it, and the account that had just been refused was marked
successful, which clears the failure history driving its cooldown backoff. A
refusal was therefore improving the rotation health of the account it hit.

The refusal is now recognized by its own predicate, returned unchanged without
rotating (it is request-scoped, so every sibling account returns the same
verdict), and classified as a non-banning routing error, so the connection
records lastErrorType/lastError/errorCode and stays active.

The account-health reset is also reserved for HTTP successes at both call sites
in the loop, since the same reset ran on any status the loop did not handle in a
dedicated branch.

Co-authored-by: Max <maxmad64@gmail.com>
2026-09-18 11:35:15 -03:00
John Costa
875a84e301 fix(docker): copy the app with node ownership instead of a second chown layer (#14010)
The runner-base stage COPY'd the standalone build as root and then ran
`RUN chown -R node:node /app`. On overlayfs a chown rewrites every file it
touches into the new layer, so the published image carried the ~2 GB
standalone tree twice (docker history of diegosouzapw/omniroute:latest:
`COPY /app/.build/next/standalone ./` 2.03 GB followed by
`RUN chown -R node:node /app` 2.04 GB).

Set `--chown=node:node` on the three COPYs that populate /app, drop the
recursive chown, and hand /app and /app/data to node non-recursively next to
the `mkdir -p /app/data` so the data dir stays writable without a volume.

Measured by rebuilding the runner-base COPY/chown sequence against the
published /app tree (root-owned source, same base image, linux/arm64):

  before: 4.38 GB of layers (COPY 2.04 GB + chown -R 2.04 GB), inspect Size 1220846106
  after:  2.34 GB of layers (COPY --chown 2.04 GB),          inspect Size  655117142

The fixed image runs as uid 1000, /app and /app/data are node-owned and
writable, the server boots and healthcheck.mjs exits 0. hadolint output is
unchanged. tests/unit/dockerfile-copy-chown-13990.test.ts guards the
mechanism.

Fixes #13990

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:35:05 -03:00
John Costa
8fbc85ee72 fix(perplexity-web): keep runs of spaces when cleaning non-streaming answers (#14009)
cleanResponse(text, strip = true) replaced every run of two or more
spaces with a single space. The non-streaming path (which tool mode
always uses, since it buffers the full completion before converting
<tool> text into tool_calls) runs cleanResponse with strip on, so any
code the model wrote through perplexity-web lost its indentation: every
nesting level came back as one space, making generated Python
unimportable. Tabs were untouched, which is what pointed at this
normalization step rather than the model.

Fold the space handling into CITATION_RE so the space before a removed
[n] marker goes with it ("text [1] more" still cleans to "text more"),
and drop MULTI_SPACE. Blank-line squashing and trim are unchanged.

Fixes #13968
2026-09-18 11:34:56 -03:00
Tiangao
95cb992c32 fix(providers): honor the base URL override in OpenRouter model discovery (#14001)
* fix(providers): honor the base URL override in OpenRouter model discovery

Model discovery for the built-in `openrouter` provider resolved its catalog
URL from PROVIDER_MODELS_CONFIG, which is pinned to the global
`https://openrouter.ai/api/v1/models`. The per-connection base-URL override
(`providerSpecificData.baseUrl`, set via "Advanced -> override base URL") was
never consulted on the discovery path, while the inference path has honored it
since #6147 (open-sse/executors/base.ts `resolveBaseUrl`).

A connection pointed at a different OpenRouter region therefore kept importing
the global catalog: the per-connection model list, and the auto-sync that
maintains it, advertised model ids the configured endpoint cannot serve. Those
ids only failed later, at inference time, so a region/catalog mismatch surfaced
as what looked like a provider outage.

The two catalogs genuinely differ — the global endpoint advertises ~444 model
ids, the EU in-region endpoint ~58 (a strict subset) — so discovery and
inference disagreed with no signal exposing it.

Discovery now prefers the override for this provider, reusing the existing
`addModelsSuffix()` normalization (drops a trailing chat/responses/messages
path, appends /models, leaves an existing /models untouched). Mirrors the
`openai` override handling added for the same class of bug in #5899. When no
override is set the built-in global catalog is still used.

Tests: tests/unit/openrouter-models-baseurl-override.test.ts covers both the
override and the unchanged default.

* chore(changelog): add fragment for #14001

---------

Co-authored-by: Tiangao (hermes) <montigaud@aikumi.pro>
2026-09-18 11:34:47 -03:00
Aref Alapour
6f55a8c44f fix(providers): keep TinyCMS DOM stub safe for Next.js SSR (#13957)
Never alias window to the Node global without location. The wasm-bindgen
shim now installs a dedicated window with a Location-shaped object and
restores after WASM init/payload generation, so getLocationOrigin cannot
crash every route after TinyCMS is used once.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Aref Alapour <aref-alapour@users.noreply.github.com>
2026-09-18 11:34:39 -03:00
Dizzle
fc6b4587ad feat(proxy): support multiple local core endpoints, one per line (#13923)
Co-authored-by: Max <maxmad64@gmail.com>
2026-09-18 11:34:30 -03:00
Shahanur Islam Shagor
b6e7bc12ea Fix/combo multimodal capability 13847 (#13863)
* fix(capabilities): align combo multimodal vision hints

* test(capabilities): cover combo multimodal consistency

* chore(changelog): note combo multimodal capability fix
2026-09-18 11:34:21 -03:00
Xmon Dai
bdc79ef883 fix(providers): normalize non-function tools for allowlisted built-in OpenAI-format providers (#13855)
Built-in providers that speak the OpenAI Chat wire format skipped
normalizeOpenAICompatibleTools(), which only ran for custom
openai-compatible-* connections. A client tool whose type is not
"function" (a named Claude server tool, a nameless hosted tool) was
forwarded verbatim, and agentrouter's GLM backend rejected the whole
request with 400 tools[0].type:type is illegal.

Extract the gate into shouldNormalizeFunctionToolsOnly(): custom
openai-compatible-* providers keep normalizing on every target, and a
conservative allowlist of built-in providers (agentrouter first)
normalizes on the OpenAI Chat target only. OpenAI itself stays off the
list, so its custom tools pass through untouched.

Closes #13789
2026-09-18 11:34:12 -03:00
luw2007
74d8690687 fix: reclaim expired half-open probe lease (#13849)
Co-authored-by: luwei.will <luwei.will@bytedance.com>
2026-09-18 11:34:03 -03:00
Paco Cartones
dfde9fc392 fix(sre): redact secrets split across stream chunks (#13837)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-18 11:33:54 -03:00
legas888Oleg
733f4c1d0a fix(providers): refresh uncloseai free-model roster after upstream rotation (#13825)
* fix(providers): refresh uncloseai free-model roster after upstream rotation

hermes.ai.unturf.com rotated its lineup: /v1/models now serves exactly one
model (Lorbus/Qwen3.6-27B-int4-AutoRound, vllm, max_model_len 65536) while
every previously catalogued id (adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic,
qwen3.6:27b, gemma4:31b) returns 404 on /v1/chat/completions. Requests routed
through the static seed failed although the provider itself is healthy — a
live completion against the new id succeeds.

- registry seed: replace the three dead ids with the live one (+contextLength)
- FREE_MODEL_BUDGETS: 3 rows -> 1 (catalog totals 443 -> 441; counts synced in
  README and free-tier-budget.svg)
- noauth authHint: verified-live-model pointer updated; PROVIDER_REFERENCE.md
  regenerated
- regression test pins the live id and forbids the retired ids in both the
  registry seed and the free catalog

Verified live on 2026-09-15 against https://hermes.ai.unturf.com/v1/models
and /v1/chat/completions.

* docs(providers): sync free-tier entry count after uncloseai merge

The uncloseai roster refresh (3 entries -> 1) dropped the live free-tier
catalog total from 491 to 489 once merged with the current release tip.
README.md and free-tier-budget.svg still quoted 491 after the merge;
update both to the real count so check:docs-counts-sync stays green.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: anon <anon@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:33:47 -03:00
Abhishek Sharma
b45e0c69e6 feat(security): warn at boot when the inference server is exposed anonymously (#13820)
* feat(security): warn at boot when the inference server is exposed anonymously

`GET /v1/models` follows the dashboard login posture
(`isAuthRequired()` / `requireAuthForModels`) while the inference routes
follow `REQUIRE_API_KEY`. On an instance with an admin password set and
`REQUIRE_API_KEY=false`, `/v1/models` answers 401 while `/v1/responses`
is open to anyone who can reach the port — so the most natural probe an
operator runs reports the opposite of the truth.

#12568 added a boot warning for exactly this combination, but wired it
only into the API bridge and the live dashboard WebSocket. The Next
server that actually answers `/v1/chat/completions` and `/v1/responses`
never reached it, and it is the one that binds every interface by
default (`process.env.HOST || "0.0.0.0"`).

Wire the existing guard into the Next boot hook, and document the split.

Resolving the bound host needed care: two entrypoints bind that server
and they read different variables. `run-next.mjs` honours `HOST`; the
Docker entrypoint delegates to Next's generated `server.js`, which reads
`HOSTNAME`. `run-next.mjs` now publishes what it actually binds as
`OMNIROUTE_BOUND_HOST`, and the guard reads that, then `HOSTNAME`, then
the shared `0.0.0.0` default. `HOST` is deliberately absent from the
chain: the standalone server ignores it, so consulting it there would
warn about an interface the server is not on — and one false warning
teaches an operator to ignore the next one.

Closes #13695

* docs(changelog): add changelog.d entry for #13820
2026-09-18 11:33:39 -03:00
William Echo
2782258846 fix(executors): strip invalid OpenCode stream options (#13819)
Co-authored-by: William Echo <175406538+qinghuanandejiangshi@users.noreply.github.com>
2026-09-18 11:33:31 -03:00
Abhishek Sharma
164043d301 fix(ci): clear the tap.testFiles drift that reds the mutation gate on every PR (#13814)
* fix(ci): clear the tap.testFiles drift that reds the gate on every PR

check-mutation-test-coverage --strict fails on a pristine checkout of
release/v3.8.51 with no PR diff involved, so the mutation-test-coverage
gate is red on every open PR regardless of what it changes.

Six covering unit tests across four mutated modules were absent from
stryker.conf.json tap.testFiles, which means their mutant kills were not
being counted:

  accountFallback.ts          daily-reset-tz-threading, noauth-model-lockout
  sse/services/auth.ts        free-badge-provider-gate, noauth-model-lockout
  combo/comboPredicates.ts    local-token-budget-429-skips-cooldown
  combo/rrState.ts            daily-reset-tz-threading

Four distinct files — two of them cover two modules each. Inserted into
the alphabetical run, matching the file's existing convention; the list
has a second unsorted appended group that is left alone.

After: "No drift — every covering unit test is listed in tap.testFiles",
exit 0. All four files pass (31 tests) so registering them does not
introduce a failing mutation run.

Noticed while reviewing #13743, which targets a fifth file that has
already been registered by 25bc16d87e.

* fix(ci): drop a dangling tap.testFiles entry and guard against new ones

Merging the release line in surfaced that stryker.conf.json still names
tests/unit/plugin-sandbox-permissions.test.ts, which does not exist —
one dangling path out of 428 entries, pre-existing on the base rather
than introduced here.

check-mutation-test-coverage already guards one direction: a test that
covers a mutated module but is missing from tap.testFiles. The other
direction was silent. Stryker resolves the list into its sandbox, so an
entry left behind after its test file is deleted or renamed costs
coverage without failing loudly — the same class of drift this PR is
about, arriving from the opposite side.
2026-09-18 11:33:22 -03:00
Domenico Massafra
6901e72fb8 test(routing): guard Astra vision cutover (#13810)
Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
2026-09-18 11:33:14 -03:00
Domenico Massafra
03c1f7545a fix(antigravity): canonicalize tiered flash quota (#13809)
Co-authored-by: ginettododo <117327638+ginettododo@users.noreply.github.com>
2026-09-18 11:33:05 -03:00
小妍儿 ✨
5de6c5108b fix(usage): preserve nested prompt cache reads before cost calculation (#13760)
* fix(usage): preserve nested prompt cache reads

* docs(changelog): add nested cache-read cost fix

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:32:56 -03:00
Xmon Dai
57d7e861d1 docs: prefill issue titles with Conventional Commits format (#13750)
bug_report.yml and feature_request.yml prefilled `[BUG] ` / `[Feature] ` while
CONTRIBUTING.md documents Conventional Commits with a scope list, so reporters
who followed the template verbatim had their title renamed by triage. The
templates now teach the convention instead: `fix(): ` and `feat(): `.

Each form gains one intro line pointing at the commit-message section of
CONTRIBUTING.md, where the scope list lives. `test_coverage_task.yml` is left
untouched, as its `[Coverage] ` prefix is not part of the convention.

Refs #13688
2026-09-18 11:32:47 -03:00
Goni Sulaiman
21d0e81332 fix(security): enforce allowedEndpoints on the alias rewrites (#13685) (#13741)
Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
2026-09-18 11:32:37 -03:00
Sean Ford
010250cf08 fix(memory): list provider-node models in the Embedding and Rerank selectors (#13740)
* fix(api): type compatible-provider-node models in /v1/models by the node's apiType

Model rows discovered from an OpenAI-compatible provider node rarely carry
endpoint metadata — a TEI / Infinity / vLLM `/v1/models` listing is just ids —
and the catalog defaulted such rows to `["chat"]`. An `embeddings`-typed node
exposing `bge-m3` and a `rerank`-typed node exposing `bge-reranker-v2-m3`
therefore both surfaced in GET /v1/models as untyped chat models: clients
that build their picker from `type: "embedding"` / `type: "rerank"` never saw
them, and chat pickers listed models that 400 on chat.

- src/shared/constants/modelSupportedEndpoints.ts: add
  defaultEndpointsForProviderNodeApiType(apiType) — embeddings → ["embeddings"],
  rerank → ["rerank"], audio-* → themselves, images-generations → ["images"],
  chat/responses/unknown → ["chat"] (unchanged default).
- src/app/api/v1/models/catalog.ts: build a node-id → apiType map next to the
  existing node-id → type map; the synced-model and custom-model loops fall
  back to the node's modality instead of ["chat"] when a row has no
  supportedEndpoints; the custom-overlay merge path also classifies
  `type`/`subtype` from the overlay's explicit supportedEndpoints, so a manual
  `["rerank"]` row layered on a discovered chat-default row is re-typed.

Explicit supportedEndpoints on any row still take precedence, and chat /
responses nodes keep the historical behavior.

tests/unit/catalog-provider-node-apitype-endpoints.test.ts covers the helper
and the catalog end-to-end for embeddings, rerank, mixed, chat, and overlay
cases via getUnifiedModelsResponse().

* chore(changelog): name the #13734 fragment

* refactor(api): keep the provider-node modality helpers out of catalog.ts

catalog.ts is frozen by the file-size gate (must not grow past 2075
lines) and the apiType fallback pushed it to 2093. Move the node
apiType index, the endpoint fallback and the overlay type/subtype
fields into catalogNodeModality.ts so catalog.ts ends one line
shorter than the base; behaviour and tests are unchanged.

* fix(api): give nodeModelEndpoints a string[] return so the catalog classifier typechecks

The API-route typecheck gate flagged TS2345 at both classifyModelSupportedEndpoints()
call sites: the helper returned `ModelSupportedEndpoint[] | unknown[]`, and unknown[]
is not a readonly string[]. The base code only passed because the synced row's
supportedEndpoints was untyped. Same pass-through cast overlayEndpoints() already uses;
no behaviour change.

* fix(memory): list provider-node models in the embedding and rerank selectors

GET /api/memory/embedding-providers and GET /api/memory/rerank-providers
appended local provider nodes by apiType alone and always with models: [].
A node typed "embeddings" that also serves a rerank model — one TEI /
Infinity / vLLM box hosting both bge-m3 and bge-reranker-v2-m3 is the
common self-hosted layout — was filtered out of the Rerank selector
entirely (apiType not in chat/responses/rerank) and showed up in the
Embedding selector as a provider with nothing to pick. Typing prefix/model
by hand worked because the request path resolves it directly; only the
convenience layer was blind.

Add src/lib/memory/embedding/nodeModalityListings.ts, which builds the
listing from the node's synced + custom model rows, typing each row the way
/v1/models does (explicit supportedEndpoints wins, otherwise the node's
apiType via defaultEndpointsForProviderNodeApiType; a custom overlay
re-types a discovered row). A node is listed for a modality when its
apiType matches, when it is a generic chat/responses node (historical
behaviour, kept so catalog-less nodes still appear), or when any of its
rows is typed for the modality. Both endpoints use it; the curated
registries stay first and win on prefix collisions.

* chore(changelog): name the #13740 fragment
2026-09-18 11:32:26 -03:00
Lukas
39cf76c11d fix(gemini): a tool name starting with a digit no longer fails the request (#13738)
Google validates every `functionDeclarations[].name` against one grammar and
rejects the WHOLE GenerateContentRequest when any single one is invalid, so six
`1c_*` tools in a 109-tool MCP catalog made every request 400, including
requests that would never call them (#13715).

`normalizeGeminiToolName` removed invalid characters, collapsed underscores and
stripped leading and trailing ones, none of which touches a leading digit. The
strip is also why a leading underscore was not a workaround: the client's
`_1c_probe` was normalized back to `1c_probe` and rejected for the same reason.

The prefix goes inside the normalizer rather than at the call site, because
that keeps the guarantee on the one value every path reads. `buildHashedGemini
ToolName` builds its name from the normalized string and inherits its first
character, so a fix applied later would hold for short names and fail silently
for the long ones a 109-tool catalog is full of. It also runs before the
collision check, so two names that newly collide are still separated by the
existing hashed path.

The reverse direction needs nothing: the sanitized name now differs from the
client's, so `buildChangedToolNameMap` carries the original and the response
translator restores the client's spelling on the model's functionCall.

Six cells: no declared name starts with a digit, a leading underscore reaches
Google letter-first, the reverse map returns the client's spelling, a
letter-first name is untouched, an over-long digit-first name keeps the
guarantee through the hash path, and two colliding digit-first names stay
distinct. Four mutations, all killed.
2026-09-18 11:32:18 -03:00
Notaloop763
1609767f37 fix(providers): sync OpenRouter :free 1000/day tier from /credits lifetime purchases (#13689)
* fix(providers): sync OpenRouter :free 1000/day tier from /credits lifetime purchases

* chore: rename changelog fragment to PR number 13689
2026-09-18 11:32:10 -03:00
Lukas
ce56098115 fix(models): keep OpenRouter :batch variants out of chat routing (#13622)
* fix(models): keep OpenRouter :batch variants out of chat routing

ModelSync imported OpenRouter's Batch-API-only variants into the chat
catalogue. A chat completion against one is rejected upstream with

  404 This model is only available through the Batch API.
      Use the /api/beta/batches endpoint instead.

which #13596 measured 91 times in 41 hours, third by volume, plus the
`model not found - locking mode` failover churn behind it.

OpenRouter's /models carries no endpoint metadata that separates a batch
variant from a chat one, so `classifyExplicitEndpoints` cannot decide it
and the rule belongs in `modelEndpointPolicy`, beside the OpenAI
image/video policy and for the same reason: the file exists so discovery,
import and catalog projection agree on one answer.

Matched as the exact `:batch` suffix, not "has a variant suffix" --
`:free`, `:nitro`, `:floor`, `:online`, `:extended` and `:thinking` are
routing hints on the same chat model, and excluding them would silently
shrink the routable catalogue. Applied unconditionally for this provider:
there is no "batch" endpoint name an upstream could declare next to a chat
one, and the already-stored rows carry the synthetic `["chat"]` default
that re-imported them in the first place.

Closes #13596

* docs(changelog): add fragment for the OpenRouter batch-variant fix
2026-09-18 11:32:01 -03:00
opensource-elearning
65263a4fe9 fix(cli): Codex long-session turn-pin fallback + codex-settings key resolution (#13564 #13563) (#13566)
* fix(sse): release native Codex turn pin when pinned model is model-scoped unusable (#13564)

Long-running Codex sessions die with 400 NATIVE_CODEX_PINNED_MODEL_UNAVAILABLE whenever
the model pinned to the current turn becomes model-scoped unusable mid-session (per-model
quota lockout, connection cooldown, exhausted accounts). Claude Code has no equivalent
pin and already falls back to the next healthy combo model; Codex now matches.

Release the turn pin when all pinned provider+model targets are model-scoped unusable
and fall through to full combo routing, re-pinning to whichever model succeeds. Preserve
the pin on provider-wide outages (circuit breaker OPEN, provider cooldown) and when the
pinned target is still healthy.

Also prunes the stale ESLint suppression entry for combo.ts that this change orphaned
(createPinnedModelUnavailableResponse import dropped; pre-existing getBootstrapLatencyMs
remains the sole residual unused var).

* fix(api): resolve codex-settings apiKey via canonical resolver instead of 400 (#13563)

Applying Codex settings from /dashboard/cli-code/codex always failed with
400 "baseUrl, apiKey and model are required" when the dashboard sent an empty
apiKey (cloud mode with no management key selected) — baseUrl and model are
already Zod-gated, so that response could only ever fire on the empty key.

The codex-settings route had diverged from the sibling CLI tools (cline/forge/
openclaw/grok-build/jcode): an inline if(!apiKey) 400 guard plus a hand-rolled
getApiKeyById lookup, instead of the shared resolveApiKey(keyId, apiKey) helper
which resolves by keyId, falls back to the submitted apiKey, then to
sk_omniroute. This change makes codex-settings use the canonical resolver, so:

- empty apiKey + valid keyId -> the real DB key is written to auth.json
- empty apiKey + no keyId   -> sk_omniroute default (config still applies)
- explicit apiKey           -> written verbatim (unchanged)

* docs(changelog): add fragments for Codex turn-pin fallback and codex-settings apiKey resolution
2026-09-18 11:31:52 -03:00
sprintberlin
2a89a3bba7 fix(translator): flatten root-level anyOf/oneOf/allOf in Claude tool schemas (#13561)
Anthropic's Messages API rejects a tool whose `input_schema` carries a
composition keyword at the root with:

  tools.N.custom.input_schema: input_schema does not support oneOf,
  allOf, or anyOf at the top level

The refusal happens before inference, so a single MCP/agent tool carrying
a root-level union fails every request that ships the catalog, and combo
failover cannot recover from it.

Both conversion paths that build a Claude `input_schema` from a client
payload now flatten such a root union into a plain object schema:
object-compatible branches contribute their `properties` (root wins on a
name collision), the root is pinned to `type: "object"`, and only `allOf`
contributes `required` — `anyOf`/`oneOf` branch requirements are
alternatives and promoting them would refuse calls the original schema
accepts. Nested unions are untouched and clean schemas pass through
unchanged.

Closes #13552
2026-09-18 11:31:43 -03:00
MumuTW
d86662d582 test(coverage): clean stale coverage output before test:coverage runs (#13408)
* test(coverage): clean stale coverage output before test:coverage runs

* chore(changelog): add fragment for coverage pre-clean
2026-09-18 11:31:35 -03:00
Xore
1c19e2c034 fix(compression): place output-style instruction in top-level system, not messages[0] (#13383)
* fix(compression): place output-style instruction in top-level system, not messages[0]

Anthropic-shaped bodies reject a synthetic system-role entry unshifted at
messages[0] (the claude passthrough forwards messages unchanged, and the
upstream API requires system content in the top-level system parameter).
Output styles and the caveman output mode now land their instruction in
the top-level system field when it exists (string or content-block array)
and only fall back to a trailing system message for OpenAI-shaped bodies,
which the claude system-role extraction hoists. Custom endpoint system
prompts skip the unshift whenever a top-level system field is present.

All compression options stay enabled; placement-only fix.

Fixes #12584

* test(compression): cover system-instruction placement branches 4 and 6

Adds direct unit cases for the placement ladder branches that had no
coverage, asserting where the instruction lands:

- branch 4: merge into a system message at index >= 1, leaving
  messages[0] untouched (plus the skip-over-a-block-content system
  message variant).
- branch 6: trailing append when the body has neither a `system` field
  nor a string-content system message.
- block-array `system`: appends one block and is idempotent on a second
  pass (first coverage of the array path of the marker check).

Also normalizes a malformed non-string/non-array top-level `system`
(e.g. `null`) to "" before the format branch in injectSystemPrompt and
injectCustomSystemPrompt. Previously such a body entered the `system`
branch, matched neither format, and silently dropped the prompt without
falling back to the messages path. Both new guards fail without this
change.

* chore(compression): add changelog fragment for top-level system placement
2026-09-18 11:31:27 -03:00
Patryk Kopyciński
d073f1b273 chore(stryker): register 3 covering unit tests missing from tap.testFiles (#13357)
* fix(test): make npm run test terminate and restore RAYCAST env-doc sync

Two independent defects, both in the test/dev entrypoint layer.

1. `npm run test` never terminated. It was a hand-maintained copy of
   `test:unit` that had drifted: it omitted `--test-force-exit` on BOTH
   node invocations and dropped the trailing `&& npm run test:unit:serial`.
   Per AGENTS.md ('Database Handles in Tests'), unreleased SQLite handles
   make Node's native runner hang indefinitely — every sibling script
   (`test:unit`, `test:unit:ci`, `test:unit:ci:shard`) already carried the
   flag; only `test` did not. Measured on m1max at 84c6ad7c2, same suite
   both arms: without the flag the runner was killed at the 420s ceiling
   (exit 137, no summary line, 23 orphaned node processes); with it the
   runner exited on its own in 419s leaving 1. `test` now delegates to
   `test:unit` so the two cannot drift again, which also makes the serial
   suite reachable from `npm run test` for the first time.

2. Removing the RAYCAST_* rows from ENVIRONMENT.md (#9) broke
   check-env-doc-sync. `parseEnvExampleVars` matches `^#?\s*(VAR)=`, so it
   counts COMMENTED-OUT vars: the four entries still sat at
   .env.example:1263-1266 and became `envMissingDoc` drift the moment their
   docs disappeared. The #9 verification only ran the fabricated-docs gate
   and missed this one. The block is dead either way — it documents
   open-sse/services/raycast.ts and scripts/raycast/usage-benchmark.mjs,
   both deleted with the GPL-derived provider in #11691, and no live code
   reads the vars — so it is removed rather than re-documented.

envMissingDoc is now []. The remaining codeMissingEnv failure
(CURSOR_AGENT_BINARY, CURSOR_MAX_FRAME_BYTES, OMNIROOT) is pre-existing
drift on the base, absent from this diff, and left alone.

* chore(stryker): register 3 covering unit tests missing from tap.testFiles

check:mutation-test-coverage --strict fails identically on pristine
release/v3.8.51 (f1e7148c1) with an empty diff — base debt blocking this PR.

- combo-identical-error-streak.test.ts -> comboPredicates.ts
- 13601-header-drop-count-surfaced.test.ts -> responseHeaders.ts
- semantic-cache-no-truncated-writes.test.ts -> semanticCache.ts

* fix(test): keep the #13187 concurrency-4 cap in test:unit

The dedupe made `test` delegate to `test:unit`, but it also silently
reverted the deliberate local concurrency cap from #13187 ("cap local
unit-test concurrency at 4 to avoid exhausting commit charge") back to 20.

Measured on m1max (16 cores), same suite and same tree, only the flag differs:
  concurrency=20 -> 356 cancelled, 356 "event loop has already resolved" bailouts
  concurrency=4  -> 0 cancelled, 0 bailouts
So 20 does not just slow the run down, it makes the runner abandon tests and
still print a summary -- a false green. Restore 4; termination is preserved via
delegation to test:unit, which already carries --test-force-exit.

* revert(test): drop redundant test-script delegation

The #13187 batch commit (178d25250) already gave `test` both `--test-force-exit` flags and the trailing `&& npm run test:unit:serial` step, so the delegation fix was redundant. It also broke tests/unit/test-serial-quarantine.test.ts, which asserts every parallel runner script ends with the serial step (base 4/4 -> head 3/4). package.json is now byte-identical to base; this PR is the stryker tap.testFiles fix only.
2026-09-18 11:31:19 -03:00
Felipe Britto
138ccf2d04 fix(build): copy ioredis and bcryptjs into the standalone bundle (#13352)
Both packages are only reachable through code paths the standalone
tracer never follows, so they get silently dropped from the built
node_modules/:

- ioredis is a deliberately lazy dependency (#6559 in
  rateLimiter.ts) — reached only via a runtime `await
  import("ioredis")` in rateLimiter.ts,
  warmupScheduler/circuitBreakerFactory.ts and
  quota/redisQuotaStore.ts, never through a static top-level import.
  Any self-hosted deployment that sets REDIS_URL crashes on first use
  with "Cannot find module 'ioredis'".

- bcryptjs is statically imported by
  src/lib/auth/managementPassword.ts, so the main server bundle is
  fine (Next inlines the small pure-JS package into the compiled
  chunk). bin/cli/settings-store.mjs (the `omniroute reset-password`
  CLI) is a separate, unbundled entrypoint that needs the real
  package physically present in node_modules/ — nothing else
  requires it as a loose runtime dependency, so it was never copied.
  `node bin/reset-password.mjs --password-stdin` failed with
  "Cannot find package 'bcryptjs'" (ERR_MODULE_NOT_FOUND) on an
  otherwise healthy production deployment.

Both reproduced on a real self-hosted Docker deployment (v3.8.49/51).
Adds the two entries to EXTRA_MODULE_ENTRIES (the single source of
truth cited in the Dockerfile) and extends the sync/async parity
test with fixtures + assertions for both.
2026-09-18 11:31:10 -03:00
Abhishek Sharma
53a147c7ca fix(backend): stop redaction truncating the message after a path (#13295)
* fix(backend): stop redaction truncating the message after a path (#13144)

`findUnquotedPathEnd` may swallow the rest of a line when it cannot tell
where a path ends, so a Windows path with spaces cannot leak a
`Files\secret` suffix. Two things made that fire far wider than the
function documents at `:606`.

**1. The licence was granted on separator evidence alone.** Every API route
carries slashes, so an ordinary `/v1/x/y` in prose qualified as unequivocal
and truncated everything after it. The image-model 400 lost the one
sentence it exists to deliver:

    built     ...cannot be used on /v1/chat/completions. Use POST
              /v1/images/generations instead.
    delivered ...cannot be used on <path>

Now only a Windows path, a file URI, or a known POSIX filesystem root may
swallow the line. @diegosouzapw's `/zz` vs `/etc` probe on the issue is why
this is the condition and not the first-segment check I originally
proposed: both truncated identically, so the root was never the driver.

**2. The ambiguity branch ran before `resolvedExtensionEnd`.** A path whose
end is pinned exactly by a known extension was still treated as ambiguous
the moment any prose followed it, so the endpoint was discarded and the
line swallowed. A determinable extension leaves nothing to fail closed
about -- the whole path is still replaced, suffix included, and the tail
survives:

    before  Provider failed in <path>
    after   Provider failed in <path> with api_key='[REDACTED]'

Both halves are independently load-bearing: reverting (1) fails the two
route tests, reverting (2) fails the extension test.

Fail-closed is narrowed, not weakened. `/etc/shadow copy failed` has
nothing to anchor an endpoint on and still collapses to `reading <path>`.
Worth recording that the guard test for that cannot be killed by mutating
either mechanism alone -- the two are mutually redundant, so it takes
disabling both, which is also why this change cannot expose a suffix these
shapes did not already hide.

Test results against base:

  chat-rejects-image-only-model          red -> GREEN
  dashboard-request-failed-redaction     its delivered-log assertion now
                                         passes; the test still fails on a
                                         second, unrelated assertion (the
                                         *internal* log is redacted where it
                                         should stay raw) that base never
                                         reached
  tunnel-routes-error-sanitization       unchanged, independent (a tripwire
                                         asserting the shared sanitizer does
                                         NOT cover a shape it now does)

836/840 pass across the sanitization, redaction and error suites; the
remaining failures are the two above plus mcp-public-error-boundaries,
which passes in isolation on base and on this branch and only flakes under
--test-concurrency=8.

* docs(changelog): fragment for the redaction truncation fix (#13144)

changelog.d/README says a PR adds exactly one fragment rather than editing
CHANGELOG.md, so the aggregation order stays deterministic and siblings
cannot conflict. This one was missing.

* test(redaction): narrow the headline case to the truncation it names

Rebased onto a base that has moved 40 commits; resolveEndpoint gained an
ignoreAmbiguity parameter and route-context callers in that window. The
merge keeps both: base's parameter, plus this PR's two changes (check the
resolved extension BEFORE the ambiguity branch, and drop
hasFilesystemEvidence from the swallow licence).

The headline assertion was that the whole message survives byte for byte.
On the current base the tail survives but the route itself still becomes
<path> in that particular message, because the quoted model slug earlier
in the line carries separators. That is a narrower, separate question from
the truncation this PR fixes, so the test now asserts the remediation
sentence survives and records the <path> substitution explicitly rather
than silently dropping the case.

* test(stryker): register the redaction-truncation test for mutation runs

errorPathRedaction.ts is mutation-tested, so a new covering test has to be
in tap.testFiles or the Stryker sandbox never runs it and its mutants
report as survived. Inserted in the alphabetical run beside the sibling
error-sensitive-redaction.test.ts, following #13036's precedent.

The list has a second, unsorted appended group; left that alone rather
than re-sorting a file this PR only needed one line in.

Requested in review by @diegosouzapw.
2026-09-18 11:31:02 -03:00
小妍儿 ✨
6d585625f0 fix(providers): honor Alibaba workspace embedding and rerank endpoints (#13293)
* fix(providers): honor Alibaba workspace embedding and rerank endpoints

* docs(changelog): add Alibaba workspace endpoint fix

* refactor(rerank): keep Alibaba response adapter focused

---------

Co-authored-by: 千乘妍 (Xiaoyaner) <xiaoyaner0201@users.noreply.github.com>
2026-09-18 11:30:53 -03:00
SIGTERM
2233a14a87 fix(vertex): preserve Claude prompt caching and usage metadata (#13220)
* fix(vertex): preserve Claude prompt caching

* fix(vertex): normalize unsupported cache TTLs

* docs(changelog): note Vertex prompt caching fix
2026-09-18 11:30:45 -03:00
Wu Shuwen
36493a6270 fix(evals): fail a case whose model call errored instead of scoring it passed (#13201)
* fix(evals): fail a case whose model call errored instead of scoring it passed

runSuite() attached caseMetrics[id].error to the graded result but never forced
`passed` to false. executeEvalCase() returns a failed call as an ordinary output
string ("[ERROR] <message>"), so any expected pattern that happened to match that
text was recorded as a pass. That inflates the reported pass rate, and reports a
non-zero score for a run in which no model was ever reached.

Built-in codex-comparison case codex-07 reproduces it: its pattern is
"try|catch|throw|error|Error" and the provider-resolution failure text ends with
"...added as a combo entry.", so the `try` alternative matches and the case is
scored as passed while carrying a non-empty error.

A case that never reached a model has no measured behaviour to grade, so a
failure is the only honest score.

Refs #13137

* docs(changelog): add fragment for the errored-eval-case fix (#13201)
2026-09-18 11:30:35 -03:00
Abhishek Sharma
bc72d03970 test(usage): pin fetcherProviders against supportedProviders (#13134)
The two lists are sibling pure-data modules that have to agree, and nothing
compared them. A provider in fetcherProviders but not supportedProviders
computes a quota nobody ever asks for; one in supportedProviders with no
dispatcher case is accepted and then falls through to
`default: "Usage API not implemented"`.

That seam has produced the same bug three times -- #9603 (bailian, whose
own comment reads "fetcher existed, list entry missing"), #11722, and
#12256 (openrouter credits, which #13080 then asked for again six days
after the fix landed because the card had never appeared).
usage-families-split.test.ts pins the dispatcher against fetcherProviders,
so the open-sse side cannot drift; this pins the other edge.

Divergence stays allowed but has to be declared with a reason:

  opencode, opencode-zen, xai   have a fetcher, not offered to the dashboard
  xiaomi-mimo-token-plan        offered, no fetcher -- but inert, because it
                                authenticates by API key and is absent from
                                PROVIDER_LIMITS_APIKEY_PROVIDERS, so
                                isSupportedUsageConnection refuses it before
                                the dispatcher is reached

I could not establish intent for the first three from the tree, so they are
pinned rather than "corrected" -- `xai-oauth`/`xao` are offered while the
API-key `xai` is not, which reads like it may be deliberate. A third test
fails if a declared divergence stops diverging, so the allowlists cannot go
stale and quietly excuse a future recurrence of the same id.

Mutations, each killed by the right test:

  new fetcher, no dashboard entry   -> the forward guard + the stale check
  dashboard entry, no fetcher       -> the reverse guard + the stale check
  a declared divergence gets fixed  -> the stale check alone
  duplicate id in either list       -> the duplicate test alone

105 related tests pass (this, usage-families-split, provider-plugin-manifest,
provider-limits*), eslint clean. Test-only; no production file touched.
2026-09-18 11:30:27 -03:00
Dor Amir
cf57129c09 fix(tests): retire claude-3-5-sonnet-20241022 from the combo integration suites (#13056)
* fix(tests): retire claude-3-5-sonnet-20241022 from the combo integration suites

Sibling of #12670. Eight integration suites still targeted the retired claude-3-5-sonnet-20241022, which the vendor lifecycle registry rejects with HTTP 410, so every combo listing it lost that target: 23 cases failed on the release tip for one cause. Replace it with claude-sonnet-4-6, the successor the lifecycle record names and the id chat-pipeline.test.ts already uses. Fixture model id only; no assertion changed, no production code touched.

* docs(changelog): fragment for the retired-fixture cleanup (#13056)
2026-09-18 11:30:19 -03:00
Tony Yu
3d3f71f514 fix(oauth): read pollToken body once on non-JSON upstream responses (kimi-coding, github) (#13046)
* fix(oauth): read pollToken body once on non-JSON upstream responses

The device-flow pollToken handlers for kimi-coding and github tried
response.json() first and fell back to response.text() in the catch.
Once .json() rejects on a non-JSON body the stream is already consumed,
so the .text() fallback always throws TypeError (Body is unusable) and
pollToken rejects, surfacing as a generic 500 on /api/oauth/<provider>/poll
instead of the intended graceful { error: "invalid_response" } payload.
Non-JSON responses are realistic when the OAuth upstream sits behind a
CDN/anti-bot HTML error page or a proxy interstitial (auth.kimi.com in
particular).

Read the body once as text, then JSON.parse it, preserving the original
invalid_response fallback. Adds a regression test that drives both
providers with a stubbed fetch returning an HTML error page and a JSON
error body. Prunes the two now-unused no-unused-vars suppressions for the
removed catch bindings.

* docs(changelog): fragment for #13046
2026-09-18 11:30:09 -03:00
ZaimMarzuki
bb198df737 fix(analytics): resolve account email/name in Utilization chart and fix tooltip stacking context (#13029)
Co-authored-by: ZaimMarzuki <ZaimMarzuki@users.noreply.github.com>
2026-09-18 11:30:01 -03:00
Felipe Fidelix
1533f16ed8 fix(claude): forward client-negotiated thinking-binding-controls and thinking-display-updates betas (#12989)
* fix(claude): forward client-negotiated thinking-binding-controls and thinking-display-updates betas

Gateways drops anthropic-beta tokens not on FORWARDABLE_CLIENT_BETAS, so
@ai-sdk/anthropic Fable 5.1 requests carrying thinking.block_binding were
rejected upstream with thinking.adaptive.block_binding: Extra inputs are
not permitted even when the client negotiated
thinking-binding-controls-2026-08-01 correctly. Same class for
thinking.display updates (thinking-display-updates-2026-08-18).

Regression guard: tests/unit/thinking-binding-controls-beta-forward.test.ts

* chore(changelog): fragment for #12989

* chore: trim comments
2026-09-18 11:29:54 -03:00
Amirreza Kimiyaei
a5d603ebc9 fix(soniox): pass client parameters through and surface speaker diarization (#12948)
handleSonioxTranscription took no formData, built the job body from a fixed
three-key object, and reduced the transcript to { text }. Every client-supplied
parameter was therefore dropped in silence: the request returned 200, the flag
had no effect, and from the caller's side an unsupported parameter and a dropped
one looked identical. Diarization and Soniox's `context` were both unreachable,
and the per-token `speaker` attribution Soniox returns was discarded.

The handler now receives the form data (as the Deepgram one already did) and
maps what the caller asked for onto the job: diarization under the three
spellings callers reach for, `context` verbatim, and `language` as a Soniox
language hint. Keys are added only when requested, so a request carrying no
options produces byte-identical job bodies and the existing
audio-soniox-provider deep-equality assertions still hold.

Response shape stays `{ text }` by default. When diarization or
response_format=verbose_json is requested, the token stream is collapsed into
contiguous single-speaker runs and returned as OpenAI-style `segments` carrying
`speaker`, with `words` when word granularity is asked for.

Verified against a live Soniox account on a real two-party Persian phone call:
default response unchanged, 13 segments over 2 distinct speakers with turn
boundaries matching the dialogue, and `context` correcting a proper noun the
model otherwise gets wrong.

Closes #12947

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:29:46 -03:00
Thiago Mafra
7a18f344b3 fix(translator): stop double-counting cached tokens in Gemini to Claude usage (#12863)
* fix(translator): stop double-counting cached tokens in Gemini→Claude usage

* docs(changelog): add fragment for gemini-to-claude cached input tokens fix
2026-09-18 11:29:38 -03:00
Dominatorrr
e20f5f34ea fix(cursor): preserve native Claude effort model IDs before executor dispatch (#12838) 2026-09-18 11:29:31 -03:00
KeelTrace
3e7764e57d fix(combo): fall through on pinned 401 responses (#12818)
* fix(combo): fall through on pinned 401 responses

* test(combo): cover pinned 401 in existing fallback regression

---------

Co-authored-by: Hermes Freebrain <freebrain@localhost>
2026-09-18 11:29:23 -03:00
MSiva
0e7925309d fix(translator): map Claude stop_sequences and stop to Gemini stopSequences (#12785) 2026-09-18 11:29:15 -03:00
Goni Sulaiman
0551893390 fix(dashboard): expose the Modal Base URL field in the connection modals (#12704) (#12736)
Modal is bring-your-own-deploy, so every connection needs its own app URL, and
the server-side validator already required providerSpecificData.baseUrl. The
add/edit connection form never rendered the field, so a Modal connection could
not be validated or saved at all.

Adding the id to CONFIGURABLE_BASE_URL_PROVIDERS reuses the same always-on Base
URL field as the kimi/moonshot case (#7447). The placeholder switch is folded
into a record lookup in the same commit so the function stays under the
complexity cap as ids are added; the record was checked against the switch for
every pre-existing id and only "modal" changes behaviour.

Rebased onto the current release tip, which now carries #13120's own entry in
the same set.

Co-authored-by: Goni Sulaiman <gonisulaimann@users.noreply.github.com>
2026-09-18 11:29:08 -03:00
Juri
22511bccb9 feat(providers): register gemini-3.8-flash (#12663)
* feat(providers): register gemini-3.8-flash

* chore(changelog): add fragment for gemini-3.8-flash #12638
2026-09-18 11:29:00 -03:00
Mike
82b02f6054 fix(cli): detect npm-global Claude .cmd shims under Program Files on Windows (#12565)
* fix(cli): find npm-global Claude .cmd shims under Program Files\nodejs (#12563)

Stock Node MSI installs drop claude.cmd there, but detection never listed that directory and Electron's PATH often omits it, so the dashboard reported settings_found_binary_unresolved while a normal shell could run the CLI.

* fix(cli): soft-fail npm prefix cache and enrich Windows lookup PATH (#12563)

Stop permanently caching a failed npm config get prefix as empty, and prepend npm-prefix / APPDATA\npm / nvm / Program Files dirs on Windows lookup PATH so custom installs survive Electron PATH gaps.
2026-09-18 11:28:52 -03:00
Alex Chan
190c80dd1b fix(sse): prefer cgroup PSI for chat admission (#12562)
Chat admission sampled host-wide /proc/pressure/memory, so a swapping
Docker host 503'd idle containers with resource_pressure. Prefer this
unit's cgroup memory.pressure and keep the host file as fallback.
2026-09-18 11:28:45 -03:00
Wahyu Hidayatulloh Pamungkas
d8c0182f62 fix(command-code): floor tiny muse-spark output budgets so hidden reasoning cannot consume the whole budget (#12497)
* fix(command-code): floor tiny muse-spark output budgets so hidden reasoning cannot consume the whole budget

muse-spark models routed through command-code burn the entire output budget on
hidden server-side reasoning before emitting visible content. A small caller-set
max_tokens (e.g. 64) comes back as HTTP 200 with null content (out=64, reasoning=61).
Reuse the prefix-aware MUSE_SPARK_PATTERN to detect prefixed ids (meta/muse-spark-1.2-contributor,
cmd/meta/muse-...) and floor tiny budgets to 512 in both the /provider/v1 path and
the /alpha/generate fallback. No budget is synthesized when absent; large budgets untouched.

* docs(changelog): add changelog fragment for the command-code muse-spark budget floor (#12497)
2026-09-18 11:28:36 -03:00
tanveer-arch
51227ccd85 fix(perplexity-web): preserve system contract on follow-up requests (#12443)
* fix(perplexity-web): preserve system contract on follow-up requests

* fix(perplexity-web): preserve search hint on follow-up requests
2026-09-18 11:28:28 -03:00
Kizuno18
6d0dc5a50c fix(combo): scope Claude model failures to the model, not the account (#12340)
* fix(combo): scope Claude model failures to the model, not the account

A priority combo whose steps are five models on one Claude OAuth connection
stops at step 1. hasPerModelQuota() returns false for the claude provider, so
markAccountUnavailable() records a model-specific 404 or 5xx against the
connection row, and getPersistedConnectionCooldownSkipReason() then skips every
sibling step before dispatch. Verified with a combo whose first step names a
model that cannot exist: the 404 lands and step 2 is never tried, while the
same combo works as soon as step 2 points at a different account.

A Claude OAuth connection multiplexes Fable 5, Opus 5/4.8/4.7/4.6, Sonnet and
Haiku behind one credential, which is the multiplexing hasPerModelQuota already
describes. Its quota is a separate question: a 429 on a Max subscription is
account-wide, and shouldMarkAccountExhaustedFrom429 pins that. So this adds
hasPerModelFailureScope() for the non-quota statuses and leaves 429 alone.

Combo exhaustion gets the same treatment for 404, which names one model the
account cannot serve rather than a bad connection. The empty-content 502 was
already exempt through isEmptyContentFailure.

Closes #12334

* fix(combo): extract Claude per-model failure scope to keep file-size caps

Move hasPerModelFailureScope into a leaf so frozen accountFallback.ts and
auth.ts stay at their file-size caps after #12334. Register
combo-claude-per-model-scope.test.ts in Stryker tap.testFiles so the
mutation-test-coverage gate sees the covering unit test.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:28:20 -03:00
Bl0ck
efbcc3d489 fix(antigravity): surface image quotas in Provider Limits (#12311)
Rebase the focused image-quota visibility fix onto the current release/v3.8.51 tip while preserving the newer shared Antigravity catalog and AGY live-discovery changes.
2026-09-18 11:28:11 -03:00
brick30llc-ctrl
3a5f37013d fix(sse): classify "N API calls / month" 429 text as quota_exhausted (#12252)
* fix(sse): classify "N API calls / month" 429 text as quota_exhausted

QUOTA_PATTERNS recognized "monthly ... limit" and "per month ... limit",
but not the reversed order providers actually use in trial-key messages —
Cohere's is "You are using a Trial key, which is limited to 1000 API calls
/ month". That 429 fell through to the generic short-backoff rate_limit
path, so a monthly allowance that will not reset for the rest of the
billing cycle kept being retried every few seconds.

Broaden the pattern set to catch both phrase orders and the "N calls /
month" and "N requests/month" spellings.

Observed in production against Cohere trial keys: 481 of 752 429s in 24h
carried this text and were all treated as short transient rate limits.

* docs(changelog): add fragment for #12252

---------

Co-authored-by: brick30llc-ctrl <admin@brick30.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-18 11:28:03 -03:00
diegosouzapw
bb85d56155 chore(quality): rebaseline codex.ts 1552->1553 (drift from #14065)
The release tip went red on check:file-size after #14065 (fix(codex): whitelist
reasoning object keys before the wire, #13643) merged with +1 line in
open-sse/executors/codex.ts and no baseline entry — the PR->release fast-gates
do not run check:file-size. Every merge-train boarding after it inherits the
red, so the drift is absorbed once at the tip (owner-approved train-rebaseline
policy, 2026-09-18). Measured clean: check:file-size passes on the tip with
this entry.
2026-09-18 11:02:25 -03:00
Bob.Hou
70e311703e ci(acceptance): emit a shadow release-acceptance report next to release-green (#13701) (#14066)
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!

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-09-18 10:21:59 -03:00
Bob.Hou
de428cef86 fix(codex): whitelist reasoning object keys before the wire (#13643) (#14065)
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!

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-09-18 10:21:44 -03:00
Bob.Hou
815dedd3c9 fix(resilience): extend process crash guard to combo hedge cancels and upstream fetch failures (#13636) (#14064)
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!

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-09-18 10:21:32 -03:00
Bob.Hou
9b92477b40 fix(providers): declare Agnes official thinking effort tiers (#13655) (#14063)
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!

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-09-18 10:21:17 -03:00
Bob.Hou
9956f13b35 fix(db): never TRUNCATE-checkpoint a live WAL (SIGBUS under traffic) (#14005)
* fix(db): never TRUNCATE-checkpoint a live WAL

A live TRUNCATE checkpoint rewrites the shared wal-index while other
processes hold it mapped; dereferencing the stale mapping SIGBUSes the
process. Two production crashes six hours apart, coredump stack in
better-sqlite3 native memcpy (issue #13973).

Remove the periodic TRUNCATE scheduler. Runtime checkpoints are
PASSIVE-only, which move pages without changing the wal-index geometry,
while TRUNCATE stays on the shutdown path where reclaiming the file is
safe. The 256MB size guard now warns instead of escalating to a live
TRUNCATE, busy PASSIVE ticks feed the persisted busy telemetry that the
TRUNCATE tick used to carry, and a positive
OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS logs a one-time deprecation warning.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(db): RESTART the WAL when it exceeds the size guard

The 256MB size guard only warned, so a live WAL could keep growing until
the next restart. wal_checkpoint(RESTART) starts a new WAL file without
rewriting the mapped wal-index, which is what SIGBUS'd the process when
we used TRUNCATE under traffic.

Related to #13973.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* docs: drop a fake TRUNCATE env name from the WAL guard row

Backticks around TRUNCATE made the env/docs checker treat it as a
variable. The VACUUM rows next to it were never part of this change
and are not in the base docs.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-09-18 10:21:03 -03:00
Diego Rodrigues de Sa e Souza
7cc454d931 fix(compression): repair the unit-test base-reds left by the 09-17 merge wave (#14082)
Every PR against release/v3.8.51 is born red on all four Unit Tests fast-path
shards. Reproduced on the pure tip (1603c86e): ~35 tests. This commit repairs
the two large clusters plus the stale assertions sharing their root cause
(17 tests); the rest is tracked separately.

RTK (12 tests, real regression): #13521 gated dedup on
  skipFilters || isDocumentLikeRead
but isDocumentLikeRead is true for ANY text whose type detection is unknown —
not only non-shell tool results — so plain repeated tool output (the classic
RTK case) stopped being deduplicated, processRtkText returned no stats, and
rtkEngine.apply().stats.engine came back undefined. The intent of #13388 was
the non-shell-tool skip, which resolveToolMeta already expresses as
skipFilters; dedup now honours only that. The #13521 regression guard
(rtk-file-content-preservation, tool named 'read' → skipFilters) still passes
4/4; rtk-engine is back to 9/9.

Golden + stale assertions (5 tests, product changes never propagated):
- tests/snapshots/provider/translate-path.json: xKiro (#12648) was added to the
  registry without regenerating the snapshot — purely additive entry.
- providers-constants-split: APIKEY_PROVIDER_COUNT 241 → 242 (xKiro).
- tests/snapshots/g13/combo-chatcore-public-seams.json and the three
  Content-Type assertions in chatcore-translation-paths / chat-route-coverage:
  #13419 deliberately made streaming responses declare
  'text/event-stream; charset=utf-8' (Arabic/Persian mojibake) and updated the
  integration test but not these unit assertions.

Before/after on the 26 base-red files: 266 tests, 23 → 18 failing here
(the RTK suites were counted per-file in CI; per-test this is 17 fixed). The
file-size ✗ on chatcore-translation-paths.test.ts is the pre-existing drift
#14016 rebaselines — this commit keeps that file's line count unchanged.
2026-09-18 08:13:49 -03:00
Diego Rodrigues de Sa e Souza
24fc202d9f fix(ci): repair the API Route Typecheck base-red blocking every PR (#14079)
The gate fails on the pure release/v3.8.51 tip (3 files above the frozen
baseline), so every PR against the release is born red on it. None of the
three is a PR defect — they are drift from merged work:

- src/lib/usage/glmResetCards.ts: entered the gate's scope when #12754 added
  the route that imports it. runWithProxyContext is an untyped async helper
  (Promise<any>), so runWithConnectionFetch<T> could not return T. Every call
  site passes an async callback and awaits it — declare that contract:
  fn: () => Promise<T> → Promise<T>.
- src/sse/handlers/chat.ts: handleSingleModelChat had no return annotation, so
  runWithTransientBackendRetry<T extends ResponseLike> fell back to the
  constraint and the value could no longer feed withSessionHeader(Response).
  Annotated Promise<Response> (every return path builds a Response).
- src/app/api/internal/codex-responses-ws/route.ts: the bridge helpers return
  either { error: Response } or a payload, but as unannotated object-literal
  unions TypeScript synthesised error?: undefined on the success member, and
  every "error" in x guard stopped narrowing (TS2339 x5 on the destructure).
  Explicit return types keep the discriminant real; the ApiKeyMetadata alias now
  points at the policy shape (the wider one both sources are assignable to),
  which clears the TS2740 self-mismatch; logger.warn → log.warn (the module
  logger factory has no .warn).

check-api-typecheck.mjs: OK — 283 errors, baseline ratcheted DOWN from 294
(codex-responses-ws 7→4 TS2339, TS2740 1→0; combos/test and keys/[id] 1→0).
typecheck:core clean; 43/43 unit tests in the touched areas green.
2026-09-18 08:13:43 -03:00
Diego Rodrigues de Sa e Souza
1b484c57a0 chore(quality): rebaseline the last ceiling the 09-17 merge wave moved (#14016)
tests/unit/chatcore-translation-paths.test.ts 3447 -> 3449, from #13173 (Fable
mid-conversation cache prefixes — the new assertions for that case).

This is the only one left. The other two this PR originally carried
(chatHelpers.ts and chatCore.ts) were absorbed by the rebaselines the merging
PRs brought with them, so the branch was rebuilt on the current tip rather than
shipping stale numbers. Measured clean: check:file-size and test-file-size both
pass with no violations.
2026-09-18 08:13:38 -03:00
Diego Rodrigues de Sa e Souza
94aa978c2a fix(ci): document OMNIROUTE_STRIP_SYSTEM_PREAMBLE — the env/docs base red blocking every PR (#14022)
* fix(ci): document OMNIROUTE_STRIP_SYSTEM_PREAMBLE (env/docs contract base-red)

* fix(ci): allowlist COMBO_LOOP_SAFETY_TIMEOUT_MS as a doc-only source constant

The env/docs contract gate had a SECOND violation on the release tip, added after
this branch was cut: #13857's comboTimeoutMs narrative in ENVIRONMENT.md cites
COMBO_LOOP_SAFETY_TIMEOUT_MS, which is a source constant
(open-sse/services/combo/comboPredicates.ts:35 — `10 * 60 * 1000`), not an
operator-facing env var. The doc regex captured the SHOUTY_NAME and reported it
as documented-but-missing-from-.env.example.

DOC_ONLY_ALLOWLIST already exists for exactly this class (see CLI_COMPAT_OMITTED_PROVIDER_IDS,
LOCAL_ONLY_API_PREFIXES, VACUUM). Gate now reports all three directions in sync.
2026-09-18 08:13:32 -03:00
Diego Rodrigues de Sa e Souza
b45e0a4b59 fix(i18n): review the 64 non-pt-BR dashboard catalogs for translation quality (#14078)
* fix(i18n): review-locale reviews every leaf of a catalog that did not exist at --since

* fix(i18n): review the 64 non-pt-BR dashboard catalogs for translation quality

Runs scripts/i18n/review-locale.mjs over every locale except pt-BR (done in
#13885): 75,263 corrections applied against the English source, 1,677 of
them reverted because the "correction" replaced a real translation with the
plain English term (the real-translation ratio gate counts those as
untranslated). zh-CN/zh-TW provider term normalised after the run.

review-locale.mjs hardening found by the run: per-batch retries with backoff
(a skipped batch is listed, not fatal), catalog checkpoint every 25 batches,
and setDeep resolving leaf keys that contain a dot.
2026-09-18 07:09:03 -03:00
anhtahaylove
1603c86e06 test: realign two stale assertions with product behavior (#13313) (#13315)
* test: expect the jina alias prefix in the custom-model catalog case

The custom-model assertions expected ids prefixed `jina-ai/`, but the catalog
prefixes model ids with the provider alias, which is `jina`. The synced-model
test directly above asserts `jina/` and passes, so the two cases contradicted
each other within the same file.

The expectation predates the alias: the test was written in v3.7.9 (2026-05-04)
and `alias: "jina"` was added in v3.8.36 (2026-06-25).

Aligns the two assertions with the sibling test and with the product. The file
now passes 44/44 (was 43 with 1 failure).

Confirmed the assertions still bite: renaming the alias to `jina-XX` fails
exactly these two cases.

* test: pin the pt-BR pack in the two language-pack fixtures

Both tests assert the Portuguese output-style string but configured
languageConfig with enabled:false and defaultLanguage:"en".
resolveOutputStyleLanguage returns "en" on its first line when enabled is not
true, so the English pack was injected and the assertion could never hold.

autoDetect:true would not have helped either: the user turns in these fixtures
are English, so the detector resolves back to "en". The pack under test has to
be pinned, hence autoDetect:false with an explicit defaultLanguage.

This restores coverage rather than just turning the suite green. Mutating the
pt-BR pack string in outputMode.ts now fails exactly these two tests; with the
old fixture the file reported 9 pass / 2 fail whether the pack was intact or
mutated, so it detected nothing. File is 11/11 (was 9 + 2 failures).

The third languageConfig fixture in this file belongs to an rtk test that makes
no language assertion and is left untouched.

* test: keep the canonical jina-ai prefix in the catalog case

Reverts 8d371d4. The catalog file holds two Jina cases that are not the
same scenario: the custom-model/alias case legitimately expects the
`jina/` alias prefix, while the specialty-model case expects the
canonical provider id `jina-ai/`. Aligning the second to the first reads
like a fix but changes a passing assertion into a failing one.

Verified against the current release tip by mutation, both directions:
the specialty case passes as `jina-ai/...` and fails as `jina/...`, with
the runner reporting `actual: 'jina-ai/jina-embeddings-v5-text-small'`.
The canonical ids are what the source declares — `embeddingRegistry.ts`
and `rerankRegistry.ts` both key the provider as `jina-ai`, as does
EMBEDDING_RERANK_PROVIDER_IDS in src/shared/constants/providers.ts.

The pt-BR language-pack commit on this branch is untouched: that one is a
real fix and repairs two genuinely failing assertions.

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 22:13:39 -03:00
Fouad Salkini
176d632a2d feat(api): add per-key allowAutoCombos to gate the built-in auto/* combos (#13670)
* feat(api): add per-key allowAutoCombos to gate the built-in auto/* combos

`auto/*` combos currently bypass per-key authorization entirely. They are
virtual — synthesised in the catalog, never stored as combo rows — so
`resolveRequestedComboName()` returns null for them and
`isComboAllowedForKey()` fails open:

    const comboName = await resolveRequestedComboName(modelStr);
    if (!comboName) return { allowed: true, comboName: null };

`validateModelAccess()` then sets `requestedComboName = modelStr` for any
`auto/` id and returns before `isModelAllowedForKey()` runs, so
`allowedModels` and `blockedModels` are skipped for those ids too.

The effect is that `allowedCombos` does not constrain `auto/*`: a key
scoped to a single cheap lane can still send `auto/best-coding` and reach
every model on the gateway. `blockedModels: ["auto/*"]` only unadvertises
the ids — it cannot deny them.

Add an explicit per-key flag instead of tightening the fail-open, which
would silently revoke `auto/*` from every key whose `allowedCombos` lacks
an entry for it. `allow_auto_combos` is NOT NULL DEFAULT 1 and the row
parser treats anything but an explicit falsy value as allowed, so every
existing key keeps working and opting out is deliberate.

When set to false:
  - `validateModelAccess()` rejects `auto/*` for that key;
  - the catalog skips the `auto/*` synthesis loop for it, reusing the
    existing `hideAuto` break so the key is not offered ids it cannot use.

Settable via PATCH /api/keys/[id]. The create path and the dashboard
toggle are deliberately left for a follow-up: the API Manager control
needs UI strings across all message catalogs, which does not belong in
the same change as the policy fix.

* feat(dashboard): add the Auto Combos toggle to API key permissions

Exposes the `allowAutoCombos` flag in the API Manager permissions modal so
the per-key gate can be managed from the dashboard rather than only over
the API.

The control mirrors the prompt-compression toggle: a small dedicated
component, a `role="switch"` button, and labels from the `settings`
message namespace.

Defaults to ON. State reads `apiKey?.allowAutoCombos !== false` — using
`!== false` rather than `=== true` so a key that predates the column, or
one that has never been configured, renders as enabled and matches the
`NOT NULL DEFAULT 1` column.

The field is threaded through all three positional lists (the save
handler signature, the modal prop type and the onSave call) plus the
PATCH payload, so no later argument shifts position.

UI strings are added to en.json and to vi.json. Vietnamese is translated
rather than left as a sync placeholder because
tests/unit/i18n-vi-completeness.test.ts asserts key parity with English
and bans `__MISSING__` markers in that locale. The remaining locales fall
back to English at runtime; `i18n:check-ui-coverage` still passes well
clear of its threshold. They are deliberately not mass-synced here: a
full `i18n:sync-ui` run also replicates ~844 unrelated pre-existing gaps
across all 50 catalogs, which does not belong in this change.

* feat(api): advertise the combo description in /v1/models

A combo's description is stored on its record and returned by
GET /api/combos, but the catalog row never carried it, so no client could
show it.

Claude Code's gateway model discovery reads exactly `id`, `display_name`
and `description` from each entry in the /v1/models `data` array and
renders the description in the /model picker — an entry without one reads
"From gateway" instead. Other OpenAI-compatible clients surface it too.

Emit it only when the combo actually has one, so rows for combos without
a description are byte-identical to before. The value is typeof-narrowed
and trimmed because ComboRecord is Record<string, unknown>, and
`comboMetadata` still spreads last so context and capability metadata
keep precedence.

`display_name` is deliberately not sent: a combo's id is already its
human-chosen name, and the field is only consulted when it differs from
the id.

Ref: https://code.claude.com/docs/en/llm-gateway-protocol.md#model-discovery

* fix(api): list a key's allowed combos in /v1/models

`allowedCombos` gates combos; `modelAccessMode`, `allowedModels` and
`blockedModels` gate provider models. The catalog consulted only the
latter, so a key with `modelAccessMode: "restricted"` and an empty
`allowedModels` received an empty catalog — zero rows — while every combo
in its `allowedCombos` dispatched normally. The catalog contradicted the
key.

Observed on a live gateway: a key with 24 entries in `allowedCombos` and
`restricted` + `allowedModels: []` returned {"object":"list","data":[]},
yet `claude-orchestrate` answered 200 on that same key.

Gate combo rows on `allowedCombos` instead of hiding them. Listing a
combo the key can already dispatch grants no new access, so this is a
consistency fix rather than a relaxation, and it needs no opt-in: the
rule is simply that a key's catalog shows what that key can use.

auto/* rows are exempt. They fail open at dispatch — they resolve to no
stored combo — and their synthesis is already gated by allowAutoCombos,
so gating them here would make the catalog stricter than dispatch.

The decision lives in a new exported helper, isComboNameAllowedForKey(),
which wraps the existing matchesComboAccessRule. An absent list means no
combo restriction, matching validateComboAccess, which skips the check
when allowedCombos is not an array; an empty list allows nothing.

Also advertise `display_name` on combo rows from an operator-set
`displayName` field. Claude Code uses it as the picker entry's name when
it differs from the id, which lets a combo carry a discovery-compatible
id and still read cleanly. It is never derived from the combo name — an
unset field advertises nothing.

* fix(api): accept displayName on the combo schemas

The previous commit advertises `display_name` in /v1/models from a
combo's `displayName`, but neither createComboSchema nor
updateComboSchema declared the field, so Zod stripped it from every
request body and the value could never be set. The endpoint would have
answered 200 and written nothing — the feature was unreachable.

This is the same silent no-op that made `blockedModels` unsettable on
API keys: a field plumbed through the route and the store, missing only
its schema declaration.

Declare it on both schemas and count it in updateComboSchema's "no valid
fields" guard, so a body carrying only `displayName` is a valid update
rather than being rejected as empty. Nullable on update so a label can be
cleared.

* feat(api): add per-key catalogScope to scope what /v1/models advertises

A key had no way to say which kinds of thing its catalog should list. It
always advertised whatever the key's model and combo policies permitted,
mixed together. A client that builds its model picker from /v1/models —
Claude Code's gateway discovery, for one — then sees provider models
alongside the curated combos it was meant to offer.

Add a three-way per-key setting: "all" (default), "combos", "models".

This is a listing preference, not an access control: narrowing it never
changes what the key may dispatch, which the model policy and
allowedCombos continue to decide. That is why it is an explicit setting
rather than implied behaviour — unlike gating combo rows on
allowedCombos, which was a correctness fix and needed no opt-in.

Defaults to "all" everywhere: the column, the parser, the metadata and
the UI state, so every existing key is unchanged. The parser widens to
"all" on an unrecognised value rather than narrowing, so a bad value can
never silently hide rows an operator expects to see.

The dashboard control is a segmented radio group beside the Auto Combos
toggle. UI strings are added to en.json and vi.json; the remaining
locales fall back to English, and vi is translated rather than left as a
sync placeholder because tests/unit/i18n-vi-completeness.test.ts asserts
key parity and bans markers there.

* fix(api): invalidate the model catalog on key visibility changes

updateApiKeyPermissions already advances the unified /v1/models catalog
generation for the fields that change what a key may dispatch, but the two
fields this branch introduces -- allowAutoCombos and catalogScope -- were
missing from that predicate. Both change what the catalog advertises, so a
PATCH toggling either one left the request-shaped catalog cache serving the
previous listing until its TTL expired, and the dashboard's API-key screen
could show a catalog that disagreed with the key it had just written.

Add the two fields to the existing predicate -- no new cache machinery. The
call still runs only after a successful write, so a no-op or failed update
does not invalidate, and unrelated metadata edits (isActive, rate limits)
still leave the catalog cached.

Observed on a live deployment before the fix: PATCH catalogScope="combos"
returned 200 and the column read back "combos", yet GET /v1/models kept
returning the previous mixed rows until a process restart, after which the
same key correctly returned combo-only rows.

* docs(changelog): add fragment for per-key allowAutoCombos and catalogScope

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(quality): rebaseline the two ceilings this PR's own growth moved

src/app/api/v1/models/catalog.ts 2075 -> 2117 and src/lib/db/apiKeys.ts
1625 -> 1659. Measured on the clean tip first: catalog.ts sits at 2074 (under
its 2075 ceiling) and apiKeys.ts at 1620 (under 1625), so none of this is
inherited — it is the feature itself. Gating the built-in auto/* combos per key
means the permission field has to be read, validated and carried all the way to
the catalog filter, and each of those is an explicit call site rather than
something extractable without hiding the gate.

Covered by the PR's 25 tests. The other violations in this tree (chatHelpers.ts,
chatCore.ts, chatcore-translation-paths.test.ts) are inherited base-reds and were
left untouched.

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:53:16 -03:00
Bob.Hou
b9dd80c8c2 fix(chat): preserve suffix reasoning intent across model attempts (#13720)
* chat/suffix-effort: keep reasoning intent tied to each model attempt

Carry resolved suffix effort through dispatch without treating a derived
value as explicit client input. Prepare reasoning defaults and dependent
parameter constraints for each handler attempt so a replacement model
does not inherit the original model's suffix.

Keep explicit reasoning choices in context-aware request hashes to avoid
sharing concurrent responses across different effort settings. Preserve
the legacy hash interface and tenant namespace.

Exercise retries, credential refresh, tool follow-ups, replacement models
and overlapping requests with local HTTP and targeted regression tests.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* chat/upstream-body: separate normalization from async payload preparation

Keep synchronous per-attempt normalization together so payload preparation
stays within the function size and complexity limits without changing its
ordering or explicit reasoning semantics. Condense redundant provider
selection comments to retain the formatted file within its size ceiling.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* changelog: record the suffix-effort propagation fix

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(quality): rebaseline file-size cap for chatHelpers.ts growth

The release tip independently grew src/sse/handlers/chatHelpers.ts from
1164 to 1213 lines while the frozen cap sat at 1214; this PR's own +3
lines (threading resolvedThinkingEffort through resolveModelOrError and
executeChatWithBreaker) push the merged result to 1217, past the cap.
Owner-approved exception for this file only, with the measured growth
breakdown recorded in the baseline entry.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:52:59 -03:00
Aaron Scherer
7d69b02a29 fix(db): skip integrity scans during health polling (#13149)
* fix(db): skip integrity scans during health polling

* test(db): update health error fixture for scan-free polling

* docs(changelog): add fragment for health poll integrity skip

* fix(db): keep the #13149 dashboard skip inside the #13717 managed health check

Merge fallout only: runManagedDbHealthCheck moved behind the health
coordinator on the release tip, so the per-call skipIntegrityCheck now
travels through it. A waived integrity scan is part of the job identity,
so it is never replayed from the 60s diagnosis cache to a caller that
asked for the full scan.

Co-authored-by: cryptiklemur <cryptiklemur@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: cryptiklemur <cryptiklemur@users.noreply.github.com>
2026-09-17 18:52:43 -03:00
Bob.Hou
5f9e153971 fix(mcp): fall back when better-sqlite3 export is not callable (#13903)
* mcp/audit: fall back when better-sqlite3 export is not callable

Dashboard MCP status polls reopen a failed native sqlite load every 30s
because a minified TypeError ("a is not a function") was not treated as
a native load failure and a failed open was not cached. Classify that
shape, fall back to node:sqlite, cache the miss, and refuse to ship a
Docker image without better_sqlite3.node.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* mcp/audit: force native better-sqlite3 compile in Docker

better-sqlite3 13 ships a linux prebuild. Bare `node-gyp rebuild`
then only TOUCHes stamp files and never writes
build/Release/better_sqlite3.node, so the new test -f gate fails the
image build. Pass --force_build=1, matching the package's own
build-release script.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* db/core: keep native-load classification under the file-size cap

The audit fallback added two TypeError fingerprints in core.ts and
crossed the frozen 1788-line cap. Move the classifier into
sqliteLoadError.ts and re-export it so existing importers stay stable.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* build/bootstrap: keep the encrypted-credentials probe narrow

The native-load classifier was copied into scripts/build/bootstrap-env.mjs
alongside the runtime one, but the two files consume its verdict in opposite
directions. In src/lib/db/sqliteLoadError.ts a true verdict means "the driver
is unusable, cascade to node:sqlite", so treating a non-callable export as a
load failure is what we want. In the bootstrap the verdict feeds
hasEncryptedCredentials, where true means "no encrypted credentials found" and
clears the way to generate a fresh STORAGE_ENCRYPTION_KEY.

With the TypeError patterns in the bootstrap copy, a binding that loads but
exports something non-callable over a database full of enc:v1: rows reads as an
empty database, and the operator silently loses access to every stored
credential. Drop those two patterns from the bootstrap copy only, and note in
both files why the pair is deliberately not identical.

A corrupt binding still fails loudly there, now with the database path, the
underlying message, and a rebuild hint, so the narrower classifier does not
cost any diagnosability.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* fix(mcp): keep audit logging recoverable when the database is created later

getDb() cached a null for the "storage.sqlite does not exist yet" branch, and
closeAuditDb() returns before clearing a falsy cache — so an MCP server started
before the app created the database stayed without audit logging for the whole
process lifetime. Only a genuine driver-load failure is cached now; the
not-found branch retries, which is how it recovers when the file appears.

Covered by a new test that fails without the change.

Also replace the fabricated minified TypeError text ("a is not a function")
thrown by the loader with "better-sqlite3 export is not a function": the
operator sees a diagnosable message and isNativeSqliteLoadError() still
classifies it (it matches on "is not a function").

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:47:25 -03:00
Bob.Hou
4be37d149b feat(dashboard): expose comboTimeoutMs next to Target timeout (#13857)
* feat(dashboard): expose comboTimeoutMs next to Target timeout

The runtime already applied config.comboTimeoutMs as the whole-combo
wall-clock budget (0 = 10-minute hang-stop). Schema treated it as an
unknown passthrough key and the dashboard only painted Target timeout,
so operators could not raise the 15-step failover ceiling from the UI.

Declare comboTimeoutMs on comboRuntimeConfigSchema, mount both knobs in
the combo editor Advanced panel and Combo defaults, and keep
comboTimeoutMs longer than targetTimeoutMs so failover still has time.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* docs(changelog): attach #13857 to comboTimeoutMs fragment

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* test(dashboard): name comboTimeoutMs store as milliseconds

The input is seconds; the stored config field is milliseconds. The old
title said "in seconds" while asserting 1_200_000.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* chore(i18n,changelog): translate #13857 keys into all locales and drop the CHANGELOG hunk

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:47:07 -03:00
Bob.Hou
ec780ef2dc feat(compression): make Lite tool-result truncation length configurable (#13915)
* feat(compression): make Lite tool-result truncation length configurable

Lite truncated tool results at a hardcoded 2000 characters. Coding-agent
payloads (file reads, crash dumps) lost the middle of the content with no
supported way to raise the cap.

Honor lite.maxToolLength from settings, then OMNIROUTE_LITE_MAX_TOOL_LENGTH,
then 2000. Existing installs keep the old length.

Related to #13178.

Signed-off-by: Minxi Hou <houminxi@gmail.com>
#13178 stays open.

* compression/lite: keep a stored cap when a step or toggle write is incomplete

An out-of-range step maxToolLength was still a number, so it hid a valid
global cap and fell through to env. A toggle-only settings PUT replaced
the whole lite row and dropped the stored cap. Save treated an out-of-range
number like a cleared field. Reject the bad Save, merge omitted caps, and
use null to clear.

Related to #13178.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* compression/lite: stop dashboard copy from hard-coding a 2000-char cap

The page overlays schema descriptions from i18n. Updating only
LITE_SCHEMA left operators seeing "over 2,000 characters" after the
cap became configurable. Also assert the Save error string, not the
Save button.

Related to #13178.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* chore(changelog): move #13915 entry to a changelog.d fragment

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:46:45 -03:00
Innokentiy Solntsev
e7872e57c3 fix(db): reclaim freed pages incrementally instead of a blocking VACUUM in the cleanup scheduler (#12821) (#12830)
* fix(db): reclaim freed pages incrementally instead of a blocking VACUUM in the cleanup scheduler (#12821)

startCleanupScheduler() ran a synchronous whole-database VACUUM on the event
loop whenever a cleanup pass deleted at least one row - 30 s after every
start and every 6 h. With node:sqlite that blocks every route (/healthz
included) for the duration: 7 min 55 s on a 540 MB storage.sqlite to reclaim
six rows. It also bypassed vacuumScheduler, the app-level owner of full
VACUUMs and the operator's scheduledVacuum / vacuumHour settings.

cleanup.ts no longer issues a full VACUUM. After each pass reclaimFreedPages()
branches on PRAGMA auto_vacuum:

- INCREMENTAL: drain the freelist with PRAGMA incremental_vacuum(N) in ~1 MiB
  batches (N from page_size), pausing between batches for as long as the last
  one took (<=250 ms), PASSIVE checkpoint every 64 batches and a TRUNCATE
  checkpoint at the end so the main file shrinks in WAL mode; hard caps of
  2048 batches / 30 s per pass, the remainder waits for the next pass.
- FULL: nothing to do, SQLite reclaims on commit.
- NONE: incremental_vacuum is a no-op, so record a request via the new
  vacuumScheduler.requestFullVacuum(); the rebuild runs in the configured
  window (or via the Storage page button). scheduledVacuum=never is honored.

vacuumScheduler persists fullVacuumRequestedAt / fullVacuumRequestReason,
clears them on the next successful runNow(), and hydrates from key_value
before an early request so it cannot clobber a persisted lastRunAt.

Loop robustness: db.exec() rather than pragma() (bun:sqlite's all() steps a
zero-column pragma once), SQLITE_BUSY/LOCKED and a handle closed under the
pass stop it quietly, other errors stop it with partial progress logged.
Also drops the duplicate cleanupProxyLogs() call in the scheduled pass -
runAutoCleanup() already covers proxy_logs.

Tests: new tests/unit/db/cleanup-reclaim-freed-pages.test.ts (INCREMENTAL
drain/pause/checkpoint, page_size-derived batch, caps, FULL no-op, NONE
defers and leaves page_count untouched, runScheduledCleanupPass() path);
vacuum-scheduler.test.ts covers requestFullVacuum persistence, restart
survival and clearing; cleanup-column-fix.test.mjs now asserts
incremental_vacuum and the absence of a full VACUUM statement.

* chore(changelog): name the #12821 fragment after its PR (#12830)

* fix(db): extract reclaimFreedPages into its own module and fix full-suite regressions

Split the #12821 incremental-vacuum reclamation logic out of cleanup.ts
into src/lib/db/reclaimFreedPages.ts (re-exported for callers/tests) so
cleanup.ts stays under the file-size cap after the #13011 reconciliation
merge grew it past the 1200-line threshold.

Also fixes two full-suite failures surfaced by running the
cleanup/vacuumScheduler/db-health suite post-merge (not just this PR's
own 3 test files, per the plan-file's mandatory item):

- tests/unit/cleanup-column-fix.test.mjs scanned cleanup.ts's raw source
  for the PRAGMA incremental_vacuum invariant, which now lives in the
  extracted module — updated to scan both files.
- tests/unit/db/cleanup-reclaim-freed-pages.test.ts asserted the freelist
  count is byte-for-byte unchanged when auto_vacuum=NONE. The tip's
  runAutoCleanup() now also runs cleanupCompressionRunTelemetry(), which
  lazily creates its table on first use (ensureCompressionRunTelemetryTable)
  — a legitimate one-time page cost from a freshly migrated DB, unrelated
  to reclaimFreedPages()'s own behavior. Loosened the assertion to a small
  tolerance while keeping the page_count assertion that actually guards
  against a full rebuild.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* chore(db): drop the reclaimable-bytes VACUUM gate test superseded by incremental reclaim

tests/unit/vacuum-reclaimable-threshold.test.ts pinned cleanup.ts's
vacuumAfterCleanup()/getReclaimableBytes()/getVacuumMinReclaimableBytes()
(#13079). This branch removes the inline post-cleanup full VACUUM entirely in
favour of reclaimFreedPages() (#12821), which reads the same freelist_count /
page_size signal and defers a full VACUUM to the vacuum scheduler when
auto_vacuum=NONE. With those three exports gone the file cannot compile, and
the behaviour it guarded no longer exists.

---------

Co-authored-by: insoln <is@careerum.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:38:26 -03:00
Innokentiy Solntsev
241e63bfea feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits (#12754)
* feat(usage): redeem GLM Coding Plan Reset Cards from Provider Limits

z.ai sells Reset Cards that clear an exhausted GLM coding-plan window (5-hour or
weekly) ahead of its natural rollover, but OmniRoute only ever read the passive
nextResetTime, so redeeming one meant leaving the dashboard.

Add the wire layer for z.ai's two reset endpoints
(/api/biz/customer-package-reset/list and /use), which authenticate with the same
Bearer API key as /api/monitor/usage/quota/limit and report failures inside an
HTTP-200 envelope, so callers must inspect success/code rather than the status line.

The banked count rides along with the quota poll - only for keys that actually
report a resettable window, and strictly best-effort so a card-less account or a
transient failure still renders its quotas. The existing reset-credit card, picker
and confirmation flow, until now gated to Codex, now also drive glm/glm-cn/glmt/zai
through the new /api/usage/glm-reset-card route, reusing z.ai's requestId as the
idempotency key so a retry cannot burn two cards.

* test(usage): cover GLM reset-card edge cases

* test(dashboard): require GLM reset-card copy

* fix(usage): harden GLM reset-card redemption

* fix(usage): treat missing GLM key as empty

* fix(usage): fence GLM reset-card operations and coalesce lease-window duplicates

- Acquire a synthetic 60s exclusive-connection lease around each list/use
  wire operation; release in finally so a competing lease can acquire
  immediately after success or failure.
- Coalesce same-key duplicates that arrive after lease acquisition by
  checking the in-flight attempt before loading the connection.
- Run the post-commit quota refresh outside the lease (redemption is
  already committed; the refresh is auxiliary and failure-tolerant).
- Do not discard a retained ambiguous attempt on a lease-conflict 409.
- Harden transport error mapping: static messages for proxy transport
  failures, keep explicit direct routing for unproxied connections
  through list, use, and refresh.

* fix(i18n): sync GLM reset-card keys to pt-BR and vi locales

---------

Co-authored-by: insoln <is@careerum.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:38:09 -03:00
Innokentiy Solntsev
4d9c4d3d8f fix(codex): fail the stream when the websocket closes before a terminal event (#12737)
* fix(codex): fail the stream when the websocket closes before a terminal event

* docs(changelog): add fragment for Codex websocket premature-close fix

* chore(quality): rebaseline file-size baselines for #12737 test and executor growth

* fix(codex): log websocket failures and harden premature-close tests

Review follow-up: failController now logs the failure (code + message)
via nextInput.log, and onclose surfaces the WS close code/reason in the
log line (the public payload stays sanitized through the allowlist).
Adds regression tests for the onerror-before-onclose sequence and a
close with zero prior events.

* chore(quality): re-measure the #12737 codex.ts file-size rebaseline after the release merge

The PR's own annotation was written against base 1505 and the frozen cap was
already at 1528 on release/v3.8.51 (which grew the file independently). With
this PR's +13 lines the merged file measures 1530, so the frozen entry moves
1528->1530 (+2 of genuine PR growth; the remaining 11 lines fit the headroom
the tip already had). No other frozen entry touched.

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:37:51 -03:00
Nguyen Thanh Dat
ac0a63117e fix(providers): read the vLLM context window from max_model_len (#12897)
* fix(providers): read the vLLM context window from max_model_len

normalizeDiscoveredModels resolved the window from inputTokenLimit,
context_length, contextLength and top_provider.context_length. vLLM
reports it as max_model_len and nothing else, so a synced vLLM model
carried no inputTokenLimit and the resolver fell back to the 128K
default - half the window on a 250K deployment.

The native vllm provider and the OpenAI/Anthropic-compatible custom
providers all pass raw records through this function, so one chain entry
covers the three connection shapes.

Closes #12858

* docs(changelog): fragment for #12897

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 18:37:34 -03:00
Paco Cartones
9688032451 test(dashboard): reactivate request logger coverage (#13843)
* test(dashboard): reactivate request logger coverage

* test(dashboard): assert the request-logger modal by its own label

The drift this PR unblocks is real — the next-intl mock lost its `useLocale`
export in #7935, so the suite died on `No "useLocale" export is defined`.
Restoring it is the actual fix.

The dialog assertions were also loosened to `[role="dialog"]`, and that part
was not needed: 20 components under src/ render that role, so the selector
stops proving this particular modal is the one on screen. Measured — keeping
only the `useLocale` fix and restoring a precise selector still passes 6/6.

Now asserting `[aria-label="ariaLabel"]`, which is what RequestLoggerDetail.tsx:469
renders (the mock returns the key rather than the translation).

---------

Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 17:52:34 -03:00
Paco Cartones
1ea87603c0 fix(qoder): unwrap split SSE error envelopes (#13838)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:17:33 -03:00
Paco Cartones
28ce4cacb2 fix(streaming): track progress across chunk boundaries (#13839)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:17:09 -03:00
Paco Cartones
1deb77a00d test(dashboard): reactivate discovery page coverage (#13842)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:16:47 -03:00
Paco Cartones
6f2eafd138 test(dashboard): reactivate API endpoints coverage (#13841)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:16:22 -03:00
Paco Cartones
4be690736c test(dashboard): reactivate webhook wizard coverage (#13844)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:15:58 -03:00
Paco Cartones
44d1760c3a fix(nlpcloud): restore chatbot endpoint coverage (#13845)
Co-authored-by: Paco Cartones <pacocartones@users.noreply.github.com>
2026-09-17 17:15:37 -03:00
Bob.Hou
c40a5f0432 fix(build): externalize @modelcontextprotocol/sdk to heal MCP initialize 500 on standalone builds (#13859)
* build/mcp: externalize @modelcontextprotocol/sdk in standalone server bundle

The SDK client graph contains a module-level class-extends-Client cycle
against the top-level-await Client module. Webpack's TLA runtime evaluates
that circular subgraph out of order when it is inlined into route chunks,
throwing "Cannot access 'l' before initialization" during module
evaluation. Every request to /api/mcp/stream then answers HTTP 500 on
initialize and the failed module is evicted and re-evaluated per request,
which floods the logs with the same ReferenceError. Node's native ESM
loader resolves the same circular graph through live bindings, so keep
the SDK external to the server bundle like the other packages that break
only when bundled.

Signed-off-by: Minxi Hou <houminxi@gmail.com>

* changelog: record @modelcontextprotocol/sdk externalize fix (#13859)

Signed-off-by: Minxi Hou <houminxi@gmail.com>

---------

Signed-off-by: Minxi Hou <houminxi@gmail.com>
2026-09-17 17:06:41 -03:00
Fouad Salkini
2387d051c1 fix(sse): strip Codex temperature on native Responses passthrough (#12585)
* fix(sse): strip Codex temperature on native Responses passthrough

Codex /responses rejects sampling params with FastAPI 400
Unsupported parameter: temperature. Native passthrough returned
before the Responses allowlist, so client temperature reached
upstream on combo traffic.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sse): extract Codex passthrough param strip under file-size cap

Keep temperature/top_p (and #3317 client-only fields) stripped before
native Codex /responses passthrough returns. Move the call to
open-sse/executors/codex/stripPassthroughRejectedParams.ts so
executors/codex.ts stays under its frozen 1505-line cap.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 16:27:19 -03:00
Fouad Salkini
fc6b240328 fix(oauth): classify an embedded invalid_grant in a refresh error body (#13466)
* fix(oauth): classify an embedded invalid_grant in a refresh error body

Cline answers a dead refresh_token with
400 {"data":"","error":"failed to refresh token: invalid_grant","success":false}
The code is the tail of a sentence — neither a bare code nor an
"error":"<code>" field pair — so extractOAuthErrorCode returned null.

A null classification means refreshClineToken emits no unrecoverable
sentinel, so a permanently consumed refresh_token is handled as a
TRANSIENT failure. tokenHealthCheck therefore never reaches its
unrecoverable branch, and never runs the credentialsChangedSinceSweep
race guard, the "please re-authenticate this account" message, or the
dead-token clear for rotating providers. The connection instead stays
active with errorCode "refresh_failed", retries the same consumed token
3x per sweep behind an exponential backoff, and 401s every request
routed to it indefinitely with no actionable operator signal.

Scan for a known unrecoverable code embedded in the error value as a
last resort, after the exact-match and nested-JSON paths, delimited on
both sides so server_error, xinvalid_grant, my_invalid_grant_flag and a
502 HTML page all still classify as null.

Also add cline to ROTATION_LOCK_GROUP: refreshClineToken reads a new
refreshToken out of every response body and a measured refresh rotated a
connection's stored token, so sibling connections must not refresh
concurrently. cline was already listed in tokenHealthCheck's
ROTATING_REFRESH_PROVIDERS but missing from the serializer.

* docs(changelog): add fragment for cline refresh token error classification

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 16:27:01 -03:00
Fouad Salkini
ec60915d91 fix(api): accept blockedModels in the key permissions schema (#13666)
* fix(api): accept blockedModels in the key permissions schema

`PATCH /api/keys/[id]` already destructures `blockedModels`, forwards it
into the update payload, and `updateApiKeyPermissions()` writes it to the
`blocked_models` column. Only the first link was missing:
`updateKeyPermissionsSchema` never declared the field, so Zod stripped it
from the parsed body and the destructured value was always `undefined`.
The request answered 200 and wrote nothing.

The API Manager permissions modal sends `blockedModels` on every save
(ApiManagerPageClient.tsx), so the Claude-Code family-blocking control
silently did nothing and an existing deny-list could not be cleared.
`blockedModels` is the deny-list half of the model policy — read by
`isModelAllowedForKey()` before the allow-list and winning over it — so
that half was only reachable by editing the database by hand.

Declare the field mirroring `allowedModels` (trimmed, non-empty, max
1000) and count it in the "No valid fields to update" guard so a body
carrying only `blockedModels` is a valid update.

Left out of `createKeySchema` deliberately: the create route does not
read `blockedModels`, so declaring it there would be dead weight.

* docs(changelog): add fragment for blockedModels key schema fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 16:26:42 -03:00
Innokentiy Solntsev
c97f61b2ac fix(sse): prevent Anthropic 400s for Claude-native handoffs (#12668)
* fix(sse): prevent Anthropic 400s for Claude-native handoffs

* docs(changelog): add fragment for Claude-native handoff 400 fix

* refactor(sse): satisfy file-size and complexity ratchets

Keep the Claude wire-body guard while staying under the frozen per-file line baselines and the complexity ratchets measured against release/v3.8.51.

Extract the final constraint coordinator, split system-message normalization into focused helpers, and isolate handoff response parsing. Reflow the universal-handoff explanation to absorb the added source-format argument without growing the frozen file.
2026-09-17 16:26:25 -03:00
Innokentiy Solntsev
dd70dbdaa0 fix(sse): Anthropic OAuth 403 "Request not allowed" is a per-request refusal — cooldown with backoff instead of an instant ban (#12859) (#12864)
* fix(sse): Anthropic OAuth 403 "Request not allowed" is a per-request refusal, not a ban

A single upstream 403 on the `claude` OAuth connection was classified
FORBIDDEN and written as the terminal `banned` connection state
(chatCore -> writeTerminalStatus). From then on every request to that
provider was short-circuited with "All 1 connection(s) banned by
upstream - please reconnect in the dashboard" without touching Anthropic,
until an operator reconnected.

Anthropic's OAuth surface answers a small fraction of otherwise-valid
requests with 403 {"type":"permission_error","message":"Request not
allowed"}. On the reporting install the same token returned 200 forty
seconds before the 403 and again right after the connection was
re-enabled; a revoked or expired token is a 401 authentication_error, not
this. It is a refusal of one request, not of the credential.

Classify it as the new non-terminal PROVIDER_ERROR_TYPES.REQUEST_REJECTED
(scoped to provider `claude` and the "Request not allowed" body) and list
that type in authTerminalStatus.isNonTerminalProviderError, mirroring the
Cloudflare FINGERPRINT_REJECTION precedent. The combo layer still falls
through to the next target for the failing request; the connection stays
active for the next one. Any other claude 403 keeps its previous
classification.

Tests: error-classifier.test.ts covers the Anthropic body, the
gateway-flattened "[403]: Request not allowed" message, the same body from
a non-Anthropic provider (still FORBIDDEN), other claude 403s (unchanged),
and the helpers; anthropic-request-not-allowed-not-a-ban.test.ts pins
resolveTerminalConnectionStatus() -> null for the new type even with a
`permanent` fallback verdict, and `banned` for a generic claude 403.

* fix(sse): cooldown with backoff and streak escalation for REQUEST_REJECTED (#12859)

Not "ignore the 403" either: if Anthropic ever made "Request not allowed"
systematic, re-sending every request into it would be the wrong thing to
do to an OAuth account. chatCore now handles REQUEST_REJECTED explicitly:

- exclude the connection via setConnectionRateLimitUntil for a growing
  cooldown (5 -> 15 -> 45 min) so a sporadic refusal costs minutes, not a
  reconnect, and a systematic one cannot become a stream of 403s;
- escalate to the terminal `banned` state only for 3 refusals within a
  60-minute window (services/requestRejectedStreak.ts, in-memory per
  connection; a restart forgets the streak, erring towards more cooldowns
  rather than an operator-undone ban), with a last_error that says so;
- probe-origin failures record but never cool down or ban (#9817).

The existing "request not allowed" text rule (5 s) is unaffected:
markAccountUnavailable skips a connection that already has a future
rateLimitedUntil, so the minute-scale cooldown written here wins.

Tests: request-rejected-streak.test.ts pins the window/threshold/backoff
arithmetic; anthropic-request-not-allowed-cooldown-escalation.test.ts drives
the real chat route against a mocked 403 upstream on a `claude` OAuth
connection: 300 s cooldown, then 900 s, then banned on the third refusal;
a different claude 403 body still bans on the first response.

* chore(changelog): name the #12859 fragment after its PR (#12864)

* refactor(sse): move the REQUEST_REJECTED branch into a chatCore leaf; register its tests for mutation coverage

chatCore.ts is frozen at 5984 lines by the file-size ratchet; the branch
body now lives in open-sse/handlers/chatCore/requestRejectedFailure.ts
(chatCore: 5974 -> 5983). stryker.conf.json tap.testFiles gains the two new
DB-backed tests so their mutant kills count (check:mutation-test-coverage).

* fix(sse): count refusal episodes, reset on success, keep the dashboard honest (#12859 review)

Review findings on the first cut of the REQUEST_REJECTED handling:

- A burst of in-flight requests that all got the 403 within seconds
  produced streak 1, 2, 3 and a ban from one upstream event. The streak
  now counts cooldown *episodes*: a refusal that lands while the
  connection is already excluded is the same event and is not counted.
- Nothing reset the streak on a healthy response, so sporadic refusals
  on a busy install could still accumulate to a ban. chatHelpers'
  onRequestSuccess now clears it (only a real success does - the recovery
  tick's clearAccountError is an elapsed cooldown, not a success).
  Clearing the cooldown by hand in the dashboard clears it too.
- The third rung of the ladder was unreachable (the third refusal
  escalates): the ladder is now 5 -> 15 min, sourced from COOLDOWN_MS next
  to the existing 5 s "request not allowed" rule, with a note on why that
  rule is superseded for claude. The 60-min window becomes a 24 h
  staleness bound - "consecutive" is defined by successes, not by time.
- Probe-origin refusals no longer touch the streak (#9817).
- The cooldown is written like every other connection-level cooldown:
  ISO rateLimitedUntil + testStatus "unavailable" (+ lastErrorAt), so the
  dashboard shows the countdown and the recovery tick restores "active".
- One refusal is re-seeded from the persisted row after a restart so a
  crash loop cannot reset the count on every boot.

Docs: RESILIENCE_GUIDE terminal states + CODEBASE_DOCUMENTATION resilience
row mention the streak module. Tests cover the burst, the success reset,
the seed, and the ISO/unavailable shape end-to-end through the chat route.

* chore(sse): drop unrelated Prettier churn in auth.ts / providers route

* style(api): keep providers route Prettier-clean

* refactor(sse): share the "exclude connection for a cooldown" leaf between GEO_BLOCKED, GCP_PROJECT_REQUIRED and the new branch

The release tip moved chatCore.ts to its frozen 5984 lines, so the
REQUEST_REJECTED branch cannot add a single net line. The GEO_BLOCKED and
GCP_PROJECT_REQUIRED branches were the same eight statements with different
constants and log wording; both now call
open-sse/handlers/chatCore/connectionCooldown.ts::excludeConnectionForCooldown
(behaviour, probe guard and log lines preserved verbatim). chatCore.ts ends
9 lines below the base it branched from.

* chore(chatCore): tighten the cooldown comments to keep the file under its size ceiling

After merging release/v3.8.51, chatCore.ts sat at 6150 lines against a
frozen ceiling of 6146. Condense the explanatory comments this PR added
to the GEO_BLOCKED and GCP_PROJECT_REQUIRED branches; no code change.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: insoln <is@careerum.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 16:26:08 -03:00
Innokentiy Solntsev
47159ed56b fix(combo): answer 503 + Retry-After, not 404, when a weighted pool is only cooling down (#12956)
* fix(combo): answer 503 + Retry-After, not 404, when a weighted pool is only cooling down

The weighted strategy filters targets before dispatch (open circuit breaker,
provider cooldown, model lockout, availability probe) and drops them
silently. When that emptied the pool the host returned the 404
"Combo has no executable targets" with the "switch combo / reconnect the
missing providers" recovery hint — for a pool that was configured,
connected and merely cooling down. Claude Code renders a 404 from
/v1/messages as "this model may not exist".

- targetResolution.ts: the eligibility predicate now reports which gate
  excluded a target and, for the resilience gates, the remaining time;
  the exclusions of fully-excluded steps travel out of
  resolveWeightedSelection. When the weighted pool ends empty and at least
  one exclusion is a resilience timer, the pipeline returns an early 503
  and logs the reasons at warn level.
- pinRecovery.ts: buildAllTargetsCoolingDownResponse() — 503
  `all_targets_cooling_down`, Retry-After = earliest exclusion to lapse,
  every excluded target in diagnostics.excluded, `wait` recovery hint with
  retry_after_seconds; formatPreDispatchExclusions() for the log line.
- error.ts: `all_targets_cooling_down` joins the public error identifiers.
- A pool emptied only by the availability probe keeps the 404.
- docs: RESILIENCE_GUIDE debugging entry; changelog fragment.

* chore(changelog): name the fragment after PR #12956 and link issue #12954

* refactor(combo): keep weighted exhaustion below complexity ratchet

---------

Co-authored-by: insoln <is@careerum.com>
2026-09-17 16:25:49 -03:00
Innokentiy Solntsev
bc7f68fb91 fix(resilience): lock the exact model, not the quota family, on 5xx model-lockout failures (#12957)
* fix(resilience): lock the exact model, not the quota family, on 5xx model-lockout failures

A 5xx model-lockout failure — a transport error (terminated, EHOSTUNREACH,
connect timeout), an upstream server error, or OmniRoute's own synthesized
502 from quality validation — is evidence about one model endpoint at that
moment, not about the account's quota family. recordModelLockoutFailure()
wrote it under the quota-family key regardless, so for codex (whose family
key is the whole `codex` scope, i.e. every gpt-5* model) one empty stream
on gpt-5.6-luna removed gpt-5.6-sol and gpt-5.6-terra from routing too,
for 2–30 min with exponential escalation, while the quota was untouched.

- exactModelLock.ts: resolveLockoutScope(status, explicit) — 429/403/402
  (and 404, already narrowed by getModelLockKey) keep the family key; any
  other status uses the exact provider/connection/model key. An explicit
  `scope` option still wins.
- recordModelLockoutFailure() resolves the scope once for key + lock fn.
- decayModelFailureCount() now walks every key shape (family, not_found,
  exact) so success-decay reaches exact-scope locks; null model stays a
  no-op.
- getAllModelLockouts() parses the `exact:` marker out of the key so the
  Model Cooldowns card lists the bare model and can clear it by that name.
- docs: RESILIENCE_GUIDE §3 key-scope-by-status; changelog fragment.

* chore(changelog): name the fragment after PR #12957 and link issue #12955

---------

Co-authored-by: insoln <is@careerum.com>
2026-09-17 16:25:32 -03:00
Nguyen Thanh Dat
21d756d7f0 fix(combos): accept isHidden in updateComboSchema (#12898)
* fix(combos): accept isHidden in updateComboSchema

A combo's visibility is stored on the record and honoured by the builder
option list and the dashboard grid, but updateComboSchema never listed
isHidden. The PUT handler spreads the validated body, so zod stripped the
field: a visibility-only update was rejected as "No valid fields to
update", and a mixed update succeeded while dropping the visibility
change.

Closes #12836

* docs(changelog): fragment for #12898
2026-09-17 16:25:16 -03:00
Lance Woodson
8074e3d596 fix(resilience): honor declared effort vocabulary in reasoning rule gate (#12686)
* fix(resilience): honor declared effort vocabulary in reasoning rule gate

The reasoning-routing rule capabilityFor() hardcoded a gpt-5.6-(sol|terra|luna)
whitelist for forced max/ultra, rejecting every other thinking-capable model
even when the model's resolved capabilities declare the requested tier (synced
supportedThinkingEfforts or an operator Model Overrides reasoning_efforts
override). This 400'd direct calls with "Reasoning effort 'max' is not
supported by the configured target" for models like Merge Gateway
zai/glm-5.3-flash, which natively accepts low|high|max.

The gate now treats a declared vocabulary containing the requested tier as
authoritative, mirroring the dispatch-time sanitizer
(open-sse/executors/base/reasoningEffort.ts) which already forwards declared
tiers verbatim. Undeclared models keep the legacy gpt-5.6 regex verdicts and
the unknown passthrough.

* fix(resilience): gate forced max against the static registry the sanitizer clamps with

Adversarial review finding: the gate read supportedThinkingEfforts from
getResolvedModelCapabilities, which prefers the DB override over the registry.
For a registered model with a narrow registry vocabulary and a widening
operator override, the gate passed forced max but the dispatch-time sanitizer
(executors/base/reasoningEffort.ts) clamps against the STATIC registry and
would silently downgrade max to the registry ceiling — converting a loud 400
into a silent wrong-effort request.

Order of precedence in the gate now:
1. static registry vocabulary (authoritative — matches sanitizer clamping)
2. declared/overridden vocabulary for unregistered providers (#8057 path)
3. legacy gpt-5.6 regex, then unknown/unsupported verdicts

Also pins the test fixture to a synthetic model id so a future models.dev
sync row cannot flip the unknown-precondition assertion.

* fix(resilience): gate registry lookup mirrors the dispatch sanitizer exactly

Review findings on the forced max/ultra gate:
- resolve the registry through getProviderModels (id->alias namespace) and
  match entry aliases, mirroring reasoningEffort.ts — a raw provider id or
  alias-spelled model no longer skips the registry branch and diverges from
  dispatch clamping
- treat an empty declared vocabulary as no declaration (falls through),
  matching the sanitizer's declaredRanked.length>0 guard — before, a model
  declaring [] was gated to unsupported while dispatch forwarded verbatim
- an operator-declared vocabulary that excludes the forced tier is terminal;
  the legacy gpt-5.6 regex can no longer resurrect a tier the override
  narrowed away
- rewrite the registry-outranks-override test: create the matching rule so
  the decision is non-null, assert unconditionally, pin gpt-5.6 narrowing,
  alias namespace parity, and use the deterministic xai/grok-4.6 fixture

* docs(changelog): clarify override scope for registry-declared models

* test: drop placeholder issue reference from test names

* chore(changelog): name fragment after PR #12686
2026-09-17 16:24:58 -03:00
Lance Woodson
821d02ba13 fix(providers): parse per-vendor-route reasoning.effort_values in discovery (#12730)
OpenAI-compatible model discovery does not recognize per-vendor-route
reasoning vocabularies declared under vendors.<vendor>.capabilities.reasoning
in GET /v1/models (Merge Gateway's documented catalog shape), so synced
models carry no supportedThinkingEfforts/defaultThinkingEffort and operator
effort data resets on every model sync; models whose upstream accepts a
native max tier cannot be used with forced-max reasoning rules.

Parse the shape into the existing supportedThinkingEfforts pipeline,
intersected across vendor routes: the same canonical model declares
different vocabularies per route and unpinned requests self-narrow to a
route honoring the requested level, so a synced tier must be honored on
every route the model can land on. Routes without effort_values declare
no effort control and are excluded; disjoint vocabularies produce an
authoritative empty list (no fall-through to generic tier shapes).

detectDefaultThinkingEffort falls back to the intersection's highest tier
ranked by the canonical effort order — only when the vendors shape is the
record's winning vocabulary source, never escaping a flat or nested
declared list.

Detection is shape-gated, not provider-gated; Zod-validated (Hard Rule #7)
with malformed vendor and tier entries dropped individually (discarding a
whole route would widen the intersection, fail-open). Precedence: flat
field > reasoning.supported_efforts / metadata (#7694) > vendor-route
intersection > capabilities.effort_tiers (#9160) / supported_reasoning_levels
/ thinking.levels (#8347).
2026-09-17 16:24:39 -03:00
Aaron Scherer
7e0c9f526a feat(sse): allow disabling conversation tracking (#13150)
* feat(sse): allow disabling conversation tracking

* docs: document OMNIROUTE_DISABLE_CONVERSATION_TRACKING
2026-09-17 16:24:21 -03:00
Aaron Scherer
30451e63af fix(sse): preserve Fable mid-conversation cache prefixes (#13173)
* fix(sse): preserve Fable mid-conversation cache prefixes

* docs(changelog): add fragment for fable cache prefix fix
2026-09-17 16:24:04 -03:00
Aaron Scherer
46730700f1 feat(usage): show separate Fable weekly limits (#13266)
* feat(usage): show separate Fable weekly limits

* docs(changelog): add fragment for fable weekly usage
2026-09-17 16:23:46 -03:00
Diego Rodrigues de Sa e Souza
2fa6ef0bdd security(runtime): harden TLS provenance, lifecycle, and public error boundaries (#11742)
* security(deps): pin and verify tls-client native artifacts

* docs(changelog): link tls-client provenance PR

* security(runtime): harden TLS and public error boundaries

* security(runtime): resolve CodeQL error-boundary findings

* security(lmarena): close public stream error boundary

* fix(lmarena): normalize public error statuses

* chore(quality): rebaseline chatCore.ts for the surviving log-boundary hardening

open-sse/handlers/chatCore.ts 6219 -> 6287. This is the one part of #11742 that
survived the rebase: sanitizeErrorMessage on the plugin onError hook, on the
semaphore-timeout path and on failureMessage before it reaches console.log and
the call log, sanitizeUpstreamDetails on the malformed-response log, and
getSafeErrorMetadata + try/catch where hostile (Proxy) metadata could throw.

That is the LOG boundary, which is broader than Hard Rule #12 (responses). The
rest of the PR was dropped as already landed on the tip.
2026-09-17 15:35:35 -03:00
Diego Rodrigues de Sa e Souza
57729db54d chore(quality): rebaseline the four ceilings the 2026-09-17 merge wave moved (#14002)
Measured on the clean tip (83fa4328), not on a branch. Each ceiling was
attributed to the PR that moved it before being raised — no blanket rebaseline:

- src/sse/handlers/chatHelpers.ts 1245 -> 1246 (#13551, combo scope on the
  fail-closed proxy guard)
- open-sse/handlers/chatCore.ts 6203 -> 6219 (#12905 DSML/preamble, #13910
  disguised-2xx classification, #12904 single post-translation system prompt)
- open-sse/utils/stream.ts 3123 -> 3140 (#12905, and #12906 empty_response 502
  retry + reasoning-aware timeout)
- tests/integration/chat-pipeline.test.ts 1736 -> 1740 (#13419, the two exact
  header assertions updated for charset=utf-8)

Other gates re-measured on the same tip and already green: typecheck:core,
check:open-sse-typecheck, check:docs-counts, and the #2331-adjacent
chatcore-translation-paths xhigh-effort case that was red before this wave.
2026-09-17 14:30:59 -03:00
Diego Rodrigues de Sa e Souza
83fa4328f3 feat(providers): add xKiro (#12648)
* test(catalog): pin the 2026-09-02 free-tier re-audit facts for gemini, ollama-cloud, groq, nara and mistral

* feat(providers): add xKiro (5M tokens/day free plan, 39 pinned free models)

* fix(catalog): re-audit gemini, ollama-cloud, groq, nara and mistral against official pages

* docs(providers): xKiro in the provider reference, counts and free-tier headline (~1.66B)

* fix(catalog): restore the console-verified Mistral 1B pool and harden its regression test

* docs(free-tiers): move headline to the re-audited ~1.50B and refresh pool counts

* chore(free-tiers): retire stale Groq free-tier text and preset model; fix catalog header

* docs(providers): align the remaining visible provider/executor counts with the catalog

* docs(providers): align remaining free-tier count chips and metadata

* docs(free-tiers): state the evidence-comment rule honestly and retire the last "14.4K RPD" Groq texts

* docs(free-tiers): retire the stale Gemini onboarding quota text

* docs(free-tier): refresh catalog-entry counts to 442 after base sync

* docs(providers): re-sync provider and free-tier counts after merging release/v3.8.51

* docs(providers): re-sync residual counts after the base merge

* docs(free-tiers): restore README spacing lost in the merge and re-sync the guide counts

* docs(free-tiers): re-sync numbers after merging release/v3.8.51 (Cerebras reclassified upstream)

* fix(docs): keep the NaraRouter plans endpoint out of the API-path checker; rebaseline gateways.ts (+3)

* chore(quality): rebaseline gateways.ts file-size cap for the xKiro entry (+20)

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-09-17 13:34:45 -03:00
Diego Rodrigues de Sa e Souza
b6975537c1 fix(providers): remove the chipotle/pepper provider (#13131) (#13913)
* fix(providers): remove the chipotle/pepper provider (#13131)

amelia.chipotle.com (the reverse-engineered Amelia chat-widget backend
chipotle/pepper-1 talked to) now returns 404 on every route, including
root, from its Azure Application Gateway — confirmed live 2026-09-15.
This regressed from a WS handshake timeout (#4037, June 2026) to a
fully decommissioned host, so the upstream protocol cannot be fixed.
Owner decided to retire the provider entirely (Option B), following
the phind/kluster quiet-removal precedent: no REMOVED_PROVIDERS.md
entry (reserved for operator takedowns), just a one-line note under
FREE_TIERS.md "Removed / no free tier".

Removed every surface: executor, registry entry, executors/index.ts
and providers/index.ts wiring, noauth provider catalog entry,
ProviderIcon generic-fallback set, the autoCombo exclusion-list
comment, the chipotle_error code from the sanitizer allowlist,
PROVIDER_REFERENCE.md (regenerated), and every doc/test reference.

Regression test: tests/unit/issue-13131-chipotle-provider-removed.test.ts
asserts the provider is fully gone from the executor registry, the
provider REGISTRY and the noauth catalog, and that the executor module
no longer resolves — not a live-network repro (flaky/third-party).

Several existing tests used "chipotle" only as a generic noAuth-provider
example (proxy scoping, error classification, onboarding, fallback
text) with no chipotle-specific behavior under test; those were
re-pointed at another still-existing noAuth provider
(cloudflare-playground / duckduckgo-web) rather than weakened.

* test(providers): document the agnes-cn/chipotle count coincidence (#13131)

provider-node-reserved-prefix.test.ts's REGISTRY id+alias walk was
already red on the base tip (414 vs. expected 412) from agnes-cn
(#13399, +id/+alias). Removing chipotle's REGISTRY id/alias in this
PR nets it back to 412, making the test pass again without a numeric
edit — record why in a comment so it doesn't read as an untracked
coincidence later.
2026-09-17 13:22:09 -03:00
Diego Rodrigues de Sa e Souza
872376bdc1 fix(chat): reject null/non-object entries in messages[] (#12643) (#13755)
* fix(chat): reject null/non-object entries in messages[] (#12643)

A messages array containing null (or any non-object entry, e.g. [null] or
[42]) passed every existing entry guard in chat.ts (#5110/#6402/#6407/#6412)
and reached downstream translators/session helpers that read `.role` /
`.content` directly off each entry (openai-to-claude.ts, sessionManager.ts,
contextManager.ts's fixToolPairs), crashing with a raw TypeError and
surfacing as an HTTP 500 instead of a clean 400. The route's Zod schema is
intentionally wide (z.array(z.unknown())), so this shape check belongs in
the handler's guard chain. Adds one more entry-shape guard clause to the
same chokepoint, rejecting the request with a clear 400 before any routing
or upstream call.

Regression test: tests/unit/chat-messages-entry-objects-12643.test.ts

PR #12644 (@soroush5) proposed this exact fix but was closed without
merging on 2026-09-12; this re-implements it fresh against the current tip
using the same guard shape and error message.

Originally-proposed-by: @soroush5 in #12644
Co-authored-by: soroush5 <mrsoroushahmadi@gmail.com>

* chore(quality): refix the chat.ts ceiling for the merged tree

This branch rebaselined src/sse/handlers/chat.ts against an older tip. After
merging the current release tip the combined file is 2520 lines, so the 2500
ceiling no longer covers it.

The tip alone is already at 2509 — above the 2500 this PR had frozen — so most
of the gap is inherited, not introduced here. This PR's own contribution is the
+10 of the messages-entry guard itself. Ceiling refixed at the value the gate
reports for the merged tree.

---------

Co-authored-by: soroush5 <mrsoroushahmadi@gmail.com>
2026-09-17 13:16:02 -03:00
Diego Rodrigues de Sa e Souza
d6f720bceb feat(i18n): new-key gate rejects __MISSING__ markers; skills translate new keys in parallel (#13996)
On 2026-09-16 eight feature PRs added 61 keys to src/i18n/messages/en.json and
stamped `__MISSING__:<en>` into all 65 locales instead of translating.
check-new-key-coverage accepted the marker as "the key reached the locale", so
nothing blocked the PRs, and the blocking real-translation ratio gate then failed
on the release tip for everybody (pt-BR 3.2 % > 2.5 % + 0.5).

- scripts/i18n/check-new-key-coverage.mjs: a leaf whose value starts with
  `__MISSING__:` is judged exactly like an absent leaf; the FAIL message names
  the marker as the cause and prints the per-locale sync-ui-keys command and the
  parallel runner. Header/JSDoc updated.
- tests/unit/i18n-new-key-coverage.test.ts: "a new key that only carries a
  __MISSING__ marker is flagged" (was the inverse case, which encoded the old
  contract); the other six cases unchanged and green.
- scripts/i18n/translate-new-keys.sh (+ `npm run i18n:translate-new-keys`):
  committed, detached-safe runner — flock queue, N workers (default 5), 3
  attempts per locale of `sync-ui-keys.mjs --translate-markers --batch-size=40`,
  per-locale logs/.exit + batch.log/batch.status/batch.rc/batch.pid under
  _artifacts/i18n-new-keys/, non-zero exit while any locale still carries a
  marker, refuses to start (exit 2, names the five OMNIROUTE_TRANSLATION_* vars)
  when the backend env is absent. Reads only the OMNIROUTE_TRANSLATION_* lines
  of the repo .env; kills nothing, matches nothing by name.
- docs: QUALITY_GATES.md (gate table + check-new-key-coverage section) and
  I18N.md (gate table + "Translating the keys a branch adds" subsection).

The implementation/port/merge skills reference the new shared snippet
`.agents/skills/_shared/i18n-translate-new-keys.md` (skills repo, separate).
2026-09-17 13:13:13 -03:00
Diego Rodrigues de Sa e Souza
ceafa55824 fix(providers): select and verify the requested gemini-web model/mode before answering (#13381) (#13919)
Root cause: GeminiWebExecutor.execute() opened the identical fixed
https://gemini.google.com/app URL and ran the identical Playwright
interaction sequence for every advertised gweb/<model> id. `model` was
read only AFTER the response was captured, purely to stamp the
OpenAI-shaped response — never to influence what was actually
clicked/typed, so two different advertised models produced
byte-identical automation and the response `model` field was a
caller-supplied label, not an observed fact.

Fix (owner decision, Option B): a new model -> Gemini UI mode map
(open-sse/executors/gemini-web/modeSelection.ts) drives an in-browser
selection step before anything is typed — try the mode control, read
back the active-mode indicator, and only proceed on a confirmed match.
An unconfirmed model, or a requested Extended Thinking control that
cannot be confirmed (#13381 follow-up comment), fails closed with 400
unsupported_control_for_provider instead of silently running the
account default under the requested label. The selectors involved are
UNVALIDATED (no live Gemini account from this checkout) — see the PR's
"Selector set is UNVALIDATED" section and the required live smoke.

Regression test: tests/unit/issue-13381-gemini-web-model-selection.test.ts
2026-09-17 13:06:37 -03:00
Diego Rodrigues de Sa e Souza
d8ad12f22d docs(agents): advance the documented Bun pin to 1.4.2 (#13946)
#13661 bumped the exact `bun` devDependency (and `@types/bun`) from 1.4.0
to 1.4.2, and #12977 moved the `oven/bun` image to 1.4.2-slim. AGENTS.md
still documented 1.4.0, so the guide disagreed with package.json for
every agent reading it as authority.

Version string only. The surrounding policy is deliberately unchanged:
Bun stays confined to the allow-listed gate/generator scripts plus the
`test:bun:db` smoke suite, and Node remains the only supported runtime.

Operator approved editing this protected surface for this change.
2026-09-17 13:06:19 -03:00
initguru
b7192b72e2 fix(thinking): parse/scrub DSML tool-call markers and recognize adaptive thinking (#12905)
* fix(thinking): recognize adaptive thinking + parse/scrub DSML tool-call markers

Two defects combined to break DeepSeek-V4-Flash turns and raise 502
empty_response on Claude Code autocompact.

Defect 1 — DSML tool-call markers leaked as visible content:
DeepSeek-V4-Flash occasionally emits tool calls in a non-standard DSML
text format using full-width pipes instead of the OpenAI tool_calls JSON.
Two shapes appear in production call logs:
  - complete block: <|DSML|:Read><path>...</path></|DSML|:Read>
  - stray closers (truncated call): </|DSML|parameter></|DSML|invoke>
    </|DSML|tool_calls>, sometimes trailing a system-prompt echo
The openai-compatible path never parsed these, so the markers leaked to
the client as visible content and the turn ended incomplete.

Fix: add open-sse/utils/dsmlToolCalls.ts — parseDsmlToolCalls() converts
complete DSML blocks into OpenAI tool_calls and strips stray closing
markers from content (streaming-safe via a holdback for partial openers).
Wire it into the response translator before extractXmlInvokeBlocks so
DSML and XML invoke tool calls share the same pending queue.

Defect 2 — adaptive thinking silently suppressed:
A prior inline === 'enabled' check on body.thinking.type silently
suppressed adaptive (the intent Claude Code actually sends), so
reasoning was dropped. The model then emitted DSML tool-call markers
as plain text, producing an incomplete stop finish. Fix: use
hasActiveClaudeThinking() (which recognizes enabled AND adaptive) to
set requestedThinking, thread it through stream.ts and translator
state, and gate thinking block emission on state.requestedThinking
so upstream reasoning_content only relays when the client opted in.

Tests: 29/29 (6 dsml-tool-calls, 5 thinking-active-claude-adapter,
3 translator-resp-dsml-integration, 15 translator-resp-openai-to-claude
incl. requestedThinking suppression regression). typecheck:core clean.

* fix(sse): strip echoed system-prompt preamble + preserve large analysis/summary blocks

DeepSeek-V4 and similar models echo the OMNIROUTE_SYSTEM_INSTRUCTION_APPEND
directive (appended to the system tail by claude-to-openai.ts) and whole chunks
of the system prompt (<analysis>/<system-reminder>/<summary> blocks, prose
reproductions of the superpowers skill section) verbatim at the START of their
reply — the 'system message leak' persisting after the request-side fix.

Add two streaming-safe preamble strippers in directivePreambleStripper.ts:
- createDirectivePreambleStripper(directive): drops a leading reproduction of
  the exact configured directive across arbitrary SSE chunk boundaries.
- createSystemPreambleStripper(): removes <analysis>/<system-reminder>/
  <summary> echo blocks and known prose heads (Phase B) from the very start
  of a stream, only while the stream is still a preamble.

Wire both into openai-to-claude.ts content-delta path: chain the exact-directive
stripper then the system-echo stripper before DSML/XML-invoke parsing, so a
leading system echo is dropped before it reaches the client.

Preserve large blocks (>= SYSTEM_ECHO_THRESHOLD=1000 chars) and blocks with no
trailing content — these are the model's real response (e.g. a Claude Code
autocompact summary), not a short system-echo. Stops the autocompact
empty-response regression where a whole-summary <analysis> block was stripped
to empty (3a8515).

Regression: origin's markdown-boundary feature (bufferedPrefix /
splitMarkdownBoundary, commit 1b39873ea) is preserved — preamble strip runs
before the markdown buffer rehydration, and the scrubbed content flows into
the existing DSML/XML-invoke/markdown pipeline unchanged.

TDD: tests/unit/directive-preamble-strip.test.ts (7 cases),
system-preamble-strip.test.ts (12 cases incl. 3a8515 regression),
system-preamble-wiring.test.ts (3 integration cases); group F regression
24/24 green; typecheck:core 0 errors.

* fix(sse): gate thinking block on requestedThinking + synthesize text block for reasoning-only responses

Reasoning-content (thinking) blocks were emitted unconditionally to
Claude-format clients, leaking reasoning to thinking-opt-out clients
(Claude Code sends thinking:{type:"disabled"}) — the operator reported
'reasoning is exposed'. On reasoning-only upstream responses (GLM-5.2
autocompact pattern), the unconditional thinking block also caused either
a 502 'no content block' at flush, or — after a text-block fallback — an
autocompact 'empty response' rejection that looped the session forever.

Streaming translator (openai-to-claude.ts):
- Compute hasReasoning outside the emission gate; accumulate into
  state._reasoningAccum always (so fix B can fire).
- Gate only the thinking-block EMISSION on requestedThinking === true.
- FIX B at finish: when no text block was started and requestedThinking
  !== true, synthesize a text content block from _reasoningAccum so
  autocompact can extract the summary (no 502, compact applies).
- Skip fix B when requestedThinking === true to avoid double-exposure
  (thinking block + text block both carrying reasoning).

Non-streaming translator (responseTranslator.ts):
- Thread requestedThinking through translateNonStreamingResponse into
  convertOpenAINonStreamingToClaude.
- suppressThinking = requestedThinking === false: drop the thinking block
  when content is present (no leak); relay reasoning as a text block when
  the response is reasoning-only (no 502). requestedThinking === undefined
  keeps the legacy 'always a thinking block' relay.

chatCore.ts: pass hasActiveClaudeThinking(body) to the non-stream
translate call (inline, since the shared const is in the stream branch's
temporal dead zone here).

Tests: 25/25 (5 gate-restore, 1 gate-502-repro, 4 nonstream-leak,
15 resp-openai-to-claude incl. requestedThinking suppression regression).
Group E (22) + F (14) regression-free. typecheck:core clean.

* fix(sse): restore requestToolIdentityMap in the Codex CLI responses-translation path

The needsResponsesTranslation branch (openai-responses -> openai, used
when the client also speaks Responses) silently dropped the
requestToolIdentityMap argument to createSSETransformStreamWithLogger
when the requestedThinking parameter was added, reverting the #7936
tool-identity round-trip fix for that branch. The sibling
needsTranslation branch was updated correctly; restore the same
argument here.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* docs(changelog): add the 3 fragments documented in the PR body

The PR body already writes out the changelog.d/ entries for the DSML
parser (Group F), the directive-preamble stripper (Group E), and the
reasoning-gate thinking-leak fix (Group G), but none of the files
existed in the diff. Add them so the release aggregator picks them up.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(sse): realign GLM's positional call after the new requestedThinking parameter

createSSETransformStreamWithLogger gained a new requestedThinking
parameter inserted before customToolNames. glm.ts's translateSseResponse
still called it with the pre-existing positional argument list, so the
new parameter silently absorbed the old customToolNames slot, and the
GLM_STREAM_BUFFER_BYTES tuning value (#12925) landed on
requestToolIdentityMap instead of streamBufferBytes — a TS2345 (number
is not assignable to Map<...> | null) caught by
check:open-sse-typecheck, and a real loss of GLM's 64KB stream buffer
budget. Insert an explicit `undefined` for requestedThinking to restore
the original alignment.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(sse): make the system-preamble stripper opt-in and flush it at stream end

`createSystemPreambleStripper()` was wired DEFAULT-ON and unconditional in the
openai→claude streaming translator, unlike the exact-directive stripper right
above it, which only runs when the operator configured
OMNIROUTE_SYSTEM_INSTRUCTION_APPEND. Cause: the system-echo stripper recognises
its openers by English-prose heuristics ("# Skill usage", "# Verification
Process", <analysis>/<summary>/<system-reminder>), so leaving it always-on made
it mutate the response payload of EVERY openai→claude stream. A legitimate reply
opening with "# Skill usage: how to write one\n\nHere is the guide." lost that
whole section. It is now gated on OMNIROUTE_STRIP_SYSTEM_PREAMBLE=1, mirroring
the directive stripper's opt-in.

Second cause, same feature: neither stripper was ever flushed. Both buffer while
a construct is still undecided — a directive prefix that never completes, an
<analysis> block that never closes — and nothing released that buffer at the end
of the stream. A reply consisting of an unterminated echo block therefore reached
the client as an EMPTY message: the answer was held in the buffer and discarded
with the stripper. Both strippers now expose flush(), the finish handler calls it
for both, and the released text is emitted as a text block. A construct that WAS
finally classified as an echo is not resurrected (the drop is final).

Tests: tests/unit/system-preamble-gate-and-flush.test.ts pins the default-off
contract, the opted-in behaviour, the flush for both strippers (unit + wiring),
and the no-resurrection guard. system-preamble-wiring.test.ts now opts in
explicitly, since it exercises the stripping path.

* fix(sse): thread the client's thinking intent into the non-streaming path

The streaming and non-streaming translators disagreed on the default meaning of
`requestedThinking`, so the SAME request produced different shapes depending on
`stream`. Cause: chatCore computes the client's intent
(hasActiveClaudeThinking) and threads it into the SSE translator, which relays
reasoning as a thinking block only when it is explicitly `true` — but NO caller
ever passed it to translateNonStreamingResponse(). The non-streaming
OpenAI→Claude conversion therefore only ever saw `undefined`, its legacy
"always relay a thinking block" default, and leaked reasoning to a client that
had opted out with `thinking: {"type":"disabled"}`. The streaming plumbing also
coerced an omitted value into an explicit `false`, hiding the divergence behind
two different spellings of "no intent".

Fix (least destructive of the options): do NOT flip either gate — both encode a
deliberate, regression-tested contract — but give the non-streaming path the
same input the streaming path already has. runNonStreamingProviderLeg owns the
client body (`sourceBody`), so it computes the intent with the very same helper
and passes it down through translateNonStreamingClientResponse. `undefined`
keeps its documented back-compat relay for callers that cannot express intent
(issue-7856 / issue-6623), and stream.ts no longer defaults the parameter to
`false`, so "absent" now means the same thing in both signatures.

No content is lost by the suppression: a reasoning-ONLY response is still
relayed as an ordinary text block (no empty response, no 502) — exactly what the
streaming finish handler does.

Tests: tests/unit/nonstream-requested-thinking-parity.test.ts drives the real
provider leg with thinking disabled / enabled / adaptive.

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 12:56:30 -03:00
Koosha Paridehpour
20f3900889 fix(claude-web): add charset=utf-8 to Content-Type headers to fix Arabic/Persian UTF-8 mojibake (#13416) (#13419)
* fix(claude-web): add charset=utf-8 to Content-Type headers to fix Arabic/Persian UTF-8 mojibake

Fixes #13416

The Claude Web endpoint and the outer SSE streaming pipeline were returning
Content-Type headers without an explicit charset parameter:

  - stream.ts responseHeaders() returned 'application/json' and
    'text/event-stream' without charset
  - responseHeaders.ts buildStreamingResponseHeaders() returned
    'text/event-stream' without charset

While RFC 8259 defaults JSON to UTF-8 and the SSE spec defaults
text/event-stream to UTF-8, some HTTP clients (notably VS Code Chat on
Windows) fall back to ISO-8859-1/Latin-1 when no charset is declared,
causing multi-byte UTF-8 characters to appear as mojibake.

For example, the Persian word for hello (سلام, UTF-8 bytes D8 B3 D9 84 D8 A7
D9 85) was decoded as Latin-1, producing the garbled output 'سلام'.

Fix:
  - claude-web/stream.ts: append '; charset=utf-8' to the Content-Type
    header in the responseHeaders() helper, with a guard to avoid double
    appending if the caller already includes a charset
  - chatCore/responseHeaders.ts: hardcode 'text/event-stream; charset=utf-8'
    in buildStreamingResponseHeaders()

Tests:
  - 11 new regression tests in claude-web-utf8-mojibake-13416.test.ts
    covering Persian, Arabic, mixed-script, emoji, and chunk-boundary-split
    scenarios across both streaming and buffered response paths
  - All 7 existing claude-web-stream tests pass
  - All 13 response header tests pass
  - All 8 adaptive-admission-lifecycle tests pass

* test(sse): confirm streaming charset header and add byte-level UTF-8 repro (#13416)

Independently verified the mojibake root cause before trusting the charset
fix: OmniRoute's claude-web decoder already reconstructs a Persian/Arabic
multi-byte UTF-8 sequence split across a chunk boundary correctly because
it decodes with TextDecoder({ stream: true }); a naive per-chunk decode
(without stream state) is what actually produces the U+FFFD garbling.
Updated the 2 exact Content-Type assertions in chat-pipeline.test.ts to
match the new "text/event-stream; charset=utf-8" header, which is already
the convention used by the other streaming executors (uc.ts, maxai.ts,
codex-app-server.ts, etc).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Koosha Pari <koosha@phenotype.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 12:44:25 -03:00
Koosha Paridehpour
f5ff7c1e1b fix(compression): skip RTK dedup and truncation for non-shell tool results (#13521)
* fix(db): union customModels with syncedAvailableModels in dispatch path

* fix(compression): skip RTK dedup and truncation for non-shell tool results

RTK's line deduplication and truncation were applied to all tool results
including non-shell tools (read, grep, glob, edit, write). This collapsed
structurally meaningful repeated lines in file content (e.g. JSON closing
braces, repeated key names), silently corrupting what the model received.

Now skipFilters (set for non-shell tools) and isDocumentLikeRead both
gate dedup and truncation, so file content survives byte-identical.

Fixes #13388

* fix(compression): restrict RTK truncation skip to document-like reads

The non-shell truncation-skip (options.skipFilters) disabled the generic
line/char cap for every non-shell tool result, including grep/glob/search
output that #4559 deliberately did NOT exempt. Only isDocumentLikeRead now
gates the generic truncation cap; the broader skip stays for dedup, which
is the operation that actually corrupts structured JSON content.

Also drops docs/omniroute-pr-body.md, an out-of-scope file carried by an
unrelated commit on this branch, and fixes the regression test's broken
relative import (tests/unit/compression -> open-sse is 3 levels up, not
2 — this is why the test file could not even load before this commit),
adjusts its truncation fixture to a genuinely document-like (non-JSON)
read so it actually exercises the isDocumentLikeRead exemption, and adds
a negative case asserting large non-shell grep output still gets
truncated by the generic cap.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(compression): make the RTK preservation test an actual regression guard

The four cases all passed against the tip WITHOUT this branch's fix, so they
guarded nothing — the next RTK refactor could reopen #13388 in silence.

Two causes, both fixed here:

- The fixture's repeated lines were not consecutive, and deduplicateRepeatedLines
  only collapses consecutive runs, so dedup never ran on it. Replaced with a
  matrix of identical rows, where collapsing them CORRUPTS the data rather than
  just reformatting it — which is the damage the fix prevents.
- Both central assertions sat inside `if (result.stats)`. With the old fixture
  the engine reported no stats, so the assertion bodies were skipped entirely
  and the test passed by doing nothing. They now run unconditionally.

Verified in both directions on the current tip:

  with this branch's fix   → tests 4 | pass 4 | fail 0
  against the tip's engine → tests 4 | pass 3 | fail 1
                             ✖ RTK should NOT dedup file content from a
                               non-shell 'read' tool

---------

Co-authored-by: Forge <forge@kooshapari.local>
Co-authored-by: KooshaPari <kooshapari@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 12:38:16 -03:00
Koosha Paridehpour
7a0b0c64fb fix(compression): track only the recursion path in isStrictlySerializable (#13154) (#13423)
* fix(compression): remove isStrictlySerializable gate that rejects valid bodies (#13154)

Fixes #13154

isCompressionWorkerEligible used isStrictlySerializable to pre-validate
bodies before posting them to a worker thread. The gate is stricter than
structuredClone (which postMessage uses natively), rejecting:
- undefined values (common in optional config fields)
- Date, Map, Set, Uint8Array, RegExp (all structuredClone-compatible)
- Shared (non-cyclic) sub-objects (misread as cycles)

This caused compression to fall back to inline execution on the main
event loop, blocking every concurrent request for the duration of
compression passes — the exact failure mode of #10300.

The serializability walk is a slower, buggier duplicate of the check
postMessage already performs. Removing it:
- Eliminates a recursive walk of the entire body on the main thread
- Fixes false rejections that prevent worker offload
- Allows the catch block at the call site to properly fall through
  to inline compression instead of silently shipping uncompressed

* fix(compression): track only the recursion path, not the whole tree, in isStrictlySerializable (#13154)

Restores the cycle-detection gate instead of removing it: the original
bug was a single `seen` set shared across the entire recursion tree,
never backtracked, so two sibling branches referencing the SAME
non-cyclic sub-object were misread as a cycle. Adding to `seen` before
descending and removing it after (try/finally) fixes the false positive
while a genuine cycle is still rejected before it ever reaches
postMessage/the worker.

Also reverts the worker-failure catch in runCompressionAsync back to
returning the body uncompressed: a worker timeout means the compression
was already too heavy for the worker's own budget, so falling through
to run that same heavy compression synchronously on the main event loop
defeats the point of offloading it to a worker in the first place.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Koosha Pari <koosha@phenotype.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 12:34:09 -03:00
Koosha Paridehpour
9c5d60027e fix(api): stream /api/logs/export with row cap to prevent V8 heap OOM (#13123) (#13428)
* fix(api): stream /api/logs/export with row cap to prevent V8 heap OOM (#13123)

Fixes #13123

GET /api/logs/export buffered every matching row into a single
JSON.stringify call with pretty-printing (null,2), roughly doubling the
string size. On tables with tens of thousands of rows this crashed the
Node process with a V8 heap OOM, taking the gateway down for minutes.

Changes:
- Stream the response via ReadableStream, serializing one row at a time
  so peak memory stays bounded regardless of table size.
- Add a configurable row cap (limit query param, default 10000, max
  50000) so callers cannot accidentally request unbounded exports.
- Remove pretty-printing (callers can pretty-print client-side).
- Include cap metadata (capped, limit, totalAvailable) when the cap
  fires so callers know they received a truncated result.
- Preserve backward-compatible response envelope: { count, hours, type,
  logs, ... }.

* fix(api): push the /api/logs/export row cap down into the DB layer (#13123)

The route-layer streaming + cap from the previous pass still called
exportCallLogsSince()/exportProxyLogsSince(), which hydrated and
buffered EVERY matching row (including rows beyond the limit) before
the cap was ever applied — peak V8 heap was essentially unchanged.

Adds countCallLogsSince()/countProxyLogsSince() (cheap COUNT(*), no row
hydration, used for totalAvailable) and iterateCallLogsSince()/
iterateProxyLogsSince() that bound the query with SQL LIMIT and
yield/hydrate one row at a time: a generator over a LIMIT-bounded id
list for call_logs, and fixed-size LIMIT/OFFSET pages for proxy_logs
(the shared SqliteAdapter only exposes run/get/all, not a `.iterate()`
cursor, so LIMIT/OFFSET pagination is the available cursor-equivalent
without widening that interface across all 4 driver adapters). The
route now streams from these instead, so the full matching row set is
never buffered.

Also moves capped/limit/totalAvailable into the response header instead
of only the trailer, so a client consuming the stream incrementally
learns about truncation before processing every row.

Rewrote the test to call the real route.GET handler against a seeded
test database instead of a local reimplementation of the stream
builder, so a regression in the route or its DB-layer delegates is
actually caught.

Documents the pre-existing (now more clearly load-bearing) breaking
change in a changelog fragment: `limit` defaults to 10,000 rows, so
exports that previously returned everything are silently truncated.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Koosha Pari <koosha@phenotype.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 12:32:43 -03:00
initguru
36df9e505a fix(codex): fail fast and release per-account Responses WS leases (#12911)
* fix(codex): fail fast and release per-account Responses WS leases

* chore(changelog): add fragment for Codex WS lease fail-fast fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* fix(codex): carry the reasoning-rule context through the leased WS path

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 10:56:01 -03:00
Diego Rodrigues de Sa e Souza
c3e966eeb9 docs(changelog): reconcile the v3.8.51 living section — round 2 (2026-09-15) (#13731)
Second `npm run release:reconcile` pass on `release/v3.8.50..release/v3.8.51`
(091589089c..c0f92ec98a, 916 non-merge commits, 877 merged PRs):

- fold the 173 changelog.d fragments accumulated since #12971 under
  `## [3.8.51]` and delete them
- generate bullets for the 58 cycle commits that had no fragment
  (4 features / 42 fixes / 12 maintenance), each with the merged PR link and
  `— thanks @author`
- link 137 fragment bullets to the PR of the commit that added them and
  credit the author; two prefix/origin mismatches reviewed (#12945→#13392,
  #13001→#13379, both maintainer rebaselines of other people's PRs)
- refresh "Release by the numbers" + Top-25 and regenerate the
  `### 🙌 Contributors` hall (112 external contributors + maintainer; every
  non-bot author of the 877 merged PRs present)
- closed-PR credit audit for the window: nothing to add (#13215→#13361 and
  #13059→#13690 are still open, #12998 was independently fixed earlier by
  #12853); no human co-author trailers, no commits without a PR
- resync the 58 i18n CHANGELOG mirrors

Gates: check:changelog-integrity OK, check:docs-sync PASS.
2026-09-17 10:47:11 -03:00
Diego Rodrigues de Sa e Souza
8587665669 fix(routing): fail over when Auggie's quota-exhausted text exits clean (#12949) (#13751)
Root cause: when a user's Augment/Auggie quota is exhausted, the local
`auggie` CLI prints its "You have run out of usage for ..." warning to
stdout and exits with code 0. AuggieExecutor treated any clean exit as
a successful completion and wrapped that text as a normal 200 assistant
reply, so combo/fallback routing never saw a failure and kept sending
requests to the same exhausted connection.

Fix: detect the CLI's known quota-exhausted phrasing before wrapping
stdout as a completion. Non-streaming returns a 429 in-band error body
(mirroring blackbox-web.ts's precedent for HTTP-200 in-band errors);
streaming buffers the first ~2KB of stdout, and on a match emits the
existing {error:...} SSE envelope (reusing the #7880 combo quality-gate
detection) instead of forwarding the text as a delta.

Regression test: tests/unit/issue-12949-auggie-quota-exhausted-200.test.ts
2026-09-17 10:46:52 -03:00
Diego Rodrigues de Sa e Souza
d92046064e fix(api): restore MCP namespace identity on follow-up Responses turns (#12996) (#13769)
Root cause: resolveRequestToolIdentity (the #7936/#9780 seam) only ever
resolved a streamed/non-streamed Responses function_call item's
{namespace, name} from the CURRENT request's own requestToolIdentityMap,
built solely from that request's own tools:[{type:"namespace",...}]
declarations. OmniRoute is a stateless-upstream-by-default proxy for the
Responses API, so nothing persisted namespace identity across separate
top-level HTTP requests in the same Codex/MCP session — a follow-up turn
that relies on previous_response_id/session continuity instead of
re-declaring its namespace tools silently lost the namespace field.

Fix: a deterministic wire-name fallback that splits a flattened
mcp__-namespaced wire name on its last "__" separator (mirroring
flattenNamespaceToolName's own construction), scoped to the mcp__
container-name convention so it cannot misfire on an unrelated flat tool
name that happens to contain "__". Applied symmetrically to the
non-streaming path.

Regression test: tests/unit/issue-12996-responses-streaming-namespace-followup.test.ts
2026-09-17 10:46:34 -03:00
Diego Rodrigues de Sa e Souza
d032431a5f test(catalog): stop the yield guard from dying on production's build budget (#13906)
tests/unit/9147-catalog-eventloop-yield.test.ts exists to prove the /v1/models
builder yields to the event loop while assembling a catalog-scale dataset. It
asserts res.status === 200 first, and only then the two checks that carry the
invariant: the max event-loop gap and the traversal to the last seeded model.

The case did not set CATALOG_BUILD_TIMEOUT_MS, so it inherited production's 8s
cold-path budget. When the seeded build overruns that on a loaded runner,
getUnifiedModelsResponse answers 503 catalog_build_timeout and the status check
fails BEFORE either real assertion runs — the guard goes silently dead exactly
when the machine is under the load that would make a pin most visible. CI hit
it at 8350ms, right at the bound.

Measured on the release tip, pristine file, 3 runs at load ~21: 2 pass with max
gaps of 247ms and 180ms, 1 fails with 503 !== 200 — roughly a 1-in-3 flake, and
the flake has nothing to do with yielding.

Pinning a 120s budget lets the invariant be evaluated. The 800ms gap bound is
untouched, and with the budget pinned the builder measures 261ms (idle) to
790ms (loaded) against it — still failing a true pin, which is seconds.
Build-latency budgeting is a separate concern from this case.

Refs #12732
2026-09-17 10:46:17 -03:00
Diego Rodrigues de Sa e Souza
209112df36 fix(cli): persist supervisor give-up crash diagnostics to disk (#13538) (#13908)
Root cause: ServerSupervisor.handleExit()'s give-up branch only printed the
crash summary/log via console.error(). In --tray/--tray-worker mode this
process is launched detached with stdio:"ignore" on Windows and Linux
(bin/cli/tray/detachedTray.mjs buildTrayLaunch()), so that output is
discarded by the OS and nothing ever explained why the tray + gateway
disappeared together.

Fix (Part A only, see plan-file for Part B — a restart-on-failure policy,
out of scope here): best-effort append the crash summary + buffered log to
<DATA_DIR>/server/crash.log, wrapped in try/catch so the write can never
block shutdown. Surface the same file from `omniroute doctor`.

Regression test: tests/unit/issue-13538-tray-crash-diagnostics-lost.test.ts
2026-09-17 10:46:00 -03:00
Diego Rodrigues de Sa e Souza
21772f40f3 fix(security): generate a random per-install CLI token salt (#13679) (#13909)
Both src/lib/machineToken.ts::getActiveSalt() and its mirror in
bin/cli/utils/cliToken.mjs derived the CLI/management bearer token as
HMAC-SHA256(raw machine-id, salt) with a checked-in literal default salt
("omniroute-cli-auth-v1"). Since /etc/machine-id is commonly world-readable,
any local user who never set OMNIROUTE_CLI_SALT could derive the same
bearer token as the server.

getActiveSalt() now generates a random 64-char-hex salt on first use and
persists it under <DATA_DIR>/cli-token-salt.json (falling back to the
literal only when neither the env override nor a persisted/writable salt
can be established). Both implementations use the same resolution order
and the same wx-flag create-race handling so the CLI and server keep
deriving the same token. OMNIROUTE_CLI_SALT stays the explicit operator
override, unchanged.

Regression test: tests/unit/machine-token-random-salt-13679.test.ts
2026-09-17 10:45:43 -03:00
Diego Rodrigues de Sa e Souza
e7b82783f2 fix(sse): classify a 2xx body as a disguised upstream failure (#13461) (#13910)
Pollinations and Perplexity-web can answer a genuine failure (expired
session, exhausted free-tier credits) with HTTP 200 and a structurally
normal completion whose assistant text is just the provider's own
error prose. classifyProviderError() only inspects the body for
400/401/402/403/429, and detectMalformedNonStream() only checked
structural emptiness, so the error text reached the client as a real
answer and combo/auto-fallback never triggered.

Adds classifyFakeSuccessBody() in errorClassifier.ts — allowlisted to
pollinations/perplexity-web, reusing the existing
CREDITS_EXHAUSTED_SIGNALS/ACCOUNT_DEACTIVATED_SIGNALS phrase lists,
gated on short content with a dominant signal match — and wires it
into detectMalformedNonStream() so the existing malformed-200 /
combo-failover path picks it up with no other handler changes.

Regression test: tests/unit/diagnostics-fake-success-13461.test.ts
2026-09-17 10:45:25 -03:00
Diego Rodrigues de Sa e Souza
de369fcc59 fix(security): container/Fly REQUIRE_API_KEY posture + free-tier usage leak (#13679) (#13911)
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-17 10:45:07 -03:00
initguru
3e080877f2 fix(sse): bound active streams without terminal events (#12913)
* fix(sse): bound active streams without terminal events

* fix(sse): derive the active-stream ceiling from the largest registered model budget

The watchdog is a hard lifetime cap that never resets on bytes, so a flat
15-minute default killed models the registry already allows to run for 20
minutes (the Codex entries declare timeoutMs: 1_200_000). The default is now
that maximum plus a one-minute margin, and a new test re-derives the maximum
from the registry so a future larger budget fails the gate instead of silently
re-opening the bug.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 10:44:48 -03:00
initguru
4c4d5c7fbe fix(resilience): allow maxWaitMs=0 as disable sentinel for execution expiration (#12902)
* fix(resilience): allow maxWaitMs=0 as disable sentinel for execution expiration

maxWaitMs normalization clamped the value to min:1, silently rewriting
an operator's 0 ("disable the limiter-managed execution deadline") into
1 — a 1ms expiration that killed every long-running job instantly. This
broke long-running reasoning models (GLM-5.2 with reasoning.effort=max
spends minutes before the first token, exceeding any practical
maxWaitMs; the TTB safety net is FETCH_TIMEOUT_MS, default 600s).

Fix: lower the floor to min:0 so 0 is preserved as the disable sentinel.
Issue #4165 follow-up.

Tests: 7/7 (resilience-normalize-maxwaitms-disable 5 + rate-limit-
maxwaitms-disable-execution 2). typecheck:core clean.

* fix(resilience): relax requestQueueSettingsSchema.maxWaitMs to allow 0

normalizeRequestQueueSettings already treats maxWaitMs=0 as an explicit
disable sentinel (queue-wait budget off), but the settings API schema
still rejected 0 with min(1), so an operator could never actually reach
the fix through PATCH /api/resilience. executionMaxWaitMs is untouched
(stays min(1) — separate field, separate decision, see #12902 item 4).

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* test(resilience): prove maxWaitMs=0 vs #12715's queue-wait gate behavior

Answers the open technical question from #12902's review: does a
GLOBAL maxWaitMs=0 reintroduce the unbounded-queue regression #12715
fixed (a request hanging ~6min until the client aborts)?

Evidence, exercising the real gate chatCore.ts actually calls
(accountSemaphore.acquireMany({ timeoutMs: requestQueue.maxWaitMs }),
not the Bottleneck reservoir the PR's own tests cover) under real
contention (maxConcurrency=1, two concurrent acquires):

  - No: it does not hang. setTimeout(reject, 0) fires on the next
    tick, so a second contending request is rejected with
    SEMAPHORE_TIMEOUT in low milliseconds, never minutes.
  - But it is also not a genuine 'no cap' — an operator setting 0
    expecting 'wait as long as it takes' instead gets near-zero
    tolerance for even momentary contention on any configured
    concurrency gate (global/provider/account). This is a real
    asymmetry vs. the Bottleneck reservoir path (where 0 truly means
    unbounded) left for the maintainer to decide how to resolve —
    not something this pass can decide unilaterally.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 10:44:30 -03:00
Koosha Paridehpour
28557418db fix(proxy): add combo scope to fail-closed proxy guard (#13551)
* fix(proxy): add combo scope to fail-closed proxy guard (fixes #13469)

The hasBlockingProxyAssignment guard only checked account, provider, and
global scopes. Combo-scoped proxy assignments were not checked, so a fully
dead combo pool fell through to direct egress — leaking the host IP.

- Add combo scope to the SQL guard query
- Add optional comboName parameter to hasBlockingProxyAssignment
- A dead combo pool now blocks egress like the other three scopes

* fix(proxy): thread comboName through safeResolveProxy to the combo-scope guard (#13469)

hasBlockingProxyAssignment() gained a comboName parameter and a combo-scope
SQL clause, but its only caller, safeResolveProxy() in chatHelpers.ts, never
passed it — the clause always bound NULL and never matched a real combo
scope_id, so a fully dead combo-scoped proxy pool still fell through to
direct egress. Thread comboName from handleSingleModelChat (where it is
already in scope) through safeResolveProxy into the guard, and add tests
covering both the guard predicate and the end-to-end wiring.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Koosha Pari <koosha@phenotype.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 10:44:13 -03:00
Koosha Paridehpour
2dae6df518 feat(sse): retry transient 5xx backend errors with jitter (global-fallback call site) (#12695) (#13143)
* feat(sse): retry transient 5xx backend errors with jitter (global-fallback call site) (#12695)

Per Diego's review feedback on the original PR (which was closed for base drift + over-broad scope):

- Helper in tests/unit/ (not open-sse/__tests__) - 7 node:test cases
- Drop 429 from retryable set (keep 502/503/504) - 429 must respect the
  RateLimit-Reset header and be surfaced, not silently retried
- Touch ONLY the global-fallback call site in chat.ts - the
  combo-target loop has its own retry semantics already

The wrap uses decorrelated full-jitter exponential backoff (per AWS
retry guidance), abort-aware (client disconnect cancels immediately),
and respects the AbortSignal via a sleep that throws on abort.

Re-cherry-pick: dropped all unrelated drift from the original PR
(videoBridgeLog plumbing, forcedConnectionId refactor,
comboCheckProvider/ghComboGate, getPassthroughProviders, Moonshot
quota fetcher, withSelectedConnectionHeader, classifyProviderBreakerResult,
reanchorVideoBridgeRedaction, etc.). chat.ts is now +20/-16 — purely the
import + the wrap around handleSingleModelChat in the global-fallback
block. Diego should now see a clean, focused diff.

Fixes #12695

* fix(sse): address kilo-code-bot review on transientBackendRetry helper

Per the review comment on the previous commit:
- Jitter formula now matches documented AWS decorrelated jitter
  (temp = min(cap, random(base, prev*3))) instead of an ad-hoc
  baseMs + rand*prev*2 approximation
- AbortSignal is honoured by the default sleep implementation (was
  previously only honoured by the loop's pre-attempt check)
- 'source' option added to TransientRetryOptions so callers can
  propagate an observability label (e.g. 'global-fallback') through
  onRetry

Adds 3 tests:
- default sleep respects AbortSignal without custom sleep
- onRetry receives source label
- decorrelated jitter is bounded by capMs

10/10 unit tests pass.

---------

Co-authored-by: KooshaPari <kooshapari@users.noreply.github.com>
2026-09-17 10:43:54 -03:00
Koosha Paridehpour
5455740faa fix(compression): log warnings for unreadable settings rows (#13522)
* fix(compression): log warnings for unreadable settings rows

getCompressionSettings() silently skipped non-string (BLOB) and
invalid-JSON settings rows, making it impossible to diagnose config
drift between the panel and the runtime.

Now logs a warn-level message for each unreadable row, including the
key name and a remediation hint (re-save from the Storage panel).

Also warns when the 'engines' row exists but yields no valid toggles,
so operators know their panel-configured engines map is being silently
replaced by the legacy fallback.

Fixes #13456

* test(compression): cover getCompressionSettings warnings for unreadable rows

The test for #13456 only asserted a stubbed console.warn recorded a
message and never called getCompressionSettings(), so it never
exercised the production change. Seed a BLOB row, an invalid-JSON row,
and an 'engines' row that isn't a usable object, and assert the
resulting warnings; also assert a legitimately empty (but valid)
'engines' map does not warn.

Also stop warning on a valid-but-empty 'engines' row: parseStoredEnginesMap
returns null both for an unreadable row and for a well-formed {} (an
operator who deliberately disabled every engine), so only warn when the
stored value isn't a usable object at all.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Koosha Pari <koosha@phenotype.ai>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 10:43:37 -03:00
Diego Rodrigues de Sa e Souza
cdcf1d2589 fix(i18n): translate the 3,719 __MISSING__ markers eight base PRs added on 2026-09-16 (#13974)
61 keys × 61 locales stamped as __MISSING__ by eight PRs on 2026-09-16 translated with sync-ui-keys --translate-markers; ratio gate green again on the release tip. ⚠️ base-red inherited: #12732
2026-09-17 09:09:02 -03:00
Diego Rodrigues de Sa e Souza
0d089e7e39 fix(quality): clear the release/v3.8.51 base-reds (#13947)
* fix(quality): clear the release/v3.8.51 base-reds

19 failing unit tests plus the API Route Typecheck and mutation-test-coverage
gates, all reproduced on the clean tip before touching anything.

Ten of the failures share one cause. #13452/#13798 made `*-compatible-*`
buildUrl() refuse a connection with no baseUrl instead of quietly defaulting to
the real OpenAI/Anthropic API — which would ship the operator's stored key to a
public third party. The guard is right; three fixtures still built those
connections unhydrated, and one of them put baseUrl at the top level of
credentials, where the chat path never reads it.

The rest:

- modelDiscovery.ts missed the VertexModelMetadataProvenance cast that its
  read-path twin in db/models/synced.ts already had — both written by #12471.
- A provider-test regexp carried raw 0x00/0x1f bytes, which makes git, GitHub
  and ripgrep treat the file as binary. Same character class, written
  with escapes instead of the bytes themselves.
- #13399 (Agnes AI China) adds "agnes-cn" + "agnescn": the only two provider
  prefixes since the count was last set (412 -> 414). Everything else added in
  that range is model ids.
- The free-tier budget card SVG was stale (443 -> 452 models); regenerated by
  its own script.
- Three new tests were missing from stryker.conf.json tap.testFiles, so the
  mutants they kill did not count.

Three guards asserted syntax rather than the invariant they protect, and broke
when the source legitimately changed. Each was re-expressed and then verified by
mutating the source back:

- #2331 required modelEffort to head the rawEffort chain; #13556 deliberately
  put the server-selected force rule first. The real invariant is relative —
  modelEffort outranks the defaults a client injects — and it still trips when
  explicitReasoning is moved ahead of it.
- The OAuth loopback guard matched the isLocalhost arm literally; #9944 added
  `&& !opts?.manualLoopback`. It now matches the arm whatever guards it, and
  still fails when the hint stops being built.
- The i18n scanner flagged dynamically-built keys — t("effort." + mode) reaches
  it as a literal prefix, never a string. It now accepts a prefix that resolves
  to a namespace holding messages, and still fails when the namespace is gone.

tests/unit/sse-auth.test.ts (#12080) expected a bare null where #13879 now
returns the key-policy diagnostic — the same sentinel shape the terminal-state
path has used since #12441. The assertion was rewritten to the constraint #12080
actually protects: nothing usable comes back and neither connection leaks. The
contract risk that remains — those sentinels are truthy, and executeWebSearch
treats any truthy value as a credential — is filed as #13945 rather than
widened into this PR.

Refs #13866

* fix(quality): clear the second wave of release/v3.8.51 base-reds

The tip moved 13 commits while the first pass was running and brought its own
reds. All reproduced locally on the merged tree first.

vitest 4.1.11 -> 5.0.0 in the #13661 development-group bump is a major, and
vitest 5 moved `vite` from a dependency to a peerDependency. This repo only ever
declared `vite` under `overrides`, which pins a version but installs nothing, so
`npm ci` stopped providing it and the Vitest job died at startup with
ERR_MODULE_NOT_FOUND. Declared as the devDependency it actually is — the same
^8.0.16 the override already pinned, and what @vitejs/plugin-react asks for as a
peer — and regenerated the lockfile: 684 lines added, none changed.

#12909 filtered a mapped array with `toolCall is JsonRecord`, but the element
type is the tool-call literal or null, and a predicate's type has to be
assignable to the parameter's (TS2677). Narrowed by the element's own type
instead; the literal still satisfies JsonRecord at the return.

#12906 added `|| result.errorCode === "empty_response"` to the stream-failure
condition and Prettier rewrapped it, so the #8928 probe — which located the
branch by an exact four-line string — stopped finding it. It now matches on what
the branch tests rather than how it is typeset, and still fails when the
eviction call is removed.

probe-7293 is the visible half of a real conflict, filed as #13948. #7293 merges
a mid-array system into index 0; #12908, landed later, demotes it to "user" in
place instead. Both target the same constraint and only one can win, and the
combination also reorders: the pre-translation hoist moves the turn forward
expecting it to stay a system message, then the demotion converts it where it
now sits, ahead of the conversation. Choosing between the two strategies is a
product call, not a base-red one, so the test was realigned to assert the half
that protects the caller — the instruction survives, as a user turn — and pins
the current ordering with a pointer to the issue, so the eventual decision shows
up as a deliberate test change instead of a silent regression.

Refs #13866, #13948

* fix(quality): allowlist vite, rebaseline tip growth, drop a dead import

Third pass on the release/v3.8.51 base-reds. Declaring `vite` in the previous
commit was correct but incomplete: check-deps is a human review point against
typosquatting, so a newly declared package has to be vouched for by name.
Recorded in dependency-allowlist.json with why it is needed — the official Vite
build tool, already pinned through overrides, and a required peer of both
vitest 5 and @vitejs/plugin-react. That also turns check-deps.test.ts green.

check-file-size went red on nine files. One is mine: sse-auth.test.ts grew when
the #12080 assertion was rewritten. Three of the four assertions I had added
were redundant with the strict deepEqual that follows them, so they are gone and
the file grows by 4 lines instead of 8; the cap absorbs the rest.

The other eight are production and test files this PR does not touch, grown by
other work and never rebaselined — which is the whole reason a base-red drain
exists. Each is attributed to the commit that grew it: #12906 (chat.ts,
chatHelpers.ts, proxyFetch.ts, stream.ts), #12904 + #12910 (chatCore.ts), and
batch_api.test.ts from the same wave. Two of them predate the wave entirely and
were already over cap on 3d5baf13 — imageGeneration.ts (#13748) and
roundRobinCombo.ts (#13776) — so they were base-reds hiding behind a gate that
only surfaced them once the tip was merged in. Both are recorded separately from
the wave so the history stays honest about when each cap actually moved.

Note for whoever reads the gate next: it counts one line more than `wc -l`,
since it measures split length rather than newlines.

Finally, #13290 replaced rmSync with cleanupTempDataDir in
zcode-executor.test.ts but left the import behind, which the frozen-warning
ESLint gate rejects. Removed.

Refs #13866
2026-09-17 05:48:40 -03:00
Diego Rodrigues de Sa e Souza
b637350680 fix(docs): re-sync the 65 documentation mirror sets; section-level docs pipeline; drift gate blocking (#13940)
1,104 mirrors rewritten over five passes of run-translation on the 22-source core set: the 14 sources edited since their translation, the 322 mirrors that were still English copies, and the frontmatter the old extractor leaked into the newer locales' bodies. The pipeline now caches per-`## `-section hashes and retranslates only changed sections, never reuses a section that is still English, rebuilds English-copy / leaked mirrors even when the source is unchanged, merges the state on save (parallel runs), and the drift gate (scoped to the core set) is blocking. Final audit: 0 stale, 0 English copies, 0 leaked frontmatter across 1,430 core mirrors.

⚠️ base-red inherited: #12732
2026-09-17 02:55:31 -03:00
anhtahaylove
cb9740b78d fix(adobe-firefly): do not spawn Chrome for CDP warm under test runners (#13289)
The CDP session-warm path gated only on ADOBE_FIREFLY_BROWSER_REFRESH, so a unit
test exercising the image-edit route spawned a real headed Chrome. The browser
holds an OS handle on its profile directory under DATA_DIR, so teardown that
rmSync()s the temp DATA_DIR failed with EPERM on Windows, and the CDP socket kept
the runner alive for the full 75s warm timeout.

Gate the browser path on the same test-runner detection diskSessionsEnabled()
already uses. 8510-adobe-firefly-edits-route: 1 pass/4 fail in 63s -> 4 pass/0
fail in 2.5s.
2026-09-17 02:32:10 -03:00
anhtahaylove
fde6241d41 test: close the database before removing temp DATA_DIR (#13290) (#13292)
* test: close the database before removing temp DATA_DIR (#13290)

Tests that set their own DATA_DIR and removed it in test.after() failed on
Windows with EPERM: nothing closed the SQLite connection, so the directory
still had an open handle and the -shm/-wal sidecars kept it locked. maxRetries
could not help because every retry hit the same open handle.

Adds tests/_setup/tempDataDir.ts with cleanupTempDataDir()/createTempDataDir(),
which close the DB singleton (lazily imported, so tests that never touch the
database do not pull in the DB layer) and then remove the directory
best-effort. Applies it to the five suites confirmed failing.

The helper's own test proves the ordering matters: skipping the close makes it
fail with 'cleanup must remove the directory'.

* test: close the database before removing temp DATA_DIR (15 more suites)

Converts the suites that measurably emitted EPERM during a full run to the
shared cleanupTempDataDir helper from #13292.

Measured on the same 15 files:
  base   -> 22 fail, 40 EPERM lines
  branch ->  7 fail, 10 EPERM lines

The 7 remaining failures are pre-existing and unrelated to teardown:
rtk-learn-discover-routes and executor-map-golden already fail on a clean
base (6 and 3 failures respectively).

* test: close the database before removing temp DATA_DIR (final 9 suites)

Completes the #13290 sweep. Two teardown shapes needed the helper:

- after()/t.after() hooks that removed DATA_DIR directly
- beforeEach() hooks that wiped DATA_DIR between tests while the previous
  test's connection was still open. These failed *before* the test body ran,
  so every test in the file reported the same EPERM path.

Three of them already called core.resetDbInstance() right before rmSync and
still leaked, which is the product-side connection leak tracked in #13303.

Measured per file, EPERM lines now 0 across all nine. Remaining failures are
pre-existing on a clean base (firefly 4->1, driverFactory 1, responses-* 1
each) and unrelated to teardown.

* test: add the missing cleanupTempDataDir import to two responses suites

The previous commit swapped rmSync for cleanupTempDataDir in these two files but
did not add the import, so both suites died with
ReferenceError: cleanupTempDataDir is not defined before running any test.

responses-parse-once-4041:            0 pass / 1 fail -> 4 pass / 0 fail
responses-route-early-keepalive-wiring: 0 pass / 1 fail -> 3 pass / 0 fail

Both now report 0 EPERM.

* test: close SQLite handles in three silently-leaking suites

These three suites requested DATA_DIR cleanup but the delete failed on
Windows because a SQLite connection was still open. They pass today, so
the leak is invisible: they carry state between tests and would surface
later as an unrelated-looking assertion, as #13303 already did in the
Firefly suite (a 500 instead of a 401).

agentbridge-mitm-router-key-6403 and agent-bridge-bypass-flow removed
their own temp dir in test.after() without closing the DB first; both now
use the shared cleanupTempDataDir helper, which closes the singleton
before removing the directory.

issue-agent-route-execution is a different case: it has no teardown at
all, so the connection stayed open until process exit and the
isolateDataDir cleanup hook then hit EPERM. It now closes the DB in
test.after().

Verified with a probe on fs.rmSync: all three reported a failed delete
before, and zero across three consecutive runs after, while the same
probe still reports four leaks in the Firefly suite.

* test: remove temp DATA_DIR in five suites that never cleaned up

These five suites create their own mkdtemp DATA_DIR, open the SQLite DB and
never remove the directory, so every run leaves a storage.sqlite behind in the
OS temp dir. Each dir is private to its suite, so this leaked disk space rather
than corrupting results - but the churn is pointless.

Each now closes the DB and removes its directory through the shared
cleanupTempDataDir helper.

Verified with an exit-time probe that lists storage.sqlite* still present in
DATA_DIR: it fired for these suites before the change and is silent after,
with the same test counts (22/14/5/3/3 passing).
2026-09-17 02:31:53 -03:00
anhtahaylove
0b96fe9dc9 fix(combo): retry pre-content streaming failures (#13630)
* fix(combo): retry pre-content streaming failures

* fix(combo): allow same-target retry for native-pinned pre-content failures

A native Codex turn pin forced maxRetries to 0 unconditionally, which also
disabled same-target retries. A pre-content stream failure sends no bytes to
the client, so retrying the same pinned target is safe and indistinguishable
from a first attempt.

Set retries stay disabled so a pinned turn can never fail over to a different
target.

Adds a regression test covering a pinned turn whose first attempt fails before
any content is streamed.

* docs(changelog): add fragment for pre-content streaming retry fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:31:36 -03:00
initguru
8deea62daf fix(sse): guard empty tool_calls[] and strip tool_choice without tools (#12901)
Two related schema-compliance fixes for upstreams that enforce the OpenAI
spec strictly (vLLM self-hosted, Kimi-K2.6):

1. Empty tool_calls[] guard: providers like Kimi-K2.6 attach an empty
   tool_calls:[] array to every content delta when tools are defined. An
   empty array is truthy, so guarding on 'delta.tool_calls' alone called
   closeMessage() after the first content delta, closing the message item
   prematurely. Subsequent content deltas arrived on a done item and were
   dropped by clients (Codex: 'OutputTextDelta without active item'),
   leaving only the first text fragment in the output. Guard both
   translation paths on 'delta.tool_calls?.length' so closeMessage runs
   only when at least one actual tool call is present:
   - open-sse/translator/response/openai-responses.ts (chatCore translate path)
   - open-sse/transformer/responsesTransformer.ts (/v1/responses direct path)

2. tool_choice schema guard: auxiliary/internal calls (e.g. WebSearch)
   legitimately send tool_choice:'auto' with no tools array, and routing
   may drop the tools array after the client sent it. vLLM rejects this
   combination with a schema 400 ('When using tool_choice, tools must be
   set'). Add stripToolChoiceWithoutTools() to targetRequestSanitizer.ts
   that removes a dangling tool_choice lacking a usable tools array at
   the common dispatch boundary. The guard is OpenAI-spec compliance, not
   provider-specific, so it fires regardless of provider.

TDD: tests reproduce both bugs (Red: 5 fail → Green: 31 pass, 0 fail):
- tests/unit/translator-openai-responses-empty-tool-calls.test.ts (2 tests)
- tests/unit/responses-transformer.test.ts (+2 tests, 21 total pass)
- tests/unit/tool-choice-schema-normalization.test.ts (8 tests)
typecheck:core: 0 errors

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
2026-09-17 02:31:19 -03:00
initguru
9bc7eb8fd2 fix(vision-bridge): nested tool_result images + provider-prefix credential check (#12903)
* fix(vision-bridge): extract/replace images nested inside tool_result content

Claude Code sends tool_result images as {type:"image",source:{base64}}
nested inside a tool_result's content array, not as top-level content
parts. The vision-bridge guardrail's extractImageParts filtered nested
hits out (!p.nested), so these images were silently dropped — a
text-only executor then received a request with no image and returned
HTTP 400.

Port the path-based nested extraction/replace fix:
- MediaPart gains a path field: the key/index chain from
  message.content[partIndex] down to the media object itself.
- inspect() tracks the path through recursion; pushPart stamps it.
- extractImageParts drops the !p.nested gate and emits path for nested
  hits (extract↔replace contract preserved: same order, every hit
  replaceable).
- replaceImageParts rewrites via detectMediaParts: top-level hits swap
  their content slot, nested hits walk MediaPart.path via the new
  replaceObjectAtPath helper.
- ensureBase64ImagesForClaudeWire skips nested hits (.filter(!p.path))
  to keep its sequential index map aligned.

TDD: 7 failing tests (path field, nested extract, nested replace,
document order) → 47/47 pass. typecheck:core clean.

* fix(vision-bridge): resolve provider prefix to node id for credential check

Re-land 932002580 (2026-08-19), which was never merged: it branched off
7acddd91a and fell outside the group-D reimplementation range (4f01fba68
re-picked only cf4dfc868). The same root cause now surfaces on the reroute
path (visionBridgeRerouteTextOnly=true): hasUsableCredentialsForModel
queried provider_connections with the bare node prefix "skhynix" → 0 rows
→ false → getBestVisionModel discarded the configured fixed model and
auto-selected cloudflare-playground/moonshotai/kimi-k2.7-code → Playwright
chromium missing → 502 on every image-bearing request.

- resolveProviderCredentialIds: literal prefix + prefix-index mapped node
  id (no-op dedup), composed after #10760's alias→canonical
  resolveProviderId.
- getPrefixToNode: 60s-cached getProviderPrefixIndex lookup, fail-open
  null.
- hasUsableCredentialsForModel: loop the resolved provider ids and return
  true when any has a usable active connection; noauth empty-set
  semantics (#10702) preserved.

TDD: resolveProviderCredentialIds 4/4 + skhynix node-id integration test
(RED confirmed: false !== true on the reroute regression). Focused
regression green: visionBridgeCredentials 10/10, vision-bridge reroute/
credentials suite 12/12, vision-bridge policy/mode/cache 18/18,
visionBridgeRouter 16/16. typecheck:core clean.

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:30:54 -03:00
initguru
f3acf4f811 fix(sse): inject global system prompt once, post-translation, across all target shapes (#12904)
* fix(sse): inject global system prompt post-translation for codex/Responses path

codex/Responses requests carry input[]+instructions, not messages[]. The
existing injectSystemPrompt runs PRE-translation (chatCore.ts) and only
handles messages[]/system fields, so the Global System Prompt (After Prompt =
suffixPrompt) never reached the provider for codex — verified 0/84 call logs
while the catalog base_instructions reached 84/84.

Add injectSystemPromptPostTranslation() and call it after prepareUpstreamBody
on the resolved messages[]. With multiple system/developer messages (codex
normalises its per-item developer roles to system), prefix goes on the FIRST
and suffix on the LAST so the After Prompt retains the highest recency
position — the semantics injectSystemPrompt's single-findIndex buries.

Also wire OMNIROUTE_SYSTEM_INSTRUCTION_APPEND on the /v1/messages (Claude
Messages -> OpenAI Chat Completions) translation path. The directive was
previously only wired on the Responses API path, so DeepSeek-V4 kept leaking
English planning/chain-of-thought into the content field on Claude Code
sessions that route through /v1/messages. Mirror the openai-responses.ts
pattern: append to string system, append a text block for array content, or
unshift a new system message when none exists.

Tests: 23/23 (19 system-prompt incl. 6 postTranslation + codex regression;
4 claude-to-openai directive append). typecheck:core clean.

* test(sse): reproduce global prompt double injection — single-injection contract tests

* fix(sse): unify global prompt injection to single post-translation pass

* fix(sse): carry single global-prompt injection across claude/gemini/responses target shapes

* fix(sse): restore global-prompt coverage for carrier-less targets via gated pre-translation pass

* fix(sse): cover codex/gemini source shapes in the carrier-less pre-translation gate

* fix(types): preserve generic system prompt return

* fix(sse): correct file reference in claude-to-openai.ts comment and add changelog fragment

Points the #reasoning-bilingual comment at the real companion file
(translator/response/openai-to-claude.ts's directivePreambleStripper.ts
from #12905) instead of the nonexistent "openai-responses.ts", and adds
the changelog fragment referenced in the PR body but missing from the
diff.

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:30:35 -03:00
initguru
d70f43d4b4 fix(sse): retry empty_response 502 + reasoning-aware direct response-start timeout (#12906)
* fix(sse): retry 0-byte empty_response 502 like STREAM_EARLY_EOF to stop autocompact 502

A genuine 0-byte upstream empty response (GLM-5.2 on a huge autocompact
context returns ONLY reasoning_content or nothing, then closes) reaches
stream.ts::emitClaudeEmptyStreamErrorAndAbort which emits a 502 with
code "empty_response" via the onFailure callback AND propagates the
failure down the pipeline as controller.error(new Error(msg)). The plain
Error carries no .code, so getUpstreamErrorIdentifier (reads only
error.code) returns undefined, result.errorCode/result.errorType become
undefined, and the single-model retry guard (chat.ts) only matches
errorType === "stream_early_eof" / errorCode === "STREAM_EARLY_EOF".
The 502 surfaces to the client with no re-attempt (call logs
1788132529140-96ef4a / 1788142914004-062cf6, ~48s, tokens out=0).

This is the same class of transient upstream glitch STREAM_EARLY_EOF was
built for (HTTP 200 then zero useful frames — #3758), but empty_response
was never wired into the retry path.

Fix (three chokepoints, all required for consistency):
- stream.ts: emitClaudeEmptyStreamErrorAndAbort now propagates an Error
  carrying code="empty_response" so a downstream classifier can identify
  it (plain new Error(msg) dropped it).
- chatHelpers.ts: shouldRetryStreamEarlyEof now treats "empty_response"
  the same as "STREAM_EARLY_EOF" via RETRYABLE_STREAM_EMPTY_CODES Set —
  ONE bounded same-connection re-attempt, never a loop
  (STREAM_EARLY_EOF_MAX_RETRIES=1 unchanged).
- chat.ts: the single-model retry guard now also enters on
  errorCode === "empty_response".

The bounded retry never marks the account unavailable (an empty response
is a transient upstream glitch, not a bad key), mirroring #3758.

Tests: 5/5 (stream-empty-response-retry-96ef4a). Existing 3758 regression
guard stays green (5/5). typecheck:core clean.

* fix(sse): make direct response-start timeout reasoning-aware to stop 504 on high-effort TTFB

Reasoning models (GLM-5.2/5.3 reasoning.effort=high/max, codex-gpt-5.x-high,
third-party Claude-format replicas) warm up with a ~78s+ TTFB before
emitting the first byte. The stream-readiness layer (streamReadinessPolicy)
already budgets 180s for this class, but the fetch-layer guard
(resolveDirectHeadersTimeoutMs) was a flat 30s — it pre-empted a warm
reasoning response the readiness layer would have permitted, surfacing a
504 (regression introduced by 142ae9349).

Fix: resolveDirectHeadersTimeoutMs now accepts the request body and, when
hasHighReasoningEffort(body) matches a quoted "reasoning_effort" or nested
"effort" field with value high/max, raises the budget to
REASONING_READINESS_CEILING_MS (180_000) — aligning to the same ceiling the
readiness layer uses. The operator env override (OMNIROUTE_DIRECT_HEADERS
TIMEOUT_MS) is treated as a FLOOR: reasoning awareness only raises the
budget, never lowers it; an override above the ceiling (e.g. 240s) is
preserved.

proxyFetch.ts passes the request body (when it is a string) to
resolveDirectHeadersTimeoutMs so the budget is per-request.

The HIGH_REASONING_EFFORT_PATTERN is a bounded, non-overlapping regex
(no variable-length quantifier overlap) — no ReDoS surface (PII rule #1).

Tests: 7/7 (direct-response-start-timeout-reasoning-504 — flat default,
env override, high/max ceiling bump, floor semantics, non-reasoning
pass-through). typecheck:core clean.

* docs(changelog): add fragments for empty_response 502 retry + reasoning-aware timeout

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:30:15 -03:00
initguru
f4be5cc0c7 fix(sse): demote mid-conversation system roles to user in claude-to-openai translation (#12908)
* fix(sse): demote mid-conversation system roles to user in claude-to-openai translation

Claude Code hook contexts (SessionStart ~25KB, PreToolUse) arrive as
role:system messages in the middle of the messages array. HCP-Vision-Latest
vLLM (via LiteLLM gateway) rejects any system not at index 0 with
400 "System message must be at the beginning." Demote every system at
output index > 0 to user with content preserved byte-identical; the
index-0 system (translator-made from the top-level system field, or
client-placed first) stays untouched. Other upstreams are unaffected:
135/138 recent mid-system calls to GLM-5.3-Flash / DeepSeek-V4-Flash
already returned 200. Supersedes the pass-through assertion of #6954
(its intent — systems never misattributed as assistant — still holds).

* test(sse): harden mid-system demotion with env guard and array-path invariant note

Quality-review Minor 1: document the array-return-path invariant in
claudeToOpenAIRequest (convertClaudeMessage arrays are tool/user only, so
no second system element can survive demotion while result.messages is
empty). Minor 2: add the same OMNIROUTE_SYSTEM_INSTRUCTION_APPEND env
guard to the #6954 test as the new mid-system test for consistency.
APPROVED items, no behavior change.

---------

Co-authored-by: Jihyun Son <jihyun.son@sk.com>
2026-09-17 02:29:57 -03:00
initguru
9febe4414d fix(compat): preserve GPT and Claude Code tool-call history (#12909)
* fix(compat): preserve GPT and Claude Code tool-call history

* docs(changelog): add fragment for tool-call history preservation fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:29:39 -03:00
initguru
051576fd3d fix(usage): finalize semantic cache hits by exact request id (#12910)
* fix(usage): finalize semantic cache hits by exact request id

* docs(changelog): add fragment for semantic cache exact-id finalize fix

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-09-17 02:29:19 -03:00
dependabot[bot]
602161d7bc deps: bump the development group across 1 directory with 15 updates (#13661)
Bumps the development group with 15 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@eslint/compat](https://github.com/eslint/rewrite/tree/HEAD/packages/compat) | `2.1.0` | `2.1.1` |
| [@playwright/test](https://github.com/microsoft/playwright) | `1.62.1` | `1.63.0` |
| [@testing-library/user-event](https://github.com/testing-library/user-event) | `14.6.6` | `14.6.7` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `26.4.0` | `26.5.1` |
| [bun](https://github.com/oven-sh/bun) | `1.4.0` | `1.4.2` |
| [@types/bun](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/bun) | `1.4.0` | `1.4.2` |
| [eslint](https://github.com/eslint/eslint) | `10.9.1` | `10.10.0` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.3.3` | `16.3.5` |
| [fast-check](https://github.com/dubzzz/fast-check/tree/HEAD/packages/fast-check) | `4.9.0` | `4.10.0` |
| [knip](https://github.com/webpro-nl/knip/tree/HEAD/packages/knip) | `6.34.0` | `6.35.1` |
| [lint-staged](https://github.com/lint-staged/lint-staged) | `17.4.1` | `17.5.1` |
| [opencode-ai](https://github.com/anomalyco/opencode) | `1.18.25` | `1.18.30` |
| [promptfoo](https://github.com/promptfoo/promptfoo) | `0.122.2` | `0.123.0` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.69.0` | `8.70.0` |
| [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) | `4.1.11` | `5.0.0` |



Updates `@eslint/compat` from 2.1.0 to 2.1.1
- [Release notes](https://github.com/eslint/rewrite/releases)
- [Changelog](https://github.com/eslint/rewrite/blob/main/packages/compat/CHANGELOG.md)
- [Commits](https://github.com/eslint/rewrite/commits/compat-v2.1.1/packages/compat)

Updates `@playwright/test` from 1.62.1 to 1.63.0
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.62.1...v1.63.0)

Updates `@testing-library/user-event` from 14.6.6 to 14.6.7
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.6...v14.6.7)

Updates `@types/node` from 26.4.0 to 26.5.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `bun` from 1.4.0 to 1.4.2
- [Release notes](https://github.com/oven-sh/bun/releases)
- [Commits](https://github.com/oven-sh/bun/compare/bun-v1.4.0...bun-v1.4.2)

Updates `@types/bun` from 1.4.0 to 1.4.2
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/bun)

Updates `eslint` from 10.9.1 to 10.10.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.9.1...v10.10.0)

Updates `eslint-config-next` from 16.3.3 to 16.3.5
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.5/packages/eslint-config-next)

Updates `fast-check` from 4.9.0 to 4.10.0
- [Release notes](https://github.com/dubzzz/fast-check/releases)
- [Changelog](https://github.com/dubzzz/fast-check/blob/main/packages/fast-check/CHANGELOG.md)
- [Commits](https://github.com/dubzzz/fast-check/commits/v4.10.0/packages/fast-check)

Updates `knip` from 6.34.0 to 6.35.1
- [Release notes](https://github.com/webpro-nl/knip/releases)
- [Commits](https://github.com/webpro-nl/knip/commits/knip@6.35.1/packages/knip)

Updates `lint-staged` from 17.4.1 to 17.5.1
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.4.1...v17.5.1)

Updates `opencode-ai` from 1.18.25 to 1.18.30
- [Release notes](https://github.com/anomalyco/opencode/releases)
- [Commits](https://github.com/anomalyco/opencode/compare/v1.18.25...v1.18.30)

Updates `promptfoo` from 0.122.2 to 0.123.0
- [Release notes](https://github.com/promptfoo/promptfoo/releases)
- [Changelog](https://github.com/promptfoo/promptfoo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/promptfoo/promptfoo/compare/0.122.2...0.123.0)

Updates `typescript-eslint` from 8.69.0 to 8.70.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.70.0/packages/typescript-eslint)

Updates `vitest` from 4.1.11 to 5.0.0
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v5.0.0/packages/vitest)

---
updated-dependencies:
- dependency-name: "@eslint/compat"
  dependency-version: 2.1.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: "@playwright/test"
  dependency-version: 1.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: "@testing-library/user-event"
  dependency-version: 14.6.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: "@types/bun"
  dependency-version: 1.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: "@types/node"
  dependency-version: 26.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: bun
  dependency-version: 1.4.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: eslint
  dependency-version: 10.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: eslint-config-next
  dependency-version: 16.3.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: fast-check
  dependency-version: 4.10.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: knip
  dependency-version: 6.35.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: lint-staged
  dependency-version: 17.5.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: opencode-ai
  dependency-version: 1.18.30
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: development
- dependency-name: promptfoo
  dependency-version: 0.123.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: typescript-eslint
  dependency-version: 8.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: development
- dependency-name: vitest
  dependency-version: 5.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-17 02:26:48 -03:00
dependabot[bot]
856051901d deps: bump oven/bun from 1.4.0-slim to 1.4.2-slim (#12977)
Bumps oven/bun from 1.4.0-slim to 1.4.2-slim.

---
updated-dependencies:
- dependency-name: oven/bun
  dependency-version: 1.4.2-slim
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-09-17 02:26:20 -03:00
Diego Rodrigues de Sa e Souza
3d5baf13f4 fix(providers): strip Vertex doc script blocks whose end tag carries junk (#13936)
The Vertex model-docs HTML is converted to plain text before the table
parser reads context-window and token-limit numbers out of the cells.
The script/style removal pass required the end tag to be `</script\s*>`,
but the HTML spec closes the element on `</script\t\n foo>` too. Such a
block survived the pass; the generic `<[^>]+>` strip below then removed
both tags and kept the script BODY, so text that only ever existed inside
a script became cell text the number parser trusts.

Accept any end tag that starts with `</script`/`</style` followed by a
tag-name boundary, matching what a browser does.

CodeQL js/bad-tag-filter, alert #1007.
2026-09-16 23:57:14 -03:00
dependabot[bot]
f89e188a9f deps: bump js-yaml (#13212)
Merged as-is — single-file lockfile bump in `/electron`, 3 lines, transitive resolution only.

js-yaml 4.3.2 hard-limits the merge-sequence size and counts empty mappings toward `maxTotalMergeKeys` (nodeca/js-yaml#797) — CPU-DoS hardening, which is worth taking promptly and carries no API surface change.

Verified against the tip before merging: `electron/package-lock.json` is the only file touched.
2026-09-16 23:17:20 -03:00
Ravi Tharuma
416c736bb9 fix(oauth): soft-fail Claude refresh so CredentialHealth is not sticky-dead (#13185)
Merged after boarding with #13426 into one worktree cut from `release/v3.8.51` (both verified as ancestors of the combined HEAD before validating).

**Evidence**
- Your own test plus **every sibling** in the module — 14 files across `tokenHealthCheck*`, `token-health-check*`, `credential-health*` and `issue-13470-token-refresh-proxy-bypass`: **72/72 pass** on the combined tree. Running the siblings and not just the PR's own file is deliberate: this PR changes sweep-path state that several of those files exercise independently.
- Gates: `check-changelog-integrity` PASS, `check-complexity` PASS (2842 vs baseline 3218), `check-cognitive-complexity` PASS (1284 vs 1437), `typecheck:core` PASS.

**Reconciled — one real gate violation, fixed in your branch (d9164886)**

`check-file-size` genuinely tripped on this PR: `src/lib/tokenHealthCheck.ts` goes 1214 → 1221, past a frozen ceiling of 1218 that had only 4 lines of headroom. Attributed by measuring both sides, not assumed — the tip is at 1214 with no violation. Rebaselined the ceiling to 1221 with a dated justification key, since the growth *is* the fix: preserving the `refresh_token` and telling a transient failure apart from a dead credential needs extra state on the sweep path that cannot leave the module without breaking its internal API.

One thing that looked like your problem and is not, recorded so nobody re-raises it: `open-sse/executors/codex.ts: 1529 > 1528` shows up when the gate runs on your branch. Your branch carries an older merge of the release where that file was longer; the tip has it at 1524, your diff never touches it, and it does not survive the squash.

Thanks, @RaviTharuma — a sticky-dead `CredentialHealth` is the worst failure mode here, because a transient refresh blip permanently parks a working account and nothing ever retries it. Driving the real provider through two consecutive sweeps and re-reading the DB row in between is the right way to prove the state actually clears.
2026-09-16 23:10:24 -03:00
Ravi Tharuma
683a552fcb fix(opencode-plugin): report disk-cache age in the stale-fallback warning (#13426)
Merged after boarding with #13185 into one worktree cut from `release/v3.8.51` (both verified as ancestors of the combined HEAD before validating).

**Evidence**
- Full `@omniroute/opencode-plugin` suite — not just this PR's file: **373/373 pass** across all 26 test files.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS (2842 vs baseline 3218), `check-cognitive-complexity` PASS (1284 vs 1437), `typecheck:core` PASS.
- `check-file-size`: no violation attributable to this PR.

**One note for the record.** On a first run the suite reported `scaffold: built ESM default export resolves with the v1 plugin shape` failing with `ERR_MODULE_NOT_FOUND … dist/index.js`. That was a fresh worktree without the plugin built, not a defect in this PR — after `npm run build` in the workspace, all 373 pass. Flagging it because that failure reads exactly like a PR bug and could easily be misattributed to you on a future run.

Thanks, @RaviTharuma — putting the snapshot age in the warning turns "using stale disk cache" from a fact into something an operator can act on, and you covered `snapshotAgeLabel` with both the numeric-`writtenAt` and missing-`writtenAt` cases rather than only the happy path.
2026-09-16 22:56:38 -03:00
Nguyen Thanh Dat
5acac8021d fix(redis): namespace warmup circuit-breaker keys with REDIS_KEY_PREFIX (#13328)
The warmup scheduler's circuit-breaker keys were written to Redis without `REDIS_KEY_PREFIX`, so they escaped OmniRoute's namespace and could collide with another app sharing the instance — the one Redis surface the prefix wasn't reaching. Probe: 2/2 pass in `tests/unit/lib/warmupScheduler/redisCircuitBreakerStorePrefix.test.ts`, covering both the prefixed case and the unset/blank case where keys must stay unchanged.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.

**Reconciled** — this PR was `CONFLICTING`. The conflict was in `docs/reference/ENVIRONMENT.md` and purely additive: the release tip had inserted `APP_BIND_HOST` / `QDRANT_BIND_HOST` / `BIFROST_BIND_HOST` rows directly above the `REDIS_KEY_PREFIX` row you edited. Kept both sides — the tip's three new rows and your updated description naming the warmup circuit breaker — then merged the current release branch in (120a92f6) and re-ran your focused test on the reconciled tree: 2/2 pass. No line of your diff was dropped.

Thanks, @datrixlab — you also updated `.env.example`, `docs/ops/REDIS_PRODUCTION_CONFIG.md` and `ENVIRONMENT.md` alongside the code, which is why the only thing left to do here was a mechanical conflict resolution.
2026-09-16 21:09:38 -03:00
Nguyen Thanh Dat
7ad270ca7a fix(translator): send a Claude tool_result image to Gemini as inlineData (#13335)
A Claude `tool_result` carrying an image was not translated into Gemini `inlineData`, so the image was dropped from the conversation. Probe on your head: 3/3 pass.

Thanks, @datrixlab.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:03:57 -03:00
Nguyen Thanh Dat
c1338f1e78 fix(translator): pair id-less Gemini tool results with their call (#13334)
Gemini tool results without an id could not be paired with their originating call, so the pairing fell apart on any history that omitted ids. Probe on your head: 24/24 pass across `gemini-tool-result-without-id` and the existing `v1beta-gemini-tool-calling-6222` suite; the combined run reconfirmed both plus the antigravity path.

Thanks, @datrixlab — extracting `geminiToolCallIds.ts` as a shared helper instead of duplicating the pairing logic across `gemini-to-openai`, `antigravity-to-openai` and the v1beta converter is what keeps the three from drifting apart later.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:03:40 -03:00
Nguyen Thanh Dat
84149b93e3 fix(translator): keep tool_choice "none" instead of turning it into "auto" (#13333)
Claude accepts `tool_choice: "none"` natively, but the translator was rewriting it to `"auto"` in both directions — a caller explicitly forbidding tool use got tools offered anyway. Probe on your head: 36/36 pass across `translator-openai-to-claude` and `translator-claude-to-openai`.

Thanks, @datrixlab — doing both directions in one PR is right; a one-sided fix here would have been worse than none.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:03:25 -03:00
Nguyen Thanh Dat
53e3e23c13 fix(api): stop DISABLE_SQLITE_AUTO_BACKUP from turning off Redis rate limiting (#13329)
A backup flag was being used as a proxy for test mode, so `DISABLE_SQLITE_AUTO_BACKUP` also disabled Redis rate limiting — two unrelated concerns riding one variable. Probe on your head: 3/3 + 13/13 pass across the new test and the existing rate-limiter suite.

Thanks, @datrixlab — catching that the existing rate-limiter tests still pass is what shows this untangled the two without changing the intended behavior of either.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:03:09 -03:00
Nguyen Thanh Dat
f781fd038b fix(cli): let restart honor PORT like serve and dashboard (#13327)
Same fix pattern #7049 already applied to `dashboard`: dropping the Commander default `20128` on `restart --port` lets `opts.port` stay undefined so `runServe()`s `opts.port ?? process.env.PORT ?? "20128"` fallback can actually reach `PORT`. 3 files, +36/-1.

Thanks, @datrixlab — matching the existing precedent instead of inventing a new mechanism made this trivial to review.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:02:51 -03:00
Nguyen Thanh Dat
bb45fb8ede fix(providers): let a dashboard OFF for private provider URLs beat the env opt-in (#13323)
Confirmed on the tip: `src/shared/network/outboundUrlGuardPolicy.ts` only checked `isTrueValue(dbValue)`, so an explicit dashboard OFF fell through to the env opt-in instead of overriding it — the operator turning something off in the UI had no effect. Probe on your head: 7/7 pass.

Thanks, @datrixlab — an explicit OFF in the UI losing to an env var is the kind of thing that erodes trust in the whole settings surface.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:02:35 -03:00
Nguyen Thanh Dat
840ae79612 fix(cli): read commander's negated --no-* flags as opts.<name> === false (#13322)
Commander exposes a negated `--no-x` flag as `opts.x === false`, not as `opts.noX`, so every `--no-*` flag on `chat`, `contexts` and `serve` was being read as undefined and silently ignored. Probe on your head: 5/5 pass in `tests/unit/cli-negated-flags.test.ts` — a real Commander parser with mocked fetch and a temp `DATA_DIR`, exercising all three commands end to end rather than asserting on the parser in isolation.

Thanks, @datrixlab.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:02:19 -03:00
Nguyen Thanh Dat
466d0306be fix(resilience): parse RFC 3339 and fractional-second rate-limit resets (#13321)
Rate-limit reset headers arriving as RFC 3339 or with fractional seconds were not parsed, so the reset hint was silently dropped and the generic cooldown applied instead. Probe on your head: 10/10 pass in `tests/unit/ratelimitmanager-headers-split.test.ts`, including your 3 new RFC3339/fractional/unix-timestamp cases.

Thanks, @datrixlab — covering all three shapes in one pass, rather than only the one you hit, is why this needed no follow-up.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:02:04 -03:00
Nguyen Thanh Dat
3f239717b5 fix(sse): keep a round-robin target's concurrency limit through a cooldown (#13320)
Verified on the tip that `getGate()` unconditionally overwrites `gate.max` (`open-sse/services/rateLimitSemaphore.ts:54-66`), so a target coming out of cooldown lost its configured concurrency limit. Probe on your head: 3/3 pass.

Thanks, @datrixlab.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:01:48 -03:00
Nguyen Thanh Dat
3bcb181cc2 fix(quota): apply the equal-split fallback in the pool usage snapshot (#13159)
The equal-split fallback was applied when computing allocations but not when building the pool usage snapshot, so `sqliteQuotaStore.ts:201` still read `totalWeight > 0 ? alloc.weight : 0` — a zero-weight pool reported every member at 0 instead of its equal share. Probe on your head: 9/9 pass in `tests/unit/quota-pool-usage-equal-split.test.ts`, with the bug confirmed unfixed on the tip.

Thanks, @datrixlab — fixing the snapshot path and not just the allocation path is the part that makes the dashboard numbers agree with the enforcement.

**Batch validation** — boarded with the other 10 PRs of your batch into one worktree cut from `release/v3.8.51`; every PR verified as an ancestor of the combined HEAD before validating.

- Focused tests across all 11 PRs: **104/104 pass** on the combined tree.
- Gates on the combined tree: `check-changelog-integrity` PASS, `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check:open-sse-typecheck` PASS.
- `check-file-size` is red, but reproduces with byte-identical line counts on the pure `release/v3.8.51` tip (`open-sse/handlers/imageGeneration.ts` 3304, `open-sse/services/combo/roundRobinCombo.ts` 1221, `open-sse/utils/stream.ts` 3115). Inherited base-red, nothing added by this batch — it is also why this PR's "Fast Quality Gates" check was red.
2026-09-16 21:01:24 -03:00
Dmitry Kuznetsov
a2c6f7188d fix(antigravity): keep unreported quota fraction unknown (#7138)
Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9944 and #9908.

**Evidence**
- Focused tests: 36/36 pass on the combined tree, including `convertUsageToQuotaInfo skips Antigravity quota entries with an unknown fraction` and the `#6295` regression that guards the same class of bug on another provider.
- Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS.
- The red `check-file-size` reproduces byte-identical on the pure `release/v3.8.51` tip — inherited base-red, not from this PR. The red CI run here dates from 2026-09-15 against an older base.

Thanks, @Ardem2025 — this is the smallest diff of your batch and arguably the one with the widest blast radius avoided. Writing `remainingPercentage: 0` for an unreported fraction made "we don't know" numerically indistinguishable from "fully exhausted" to every downstream consumer of the quota cache; omitting the field so preflight fails open is the correct read of the upstream's silence.
2026-09-16 19:59:58 -03:00
Dmitry Kuznetsov
00860f3278 fix(antigravity): rotate image accounts on explicit quota exhaustion (#9908)
Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9944 and #7138.

**Evidence**
- Focused tests: 36/36 pass on the combined tree, including your 4 classification-boundary cases in `tests/unit/antigravity-image-credential-retry.test.ts` (Antigravity quota-exhausted `429` rotates; generic `RESOURCE_EXHAUSTED`, ordinary image rate-limit and non-Antigravity `429` stay terminal).
- Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS.
- The red `check-file-size` reproduces byte-identical on the pure `release/v3.8.51` tip — inherited base-red, not from this PR. The red CI run on this PR dates from 2026-09-15 against an older base.

**Related dispositions**
- #8053 is being closed in your favour: it chased the same account-rotation goal across 3 files plus a new `routingInstrumentation.ts`, while its `AbortSignal` half was already superseded on the tip by independent work. This PR does the same job in 31 lines of production code by reusing the existing `classify429` engine.

Thanks, @Ardem2025 — the deliberate narrowness here is the reason this merged and the bigger version didn't. Gating rotation on `provider === "antigravity" && status === 429 && classify429() === "quota_exhausted"` keeps non-idempotent image generation from being retried on ordinary rate limits, and you proved each negative case rather than just the happy path.
2026-09-16 19:59:43 -03:00
Dmitry Kuznetsov
2f0a01d75c fix(oauth): expose manual Codex callback entry (#9944)
Merged after batch validation on a combined worktree cut from `release/v3.8.51` with #9908 and #7138.

**Evidence**
- Focused tests: 36/36 pass on the combined tree (`oauth-modal-codex-lan-ip-8046`, `antigravity-image-credential-retry`, `antigravity-usage-service`, `generic-quota-fetcher`), including the 2 pre-existing anchor tests that assert `codex` stays in `PKCE_CALLBACK_SERVER_PROVIDERS` and that the `localhost:1455` redirect URI is untouched.
- Gates on the combined tree: `check-complexity` PASS, `check-cognitive-complexity` PASS, `typecheck:core` PASS, `check-changelog-integrity` PASS.
- `check-file-size` is red, but reproduces byte-identical on the pure `release/v3.8.51` tip (`imageGeneration.ts`, `roundRobinCombo.ts`, `stream.ts`) — inherited base-red, not from this PR.

**Reconciled**
- `changelog.d/fixes/codex-manual-loopback-action.md` did not start with a markdown bullet, which is the one thing `check-changelog-integrity` failed on. Fixed in your branch (d3dbb5b) so the fragment convention holds; nothing else in your diff was touched.

Thanks for this one, @Ardem2025 — exposing the manual callback entry that already existed in the code instead of adding a new flow is exactly the right shape for the LAN/remote case, and keeping every PKCE/state check untouched made it easy to verify.
2026-09-16 19:59:25 -03:00
Jan Leon
f1e7148c19 fix(routing): preserve reasoning overrides across transports and fallbacks (#13556)
Merged. The failure mode was concrete — a matched reasoning rule dropped on native Responses/Anthropic paths, model-suffix/account defaults, or fallback preparation, and `_omnirouteReasoningRule` leaking upstream as `Unsupported parameter` — and the fix is carried in the request-local credential context through dispatch, refreshed credentials and fallbacks, with forced effort winning over defaults and client-forged markers dropped at ingress. The 11-case integration suite exercises the real routing/translation modules.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you — and for keeping this a runtime-only change with the editor and service-tier work in their own PRs.
2026-09-16 17:01:51 -03:00
Jan Leon
a928ea8762 fix(models): reconcile provider dashboards with active live catalogs (#13434)
Merged. The NVIDIA 116-vs-82 discrepancy is the visible symptom; the fix is the right one — reuse the existing `liveCatalogAuthoritative` policy on the dashboard listing instead of a provider-specific filter, refresh after a removals-only import, keep the last confirmed snapshot on a failed refresh, and apply the same membership rule to the OpenRouter/compatible/passthrough row builders so static fallbacks cannot resurrect retired rows. Operator custom models and overrides preserved.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you — checking the projection against 14 production catalog snapshots is the kind of evidence that makes a listing change safe to land.
2026-09-16 16:56:28 -03:00
Jan Leon
bd8a12f304 fix(providers): fetch live Qwen and Alibaba Token Plan catalogs (#13299)
Merged. Both Token Plan providers had no `modelsUrl` and no discovery config, so Sync Models never even tried a live request. Fetching the public Personal Plan catalog through `safeOutboundFetch` with fixed hosts, no inference keys and no cookies, validating the gateway envelope and reusing the DashScope text-model classifier keeps this narrow and safe; a failed or media-only result falls back without touching the previous catalog.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you.
2026-09-16 16:50:42 -03:00
Diego Rodrigues de Sa e Souza
af2002a493 chore: reconcile the JxnLexn merge wave with the release tip (#13921)
Lands the combined-board reconciliation of today's JxnLexn wave as one follow-up: i18n fill for #12471/#13555's new keys (real vi translations), free-tier count 446→452, file-size rebaseline for #13556. check-new-key-coverage PASS; only the three pre-existing file-size reds remain.
2026-09-16 16:43:44 -03:00
Jan Leon
a16705344e feat(dashboard): add a dedicated API-key routing editor (#13555)
Merged. Moving rule editing out of the permissions modal into `/dashboard/api-manager/routing` fixes the real problem (a cramped modal for something with conditions, effects and three target kinds), and the contract tests pin what matters: persisted fields round-trip, combo names match without rewriting off-catalog IDs, failed saves stay editable, unsaved drafts survive key switches, and writes are disabled after a failed load. Rule evaluation and authorization are untouched.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you.
2026-09-16 16:38:04 -03:00
Jan Leon
502e614850 fix(vertex): discover and route partner models correctly (#12471)
Merged. Root cause first: `publishers.models.list` was called with `pageSize=1000` against Google's hard maximum of 300, so every publisher answered 400 and discovery silently fell back to the stale static catalog. On top of that the PR separates API-key from Service-Account capabilities correctly (keys cannot list Model Garden — project-scoped curated catalog; SA tokens can — live catalog), stops treating the expected generativelanguage rejection of a Service Account as a discovery failure, replaces the speculative partner IDs with documented MaaS IDs, and rejects OAuth client-config JSON with a clear message instead of a misleading one.

The 5 ESLint errors flagged during the earlier fix sweep were fixed in your own follow-up commits; the branch was reconciled with the release tip and the 42 locale files were checked for lost keys before this merge.

Validated as a combined board first (this PR merged with the 4 siblings of the JxnLexn wave on the release tip): eslint with the frozen suppressions, typecheck:core, check:open-sse-typecheck, complexity, cognitive-complexity, changelog-integrity, docs-sync, migration-numbering, provider-consistency and a duplicate-identifier audit all green, plus 77 passing / 0 failing focused node:test cases across the test files the wave touches. The wave's i18n fill (new keys carried to all 66 locales), free-tier doc counts and file-size rebaseline land in one follow-up PR right after the wave, as with #13904.

Thank you — the credential-capability distinction and the retired/non-chat filtering are what make the Vertex listing trustworthy.
2026-09-16 16:37:41 -03:00
Patryk Kopyciński
c1ed830eb9 chore(lint): prune 3 obsolete eslint suppressions (#13359)
Merged. Your later commits (re-prune on the current base, then restoring the live `react-hooks/immutability` suppressions the first prune had wrongly dropped) were the right call, and they are carried intact.

Maintainer note: a repo-wide `eslint` run on today's release tip (the full validation for a suppressions prune) found **0 code errors** and only more entries that had gone stale since your base — the maintainer branch carries that same prune to the current tip on top of your commits. Removals only, nothing added.

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 chasing the false-positive prune to the end instead of leaving the live suppressions out.
2026-09-16 15:56:36 -03:00
Markus Hartung
1cf8e4bcc6 fix(db): auto-clean terminal batch checkpoints and expired file content (#12999)
Merged after a maintainer rework that kept every one of @hartmark's commits intact.

**What the rework added:** the auto-clean of terminal batch checkpoints and expired file content is gated behind a default-off feature flag (`BATCH_AND_FILE_AUTO_CLEANUP_ENABLED`, `defaultValue: "false"`, documented in `docs/reference/FEATURE_FLAGS.md` and described in all 66 locales) so the release default keeps today's behaviour and operators opt in; the DB handle leak in the test was fixed so the Node runner exits cleanly.

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 — the cleanup itself is exactly the kind of maintenance that stops a data dir from growing forever.
2026-09-16 15:45:34 -03:00
Patryk Kopyciński
c4293e28e2 fix(cursor): recover Kimi tool calls emitted as history narration (#12723)
Merged. Kimi emitting tool calls as history narration is a provider quirk we have to absorb rather than pass through; recovering them keeps the tool contract intact for clients that never see the quirk.

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 15:34:54 -03:00
Patryk Kopyciński
f653a6f94e fix(dispatch): strip every _omniroute* internal marker before upstream send (#13355)
Merged. Internal `_omniroute*` markers must never reach an upstream: at best they are noise in someone else's logs, at worst they change the upstream's parse. Stripping every one of them before the send is the right invariant.

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 15:32:58 -03:00
Patryk Kopyciński
23c5772ccb feat: adaptive reasoning effort (auto) — gateway-resolved, per-turn pinned, all harnesses (#13448)
Merged after a maintainer rework that kept every one of @patrykkopycinski's commits intact — including the two refactors you pushed later (extracting the adaptive-effort wiring out of `chatCore.ts` and reading `x-omniroute-effort` inside the wiring module), which were merged into the rework rather than overwritten.

**What the rework added:** the adaptive-effort wiring is scoped to OpenAI-dispatch requests only (the claim in `docs/routing` was corrected to match), and `defaultReasoningEffort` was widened to accept `auto` explicitly instead of relying on a loose string.

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 — gateway-resolved, per-turn pinned effort is a real feature, and the header contract makes it usable from every harness.
2026-09-16 15:18:36 -03:00
Patryk Kopyciński
421d1ff912 fix(devin): fall back to CLI probe when the HTTP API rejects a CLI-format key (#13617)
Merged after a maintainer rework that kept every one of @patrykkopycinski's commits intact, including the changelog fragment you added afterwards.

**What the rework added:** the `eslint-suppressions.json` diff was corrected (the PR had dropped live entries) and a test now proves the CLI-probe fallback path is actually taken when the HTTP API rejects a CLI-format key — before, the fallback existed but nothing exercised it.

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 15:15:16 -03:00
Markus Hartung
88d5e0cde6 fix(db): gate the auto-cleanup VACUUM on reclaimable space, not row count (#13079)
Merged after a maintainer rework that kept every one of @hartmark's commits intact.

**What the rework added:** the reclaimable-space gate for the auto-cleanup VACUUM sits behind a default-off feature flag so the release default is unchanged, with the flag documented in `docs/reference/FEATURE_FLAGS.md` and described in all 66 locales; the rest is your change as submitted.

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 — gating VACUUM on reclaimable pages instead of row count is the right signal.
2026-09-16 15:09:07 -03:00
Diego Rodrigues de Sa e Souza
86b8bbfd3e fix(tests): bump the APIKEY_PROVIDERS tripwire to 241 (Agnes AI China) (#13905)
`APIKEY_PROVIDERS merges the 6 family files into 240 entries` fails on the
release tip, taking a unit-test shard red on every open PR.

Agnes AI China (#13399, cdcde97c7) added one `apikey/regional` entry, so the
real count is 241 — confirmed at runtime: `Object.keys(APIKEY_PROVIDERS).length`
is 241 and includes `agnes-cn`.

The hardcoded number is deliberate, not an oversight: the test is a tripwire for
silent loss or duplication across the six family files, and each bump is
documented in the header with the provider and the PR that caused it. Kept that
convention rather than deriving the count at runtime, which would make the test
assert nothing.
2026-09-16 13:48:13 -03:00
Patryk Kopyciński
63070bec32 fix(semantic-cache): never cache a truncated completion (#7) (#12885)
Merged. Caching a truncated completion poisons the entry for every later exact-match read — and the analysis in the description is right that the exact-zero cache-read gate is what made it reachable. Refusing the write on both store paths, while keeping `stop`, `tool_calls` and unknown/missing reasons cacheable, is the narrow version of this fix.

Maintainer note: the PR was opened against `main` and its branch had drifted far enough that GitHub reported 1289 changed files. Your single commit was rebased onto the active release tip with authorship intact (nothing else carried over), the PR was retargeted to `release/v3.8.51`, and `tests/unit/semantic-cache-no-truncated-writes.test.ts` re-run there: 3 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.
2026-09-16 13:46:04 -03:00
Diego Rodrigues de Sa e Souza
68b9fc21a5 chore: reconcile the 2026-09-16 merge wave with the release tip (#13904)
Lands the combined-board reconciliation of today's 22-PR wave as one follow-up: i18n fill for #13115's two new keys (65 locales, real translation in vi) and the file-size rebaseline for cursor.ts (#13627) and chatHelpers.ts (#13879). check-new-key-coverage PASS; only the three pre-existing file-size reds remain on the tip.
2026-09-16 13:42:04 -03:00
Patryk Kopyciński
9250a231c5 test(adobe-firefly): lock in the browser-spawn guard with a regression test (#13358)
Merged. Test-only, and the right kind: a browser-spawn guard is precisely the thing that gets removed by accident during a refactor, and nothing was holding it in place 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:39:45 -03:00
Markus Hartung
76b037c2e7 fix(combo): stop retrying an unrecoverable request-shape error across the whole fallback chain (#13338)
Merged. Retrying an unrecoverable request-shape error across the whole fallback chain burns every target on an error that will never change — the caller waits longer for the same failure. Stopping at the first one is right.

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:39:29 -03:00
Patryk Kopyciński
b269821c83 fix(cursor): kv_after_text must not settle away a trailing exec_mcp tool call (#13627)
Merged. Settling on `kv_after_text` while a trailing `exec_mcp` call is still pending drops the tool call entirely — the client then sees a finished turn that never ran the tool. Correct place to fix it.

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:39:14 -03:00
Markus Hartung
3df5d50619 fix(gemini): never send a functionCall-opening turn with no preceding user turn (#13573)
Merged. Gemini rejecting a turn that opens with a `functionCall` and no preceding user turn is a contract we have to honour on our side; building the turn correctly is cheaper than reading the upstream 400 that comes back.

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:38:56 -03:00
Markus Hartung
c099892ac5 fix(embeddings): log server-side when a provider can't be resolved (#13687)
Merged. A provider that cannot be resolved should never be a silent no-op on the server side — the operator is the only one who can act on it, and until now only the caller saw anything.

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:38:40 -03:00
Patryk Kopyciński
4621842d93 feat(reasoning): opt-in min output budget floor for thinking models (#12742)
Merged. Opt-in is what makes this safe to ship: thinking models that need a floor get one, everyone else sees no change in behaviour, and the env var is documented in `.env.example` and `ENVIRONMENT.md` rather than being folklore.

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:38:23 -03:00
Patryk Kopyciński
9cc077ef55 fix(providers): resolve unsupportedParams via model aliases so K3 stops 400ing on temperature (#13037)
Merged. Resolving `unsupportedParams` through the model aliases is the fix that generalises — K3 stops 400ing on `temperature`, and any other alias of a model with the same restriction is covered by construction instead of by a second patch later.

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:37:57 -03:00
Markus Hartung
4591476141 fix(providers): lazily load chatgpt-web-codex admin helpers in PUT /api/providers/[id] (#13071)
Merged. Loading the chatgpt-web-codex admin helpers lazily keeps a rarely used path off `PUT /api/providers/[id]`'s cost — the kind of change that only shows up as latency nobody can explain.

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:37:39 -03:00
Markus Hartung
ac32cea4f0 fix(logs): recover concatenated JSON objects in the Provider Event Stream viewer (#13115)
Merged. Concatenated JSON objects in the Provider Event Stream are a real wire shape, not a corruption — recovering them in the viewer instead of dropping the batch makes the tool trustworthy when it matters.

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:37:23 -03:00
Sahil Daswani
ff493da805 fix: match compatible-provider models owned by public prefix (#13831)
* fix: match compatible-provider models owned by public prefix

Resolves #13829

The provider-scoped /v1/providers/{provider}/models route filters
unified-catalog rows by internal provider ID. For compatible provider
nodes, the catalog emits the configured public prefix in owned_by, so
all valid models were dropped and the endpoint returned an empty list.

Resolve the compatible node's prefix and accept it alongside the
internal ID when filtering and when stripping the prefix from returned
model ids.

* chore(quality): satisfy the format and lint-suppression gates for #13829

Two gate-only touch-ups on top of the fix, no behaviour change:

- prettier --check rejected tests/unit/provider-models-v1-route.test.ts over a
  double blank line before a test block.
- typing the map callback removed the file's only `any`, which left the frozen
  entry in config/quality/eslint-suppressions.json unused; the lint gate fails
  on a stale suppression, so it is pruned.

Both were found by running the gates locally, because this fork PR's workflow
runs are still awaiting maintainer approval and only the semgrep check had run.

Co-authored-by: sahildaswani <sahildaswani@users.noreply.github.com>

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: sahildaswani <sahildaswani@users.noreply.github.com>
2026-09-16 13:37:11 -03:00
Markus Hartung
bc36b1d1aa fix(i18n): stop swallowed FORMATTING_ERROR from showing raw keys/garbled text (#12995)
Merged. A swallowed `FORMATTING_ERROR` surfacing as raw keys or garbled text is exactly the failure mode i18n is supposed to prevent; the user sees the plumbing. Good catch.

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:37:07 -03:00
Markus Hartung
7dbe850daa fix(providers): honor operator-set endpoint overrides for local models (#13078)
Merged. An operator-set endpoint override that is ignored for local models is the worst kind of setting — it looks applied and is not. Honouring it is the whole 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:36:52 -03:00
lorenzozane
5847c43922 fix(resilience): name collision keys, surface header drops, retry embeds, skip far-reset pings (#13766)
Merged. Four small, independently justified changes, each with its own regression test — naming both sides of a case-insensitive key collision, surfacing the dropped-header count to the caller instead of only to the log, one retry before a memory is left unvectorized, and skipping warm pings for a window whose reset is more than 24h out. The first-seen-wins resolution and the caller-visible behaviour of everything else are unchanged.

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 keeping each of the four minimal and commented — that is what made this reviewable as one PR.
2026-09-16 13:36:35 -03:00
lorenzozane
c8d102179d fix(docker): smoke-test images before promoting latest (#13761)
Merged. The ordering is what makes this fix real: the version manifests are published first, the smoke test runs against the published image, and only then are the floating `latest` tags promoted — so a broken build can no longer become `latest`. Both Dockerfiles carry a `HEALTHCHECK`, and the 30×2s window covers the 15s `start-period`.

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.

Thanks for closing #11835 properly instead of just adding a check after the fact.
2026-09-16 13:36:18 -03:00
lorenzozane
20c6d89c2d fix(admission): measure request bodies with the active cost budget (#13762)
Merged. Good catch: the feature extractor was silently capped by the size estimator's 256 KiB default, so every request above it reported the same flattened body/token estimate and admission decided on a number that was not the request. Deriving the measurement limit from the active cost budget — runtime overrides included — is the right place to fix it.

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:36:08 -03:00
lorenzozane
9ce43850b7 fix(compression): preserve case, tags and negations through llmlingua engine (#13768)
Merged. Shielding XML tags and negation/absolute words from an uncased backend, restoring source casing and edge whitespace on re-stitch, and failing open on an empty reply are each the difference between "compressed" and "changed the meaning". The 6 fidelity tests make the negation list something a future reviewer can argue with in code instead of in the abstract.

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:36:02 -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
12904 changed files with 4249865 additions and 361638 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

@@ -112,8 +112,9 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Example: redis://localhost:6379 (or redis://redis:6379 in Docker)
# REDIS_URL=redis://localhost:6379
# Namespace prefix for ALL OmniRoute Redis keys (rate limiter + auth cache +
# quota store). Prevents key collisions when OmniRoute shares a Redis instance
# with other apps (e.g. on 127.0.0.1:6379). Default when unset: omniroute:
# quota store + warmup circuit breaker). Prevents key collisions when OmniRoute
# shares a Redis instance with other apps (e.g. on 127.0.0.1:6379). Default when
# unset: omniroute:
# REDIS_KEY_PREFIX=omniroute:
# Host interface docker-compose publishes the Redis sidecar on.
# Default: 127.0.0.1 (loopback only). The compose Redis runs WITHOUT
@@ -124,6 +125,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 +389,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.
@@ -523,6 +541,48 @@ ALLOW_API_KEY_REVEAL=false
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
# OUTBOUND_SSRF_GUARD_ENABLED=true
# ── Self-hosted unified OpenAI-compatible entry (RIC-738, D4) ────────────────────
# When set, /v1/chat/completions diverts to the self-hosted provider adapters
# (open-sse/services/selfHostedEntry.ts) instead of the cloud pipeline. YAML inline
# (example) — or point OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE at a YAML file. Secrets
# are runtime-only, never logged. While ANY of these is set, the entry is active;
# config present but unparseable returns a 500 (never silently falls through).
# OMNIROUTE_SELF_HOSTED_PROVIDERS='
# providers:
# - id: local
# kind: openai
# baseUrl: http://127.0.0.1:11434/v1
# model: llama3
# - id: claude
# kind: anthropic
# baseUrl: http://127.0.0.1:8080
# model: claude-sonnet
# '
# OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE=/etc/omniroute/providers.yaml
# Optional shared API key for the unified entry (D5 reserved). When set, require
# `Authorization: Bearer <key>`; empty = open loopback/trusted-network route.
# OMNIROUTE_SELF_HOSTED_API_KEY=
# ── Deterministic routing strategies (M2 / RIC-740, D3 可审计路由) ─────────────
# Optional `strategy:` block — either inline in the providers document above, or a
# standalone document via these env vars. One rule per line; every decision is
# explainable via the `x-omniroute-route-decision` response header. No ML/predict.
# Malformed strategy config returns a 500 (never silently becomes a no-op).
# Example (inline, same shape as `strategy:` inside the providers YAML):
# OMNIROUTE_SELF_HOSTED_STRATEGY='
# blacklist: []
# whitelist: [cheap, fast, premium]
# costPriority: true
# latencyAware:
# enabled: true
# cooldown:
# consecutiveFailures: 2
# cooldownMs: 30000
# fallbackChain: [cheap, fast, premium]
# '
# OMNIROUTE_SELF_HOSTED_STRATEGY_FILE=/etc/omniroute/strategy.yaml
# See docs/routing/DETERMINISTIC_ROUTING.md for the full strategy surface.
# ═══════════════════════════════════════════════════════════════════════════════
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
# ═══════════════════════════════════════════════════════════════════════════════
@@ -604,6 +664,15 @@ ALLOW_API_KEY_REVEAL=false
# Validated to >= 1, clamped to <= 32. | Default: 3
# COMBO_CONCURRENCY_PER_MODEL=3
# Disable conversation-history tracking (#13150).
# Used by: open-sse/services/conversationTracker.ts. resolveConversationId()
# returns an untracked result before it reads SQLite or parses message history,
# and the switch also covers client-supplied session IDs. Routing sessions are
# unaffected and existing records are not deleted. Use it when the dashboard's
# conversation view is unused and the turn table has grown large.
# Set to 1 to disable. | Default: unset (tracking enabled)
# OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1
# ═══════════════════════════════════════════════════════════════════════════════
# 7. URLS & CLOUD SYNC
# ═══════════════════════════════════════════════════════════════════════════════
@@ -694,14 +763,26 @@ NEXT_PUBLIC_CLOUD_URL=
# OpenCode Go/Zen VPS egress (#5997): on a datacenter VPS, Cloudflare in front of
# opencode.ai/zen/go 403s chat requests that lack OpenCode CLI identity headers.
# When your clients don't already send them, set this to synthesize the CLI headers
# (User-Agent, x-opencode-client, x-opencode-project, fresh request/session UUIDs) on
# absent keys. OFF by default — forward-only is safer when clients already send them.
# (User-Agent, x-opencode-client, x-opencode-project, canonical request/session ids) on
# absent keys. ON by default — a client value always wins, these only fill gaps.
# Values are overridable via OPENCODE_GO_USER_AGENT / OPENCODE_USER_AGENT / OPENCODE_CLIENT /
# OPENCODE_PROJECT (defaults: opencode-cli/1.0.0 / cli / default).
# OPENCODE_PROJECT (defaults: opencode/1.18.31 / desktop / global).
#OPENCODE_SYNTHESIZE_CLI_HEADERS=true
#OPENCODE_USER_AGENT=opencode-cli/1.0.0
#OPENCODE_CLIENT=cli
#OPENCODE_PROJECT=default
#OPENCODE_USER_AGENT=opencode/1.18.31
#OPENCODE_CLIENT=desktop
#OPENCODE_PROJECT=global
# Keyless OpenCode models are answered only when the request declares a non-empty tool
# list, and the upstream inspects which names it carries. OmniRoute reuses the list a
# request of the same conversation was last seen getting through, so a request that
# carries none — a title or a summary — goes out with the list its own client already
# declared. Set to off to stop adjusting request bodies entirely; headers are unaffected.
#OPENCODE_FREE_TIER_REQUEST_CONTRACT=off
# Tool names to declare when nothing has been observed yet for a model, comma-separated.
# Empty falls back to a single placeholder the model is told not to call. Only useful on
# an install where no client sends tools, since there is then nothing to learn from.
#OPENCODE_FREE_TIER_PLACEHOLDER_TOOLS=glob,grep,read
# Ollama Cloud quota scraping. Prefer configuring this per connection in
# Dashboard → Providers → Ollama Cloud. The cookie is sensitive.
@@ -720,6 +801,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
@@ -1017,6 +1104,12 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts.
#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000
# Opt-in minimum output budget (tokens) for reasoning models (#10281 follow-up).
# When set, a caller max_tokens in [256, floor) on a thinking-capable model is
# raised to the floor so reasoning tokens cannot consume the whole budget
# (zero-content finish_reason=length turns). Unset = never enlarge client budgets (#9507).
#OMNIROUTE_REASONING_MIN_BUDGET=4096
# Spend write batcher cadence (ms) and buffer size before forced flush.
# Used by: src/lib/spend/batchWriter.ts. Defaults: 60000 ms / 1000 entries.
#OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000
@@ -1063,6 +1156,11 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
# Character cap for Lite proactive tool-result truncation when lite.maxToolLength
# is unset. Range 256-1000000. Dashboard setting wins over this env.
# Used by: open-sse/services/compression/lite.ts. Default: 2000.
#OMNIROUTE_LITE_MAX_TOOL_LENGTH=2000
# Maximum concurrent synchronous compression workers. Excess jobs wait FIFO.
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 2.
#OMNI_COMPRESSION_WORKERS=2
@@ -1119,10 +1217,38 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
# WAL truncate cadence override (ms). Set to 0 to disable. Default: 21600000 (6h).
# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
# Removed: periodic live wal_checkpoint(TRUNCATE) could SIGBUS the process (issue
# #13973). The variable is inert: a positive value logs a one-time deprecation warning,
# while 0 or unset stays silent. The WAL is kept small
# by the PASSIVE scheduler below and truncated by the shutdown checkpoint.
#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 runs wal_checkpoint(RESTART) so the
# WAL starts over without rewriting the mapped wal-index. Default: 256.
# Used by: src/lib/db/walMaintenance.ts.
#OMNIROUTE_WAL_GUARD_MAX_MB=256
# 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=
# Days a terminal (completed/failed/cancelled/expired) Batch API job's checkpoints,
# referenced files, and row are kept by the automatic cleanup sweep. Default: 30
# (matches OpenAI's own Batch API output retention window). Only takes effect once
# BATCH_AND_FILE_AUTO_CLEANUP_ENABLED is turned on.
# Used by: src/lib/db/cleanup.ts::getBatchRetentionDays().
#OMNIROUTE_BATCH_RETENTION_DAYS=30
# Let the automatic cleanup sweep delete terminal Batch API jobs (and their
# checkpoints) past OMNIROUTE_BATCH_RETENTION_DAYS, and clear the content of
# uploaded files past their own expires_at. Off by default: every existing
# install keeps this data exactly as before until an operator opts in.
# Used by: src/lib/db/cleanup.ts (feature flag; see docs/reference/FEATURE_FLAGS.md).
#BATCH_AND_FILE_AUTO_CLEANUP_ENABLED=false
# 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 +1301,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 +1442,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 +1463,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
@@ -1342,6 +1474,13 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Override the advertised GitHub Copilot CLI version independently of
# GITHUB_USER_AGENT. Used by: open-sse/config/providerHeaderProfiles.ts.
# GITHUB_COPILOT_CLI_VERSION=1.0.82
#
# Pin the `copilot-integration-id` header sent to standard GitHub Copilot,
# overriding the default copilot-developer-cli identity (and disabling the
# automatic 403-identity fallback to copilot-chat). Set this only if your
# Copilot account/org requires a specific integration id. Used by:
# open-sse/config/providerHeaderProfiles.ts, open-sse/executors/copilotIdentityFallback.ts.
# COPILOT_INTEGRATION_ID=copilot-chat
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
# from the Codex Responses stream. These frames break the OpenAI SDK's
@@ -1449,7 +1588,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
#
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
# If set, it becomes the default for FETCH_TIMEOUT_MS, STREAM_IDLE_TIMEOUT_MS,
# and STREAM_READINESS_TIMEOUT_MS.
# and STREAM_READINESS_TIMEOUT_MS. STREAM_ACTIVE_TIMEOUT_MS is independent.
# The fine-grained variables below override their respective defaults only when set.
# ── Global shortcut ──
@@ -1470,6 +1609,18 @@ CURSOR_USER_AGENT="Cursor/3.4"
# # caller's deadline; on expiry the request retries
# # once on a fresh no-keep-alive socket. 0 disables
# # the bound (default: 30000 = 30s).
# OMNIROUTE_DIRECT_RESPONSE_RETRY_TIMEOUT_MS=600000 # Ceiling (ms) for the fresh-socket
# # RETRY attempt above (#13703). Only applies when
# # the caller already attached its own deadline
# # signal (the resolved connection/model/provider/
# # FETCH_TIMEOUT_MS cascade) — that signal is the
# # real bound and fires first in the intended path,
# # so this is a generous backstop rather than a flat
# # cap: without it the retry reused the same short
# # OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS window as the
# # pooled attempt and 504'd healthy slow-TTFB
# # reasoning models. Never allowed below the flat
# # floor above (default: 600000 = 10 min).
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
@@ -1631,6 +1782,8 @@ CURSOR_USER_AGENT="Cursor/3.4"
# ── Stream idle detection ──
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
# # Extended-thinking models rarely pause >90s.
# STREAM_ACTIVE_TIMEOUT_MS=1260000 # Max total active SSE lifetime (default: 21 min = the largest registered model timeoutMs + 1 min; 0 disables)
# # Independent of REQUEST_TIMEOUT_MS and byte activity.
# STREAM_READINESS_TIMEOUT_MS=80000 # Time to receive the first non-ping SSE event
# STREAM_READINESS_MAX_TIMEOUT_MS=180000 # Cap for adaptive first-event extensions
# # (large/tool-heavy/high-reasoning requests).
@@ -1646,6 +1799,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 +1932,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 +1956,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 +2105,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 +2250,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 +2347,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
@@ -2280,6 +2465,10 @@ APP_LOG_TO_FILE=true
# Cursor stream idle timeout (ms). Default: 300000 (5 min).
# Used by: open-sse/executors/cursor.ts.
# CURSOR_STREAM_TIMEOUT_MS=300000
# Grace window (ms) after a composer kv_after_text soft terminator when bytes remain
# buffered — gives a trailing exec_mcp tool call time to complete its frame. 2s covers
# every exec_mcp-behind-kv ordering observed live.
# CURSOR_KV_GRACE_MS=2000
# Cursor tool-commit directive toggle. Default-on: when a request declares
# tools, a directive is prepended so composer-2.5 reliably issues tool calls
@@ -2287,6 +2476,22 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/executors/cursor.ts.
# CURSOR_TOOL_DIRECTIVE=1
# Operator-defined system prompt text appended to the system message AFTER
# translation (post-translation injection), so it reaches codex/Responses and
# /v1/messages paths. Also used as the directive prefix stripped from echoed
# system preamble blocks. Leave unset to disable.
# Used by: open-sse/translator/request/claude-to-openai.ts, open-sse/translator/response/openai-to-claude.ts.
# OMNIROUTE_SYSTEM_INSTRUCTION_APPEND=
# Set to "1" to also strip echoed system-prompt PREAMBLE blocks
# (<analysis>/<system-reminder>/<summary> blocks, prose reproductions of the skill
# section) from the start of an openai->claude stream. OFF by default: it recognises
# constructs by English-prose heuristics and DOES mutate the response payload, so a
# reply that genuinely opens with such a section would lose it. Turn it on only when
# you actually hit the system-echo leak.
# Used by: open-sse/translator/response/openai-to-claude.ts, open-sse/utils/directivePreambleStripper.ts.
# OMNIROUTE_STRIP_SYSTEM_PREAMBLE=0
# Per-image fetch timeout (ms) for remote image_url vision input. Default: 15000.
# Used by: open-sse/utils/cursorImages.ts.
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
@@ -2519,6 +2724,16 @@ APP_LOG_TO_FILE=true
# When enabled, the node authenticates with the API key stored on its connection.
# AUDIO_REMOTE_PROVIDER_NODES=false
# Used by: src/app/api/v1/_shared/rerankProviderNodes.ts — lets POST /v1/rerank (and
# the memory engine's loopback rerank step) use an OpenAI-compatible provider node
# hosted outside localhost, e.g. a LAN box or Tailscale peer running TEI/Infinity/vLLM.
# OFF by default: routing to a remote host changes egress identity, so it must be an
# explicit operator decision. Loopback/private nodes (localhost, 127.0.0.1,
# 172.16-31.x) are always allowed and unaffected by this flag. Remote nodes must also
# pass the provider outbound URL policy (see OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS);
# cloud-metadata hosts are never routed to.
# RERANK_REMOTE_PROVIDER_NODES=false
# ── Free Proxy Pool (auto-sync scheduler) ──
# Background refresh of the free-proxy pool. Opt-in, OFF by default (parallels
# Hard Rule #20's default-off posture for data-mutating background features).
@@ -2671,6 +2886,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 +2943,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 +3188,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 +3223,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 +3304,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

41
.env.selfhost.example Normal file
View File

@@ -0,0 +1,41 @@
# ──────────────────────────────────────────────────────────────────────
# OmniRoute — Self-Host env (minimal, zero-fee self-host)
# ──────────────────────────────────────────────────────────────────────
# cp .env.selfhost.example .env
# Edit only the two lines marked `# EDIT ME`. Everything else has a sane
# default. No secrets are baked in — OmniRoute never ships credentials.
#
# Full variable reference: docs/guides/DOCKER_GUIDE.md and .env.example
# ──────────────────────────────────────────────────────────────────────
# ── Ports (host-side) ──────────────────────────────────────────────────
# Dashboard + API + Live-WS. Already match the image defaults.
DASHBOARD_PORT=20128
API_PORT=20129
LIVE_WS_PORT=20132
# ── Bind address ───────────────────────────────────────────────────────
# 127.0.0.1 = loopback only (safe with REQUIRE_API_KEY=false, the default).
# Set to 0.0.0.0 ONLY when REQUIRE_API_KEY=true OR a reverse proxy
# enforces auth upstream. Exposing an unauthenticated /v1 proxy on the
# LAN/WAN lets anyone burn your provider quotas. # EDIT ME if you must.
APP_BIND_HOST=127.0.0.1
# ── Auth ──────────────────────────────────────────────────────────────
# false = the dashboard and /v1 proxy are open to APP_BIND_HOST's network.
# true = every request needs an API key / dashboard login. The dashboard
# auto-creates INITIAL_PASSWORD on first boot (read it from the logs:
# `docker logs omniroute | grep -i password`). # EDIT ME — set true.
REQUIRE_API_KEY=false
# INITIAL_PASSWORD= # uncomment to pre-seed the dashboard password
# ── Memory ceiling (V8 old-space) ──────────────────────────────────────
# 1024 = dashboard + light chat. Coding agents (long POST /v1/responses
# bodies) need more — see SELF_HOST_GUIDE.md "sizing". 2048 is a safe
# default for a single user who runs Claude Code / Codex through it.
OMNIROUTE_MEMORY_MB=2048
# ── Browser-facing origin (optional) ───────────────────────────────────
# Set ONLY if you expose OmniRoute behind a domain via a reverse proxy.
# NEXT_PUBLIC_BASE_URL=https://your-domain.example.com
# BASE_URL=http://omniroute:20128

View File

@@ -1,6 +1,6 @@
name: Bug Report
description: Report a bug or unexpected behavior in OmniRoute
title: "[BUG] "
title: "fix(): "
labels: ["bug"]
body:
- type: markdown
@@ -8,6 +8,8 @@ body:
value: |
Thanks for taking the time to report a bug. Please fill out the sections below so we can reproduce and fix the issue.
The title is prefilled as `fix(): ` to match the [Conventional Commits](https://github.com/diegosouzapw/OmniRoute/blob/main/CONTRIBUTING.md#commit-messages) convention — pick a scope from the list documented there (e.g. `providers`, `resilience`, `dashboard`, `api`).
- type: input
id: version
attributes:

View File

@@ -1,6 +1,6 @@
name: Feature Request
description: Suggest a new feature or improvement for OmniRoute
title: "[Feature] "
title: "feat(): "
labels: ["enhancement"]
body:
- type: markdown
@@ -8,6 +8,8 @@ body:
value: |
Thanks for suggesting a feature! Please describe the problem you're trying to solve and how you'd like it to work.
The title is prefilled as `feat(): ` to match the [Conventional Commits](https://github.com/diegosouzapw/OmniRoute/blob/main/CONTRIBUTING.md#commit-messages) convention — pick a scope from the list documented there (e.g. `providers`, `resilience`, `dashboard`, `api`).
- type: textarea
id: problem
attributes:

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.
@@ -446,8 +453,11 @@ jobs:
# One FS inventory of src/app/api for both anti-hallucination directions.
- name: API docs refs (openapi + prose → routes)
run: npm run check:api-docs-refs
- name: i18n translation drift (warn)
run: node scripts/i18n/check-translation-drift.mjs --warn
# Blocking since the 2026-09 docs re-sync: a core doc edited without `npm run i18n:run
# --files=<doc>` (or `--adopt` for a mechanical edit) leaves 65 stale mirrors behind;
# the run only retranslates the `## ` sections whose text changed, so it is cheap.
- name: i18n docs drift (sources changed since their translation)
run: node scripts/i18n/check-translation-drift.mjs
docs-lint:
name: Docs Lint (prose — advisory)
@@ -504,9 +514,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 +526,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

@@ -22,7 +22,7 @@ jobs:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
- uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
with:
languages: javascript-typescript
queries: security-extended

View File

@@ -411,16 +411,12 @@ jobs:
path: /tmp/digests/bun-web
merge-multiple: true
- name: Create Docker Hub manifest
- name: Create Docker Hub version manifests
run: |
set -euo pipefail
create_manifest() {
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
local tags=(-t "${image}:${VERSION}${suffix}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${image}:latest${suffix}")
fi
local refs=()
while IFS= read -r digest_file; do
refs+=("${image}@sha256:$(basename "$digest_file")")
@@ -433,7 +429,7 @@ jobs:
echo "No image digests in $dir" >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
docker buildx imagetools create -t "${image}:${VERSION}${suffix}" "${refs[@]}"
}
create_manifest "${IMAGE_NAME}" "" /tmp/digests/base
@@ -441,16 +437,12 @@ jobs:
create_manifest "${IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
create_manifest "${IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
- name: Create GHCR manifest
- name: Create GHCR version manifests
run: |
set -euo pipefail
create_manifest() {
local image="$1" suffix="$2" dir="$3" optional="${4:-}"
local tags=(-t "${image}:${VERSION}${suffix}")
if [ "$PROMOTE_LATEST" = "true" ]; then
tags+=(-t "${image}:latest${suffix}")
fi
local refs=()
while IFS= read -r digest_file; do
refs+=("${image}@sha256:$(basename "$digest_file")")
@@ -463,7 +455,7 @@ jobs:
echo "No image digests in $dir" >&2
exit 1
fi
docker buildx imagetools create "${tags[@]}" "${refs[@]}"
docker buildx imagetools create -t "${image}:${VERSION}${suffix}" "${refs[@]}"
}
create_manifest "${GHCR_IMAGE_NAME}" "" /tmp/digests/base
@@ -471,6 +463,59 @@ jobs:
create_manifest "${GHCR_IMAGE_NAME}" "-bun" /tmp/digests/bun-base optional
create_manifest "${GHCR_IMAGE_NAME}" "-web-bun" /tmp/digests/bun-web optional
- name: Smoke-test published Docker image
if: needs.prepare.outputs.version != 'main'
run: |
set -euo pipefail
container="omniroute-smoke-${VERSION//[^a-zA-Z0-9_.-]/-}"
trap 'docker rm -f "$container" >/dev/null 2>&1 || true' EXIT
docker run --detach --name "$container" "${IMAGE_NAME}:${VERSION}"
for attempt in $(seq 1 30); do
status="$(docker inspect --format '{{.State.Health.Status}}' "$container")"
if [ "$status" = "healthy" ]; then
exit 0
fi
if [ "$status" = "unhealthy" ] || [ "$(docker inspect --format '{{.State.Status}}' "$container")" = "exited" ]; then
docker logs "$container"
exit 1
fi
sleep 2
done
docker logs "$container"
exit 1
- name: Promote Docker Hub latest tags
if: needs.prepare.outputs.promote_latest == 'true'
run: |
set -euo pipefail
promote_tag() {
local suffix="$1"
docker buildx imagetools create -t "${IMAGE_NAME}:latest${suffix}" "${IMAGE_NAME}:${VERSION}${suffix}"
}
promote_tag ""
promote_tag "-web"
for suffix in -bun -web-bun; do
if docker buildx imagetools inspect "${IMAGE_NAME}:${VERSION}${suffix}" >/dev/null 2>&1; then
promote_tag "$suffix"
fi
done
- name: Promote GHCR latest tags
if: needs.prepare.outputs.promote_latest == 'true'
run: |
set -euo pipefail
promote_tag() {
local suffix="$1"
docker buildx imagetools create -t "${GHCR_IMAGE_NAME}:latest${suffix}" "${GHCR_IMAGE_NAME}:${VERSION}${suffix}"
}
promote_tag ""
promote_tag "-web"
for suffix in -bun -web-bun; do
if docker buildx imagetools inspect "${GHCR_IMAGE_NAME}:${VERSION}${suffix}" >/dev/null 2>&1; then
promote_tag "$suffix"
fi
done
- name: Inspect image
if: needs.prepare.outputs.version != 'main'
run: |

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

@@ -0,0 +1,42 @@
name: Release acceptance
on:
push:
branches: ["release/v*"]
workflow_dispatch:
permissions:
contents: read
concurrency:
group: release-acceptance-${{ github.ref }}
cancel-in-progress: false
jobs:
acceptance:
name: Release acceptance
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
persist-credentials: false
- uses: actions/setup-node@v5
with:
node-version: "22"
cache: npm
- run: npm ci
- name: Emit shadow acceptance report
run: |
node scripts/quality/validate-release-acceptance.mjs \
--plan tests/fixtures/release-acceptance/plan-lint.json \
--manifests tests/fixtures/release-acceptance/shadow-manifests \
--out release-acceptance-report.json
continue-on-error: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: release-acceptance-report
path: release-acceptance-report.json
if-no-files-found: ignore
retention-days: 30

1
.gitignore vendored
View File

@@ -74,6 +74,7 @@ yarn-error.log*
# Local gitleaks artifacts (do not commit)
gitleaks-local.json
!.env.example
!.env.selfhost.example
!.env.homolog.example
!.env.devin-bridge.example
# Provider API keys (never commit)

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

@@ -1,3 +1,7 @@
# wasm-bindgen glue + embedded WASM_BASE64. Prettier rewrites the generated JS
# (quotes, wrapping) on any touch of this file; format tinycmsDomMocks.ts instead.
open-sse/executors/tinycmsSigner.ts
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md

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
@@ -5498,9 +5556,17 @@ export function createOmniRouteConfigHook(
if (modelsFetchThrew && wantDiskCache && !warmSnapshot) {
const snapshot = await diskSnapshotReader(resolved.providerId, snapshotFingerprint);
if (snapshot && snapshot.rawModels.length > 0) {
// Report snapshot age like the warm-startup path already does:
// "stale" alone reads as a transient blip, so a week-old catalog
// is indistinguishable from a five-minute-old one.
const snapshotAge = snapshot.writtenAt;
const snapshotAgeLabel =
typeof snapshotAge === "number"
? `${Math.round((Date.now() - snapshotAge) / 3_600_000)}h`
: "unknown";
logAt(
"warn",
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models)`
`config shim: /v1/models unreachable; using stale disk cache (${snapshot.rawModels.length} models, age ${snapshotAgeLabel})`
);
localRawModels = snapshot.rawModels;
localRawCombos = snapshot.rawCombos;

View File

@@ -481,10 +481,7 @@ test("config: combos fetcher throws → emit models-only catalog (no combos in m
];
assert.ok(entry);
const ids = Object.keys(entry.models).sort();
assert.deepEqual(ids, [
"claude-sonnet-4-6",
"gemini-3-flash",
]);
assert.deepEqual(ids, ["claude-sonnet-4-6", "gemini-3-flash"]);
assert.equal(entry.models["claude-tier"], undefined, "no combo entry");
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/combos fetch failed")),
@@ -1041,11 +1038,7 @@ test("config: features.enrichment=false skips enrichment fetch + keeps raw-id na
];
assert.ok(entry);
assert.equal(enrichmentFetcher.callCount(), 0, "enrichment fetch suppressed by feature flag");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
});
test("config: enrichment fetcher throws → soft-fail (warn + raw-id static catalog)", async () => {
@@ -1068,11 +1061,7 @@ test("config: enrichment fetcher throws → soft-fail (warn + raw-id static cata
"opencode-omniroute"
];
assert.ok(entry, "static block still published on enrichment failure");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id retained"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "claude-sonnet-4-6", "raw id retained");
assert.equal(enrichmentFetcher.callCount(), 1);
assert.ok(
logger.entries.some((e) => String(e[0]).includes("/api/pricing/models fetch failed")),
@@ -1270,10 +1259,7 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
const entry = (input as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.ok(
entry.models["claude-sonnet-4-6"],
"stale snapshot hydrated into static block"
);
assert.ok(entry.models["claude-sonnet-4-6"], "stale snapshot hydrated into static block");
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"Claude Sonnet 4.6 (cached)",
@@ -1281,14 +1267,95 @@ test("config: diskCache hydrates stale snapshot when /v1/models throws", async (
);
assert.equal(writes, 0, "disk write skipped when live fetch failed");
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("using stale disk cache") ||
String(e[0]).includes("warm startup from disk snapshot")
logger.entries.some(
(e) =>
String(e[0]).includes("using stale disk cache") ||
String(e[0]).includes("warm startup from disk snapshot")
),
"disk-cache hydration breadcrumb emitted"
);
});
// The stale-fallback branch (`modelsFetchThrew && wantDiskCache && !warmSnapshot`)
// only runs when the warm-startup read found nothing — a snapshot can appear on
// disk between that first read and the live fetch failing (e.g. another OC
// process instance wrote one concurrently). A stateful reader simulates that:
// empty on the warm-startup read, populated by the time the fallback re-reads.
function emptyThenSnapshotReader(
snapshot: Omit<
Awaited<ReturnType<typeof import("../src/index.js").defaultDiskSnapshotReader>> & object,
never
>
): typeof import("../src/index.js").defaultDiskSnapshotReader {
let calls = 0;
return (async () => {
calls++;
return calls === 1 ? undefined : snapshot;
}) as typeof import("../src/index.js").defaultDiskSnapshotReader;
}
test("config: stale-fallback warning reports the disk snapshot age in hours", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const writtenAt = Date.now() - 2 * 3_600_000; // 2h old
const diskSnapshotReader = emptyThenSnapshotReader({
rawModels: [MODEL_CLAUDE],
rawCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
writtenAt,
});
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { diskCache: true } },
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
);
await hook(makeInput());
assert.ok(
logger.entries.some((e) => String(e[0]).includes("using stale disk cache (1 models, age 2h)")),
"stale-fallback warning includes the computed snapshot age"
);
});
test('config: stale-fallback warning falls back to "unknown" age without writtenAt', async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-test", baseURL: "https://or.example/v1" },
});
const fetcher = throwingModelsFetcher();
const combosFetcher = stubCombosFetcher([]);
const logger = captureWarn();
const diskSnapshotReader = emptyThenSnapshotReader({
rawModels: [MODEL_CLAUDE],
rawCombos: [],
rawEnrichment: new Map(),
rawCompressionCombos: [],
rawConnections: [],
});
const hook = createOmniRouteConfigHook(
{ providerId: "omniroute", features: { diskCache: true } },
{ readAuthJson, fetcher, combosFetcher, diskSnapshotReader, logger }
);
await hook(makeInput());
assert.ok(
logger.entries.some((e) =>
String(e[0]).includes("using stale disk cache (1 models, age unknown)")
),
'stale-fallback warning falls back to "unknown" when writtenAt is absent'
);
});
test("config: cached rawEnrichment from earlier provider hook is reused (no refetch)", async () => {
const readAuthJson = stubReadAuthJson({
"opencode-omniroute": { type: "api", key: "sk-shared", baseURL: "https://or.example/v1" },
@@ -1376,10 +1443,7 @@ test("config: providerTag (default-on) prepends '<provider> - ' to enriched raw-
"opencode-omniroute"
];
assert.ok(entry);
assert.equal(
entry.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entry.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
assert.equal(entry.models["gemini-3-flash"].name, "Gemini - Gemini 3 Flash");
// Combos stay untouched — `Combo: ` prefix already conveys multi-upstream.
assert.equal(entry.models["claude-tier"].name, "Claude Tier");
@@ -1495,10 +1559,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const entryA = (inputA as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryA.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entryA.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
// Second invocation (cache hit) — name must still be single-suffixed.
const inputB = makeInput();
@@ -1506,10 +1567,7 @@ test("config: providerTag is idempotent — second hook call doesn't double-suff
const entryB = (inputB as { provider: Record<string, OmniRouteStaticProviderEntry> }).provider[
"opencode-omniroute"
];
assert.equal(
entryB.models["claude-sonnet-4-6"].name,
"Claude - Claude Sonnet 4.6"
);
assert.equal(entryB.models["claude-sonnet-4-6"].name, "Claude - Claude Sonnet 4.6");
});
// ────────────────────────────────────────────────────────────────────────────

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 (171 migrations) |
| Database | `src/lib/db/` | SQLite domain modules (178 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:
@@ -647,7 +664,7 @@ focused checks, and use a Conventional Commit message (for example, `docs: slim
## Environment
- **Runtime**: Node.js ≥22.22.2 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.4.0` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.4.2` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
- **Default port**: 20128 (API + dashboard on same port)

File diff suppressed because it is too large Load Diff

View File

@@ -106,7 +106,8 @@ RUN test -f package-lock.json \
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& (cd node_modules/better-sqlite3 \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --force_build=1) \
&& test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node \
&& node -e "require('better-sqlite3')(':memory:').close()" \
&& node -e "const wreq=require('wreq-js'); if(typeof wreq.createTransport!=='function') process.exit(1)"
@@ -225,7 +226,19 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
# Data directory inside Docker — must match the volume mount in docker-compose.yml
ENV DATA_DIR=/app/data
RUN mkdir -p /app/data
RUN mkdir -p /app/data && chown node:node /app /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,
@@ -235,23 +248,24 @@ RUN mkdir -p /app/data
# The old per-module overrides were therefore pure duplication and were removed
# (build-output-isolation cleanup). See scripts/build/assembleStandalone.mjs
# (EXTRA_MODULE_ENTRIES) for the single source of truth.
COPY --from=builder /app/.build/next/standalone ./
COPY --chown=node:node --from=builder /app/.build/next/standalone ./
# better-sqlite3 is the one exception still copied explicitly: assembleStandalone
# only syncs its native build/ dir; the JS wrapper (lib/, package.json) is left to
# Next.js tracing. bootstrap-env requires SQLite BEFORE the standalone server
# starts, so guarantee the complete package independent of trace behaviour.
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
COPY --chown=node:node --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
RUN test -f /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node
# migrations land at <standalone>/migrations via assembleStandalone; point the runtime at them.
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
# Docker healthcheck script — not traced by Next.js standalone output, so copy
# it explicitly. The HEALTHCHECK CMD references it as `node healthcheck.mjs`.
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
COPY --chown=node:node --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
# runtime process never holds root privileges. The chown happens after all
# COPYs so it covers files originally owned by root in the builder stage.
RUN chown -R node:node /app
# Every COPY above hands its files to the baked-in `node` non-root user
# (UID/GID 1000) at copy time. Do NOT add a `RUN chown -R node:node /app`
# afterwards: in the overlay filesystem changing ownership rewrites every file
# into a new layer, which stored the ~2 GB standalone build twice (#13990).
EXPOSE 20128
@@ -340,7 +354,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

@@ -1,5 +1,5 @@
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
FROM oven/bun:1.4.0-slim AS base
FROM oven/bun:1.4.2-slim AS base
WORKDIR /app
RUN apt-get update \
@@ -56,7 +56,7 @@ ENV NODE_ENV=production
RUN bun run --quiet build
# ── Runner Base stage (100% Bun Native Production Runtime) ──────────────────
FROM oven/bun:1.4.0-slim AS runner-base
FROM oven/bun:1.4.2-slim AS runner-base
LABEL org.opencontainers.image.title="omniroute" \
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \

View File

@@ -7,19 +7,19 @@
# 🚀 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.62B free tokens/mo · 19 routing strategies · $0 to start."/>
</div>
<div align="center">
## 💰 ~1.47B Free Tokens / Month
## 💰 ~1.62B Free Tokens / Month
</div>
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **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 **489 free-tier entries across 35 recurring pool keys** and computes the token headline from the **17 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.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 35 documented recurring pool keys covering 489 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 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, xKiro 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)**.
>
@@ -63,7 +63,7 @@
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
| ------------------------- | :-----: | :-----------------------: | :---------: |
| 🌐 Providers | 290 | **352** | more queued |
| 🌐 Providers | 290 | **357** | more queued |
| 🧠 Unique chat model IDs | 1185 | **1312** | — |
| 🖼️ Modality Bridge | — | 🆕 vision + audio + video | — |
| 📡 Radar free catalog | — | 🆕 opt-in | — |
@@ -101,7 +101,7 @@
<tr>
<td align="right"><b>⚙️ Features</b></td>
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
<td align="center"><a href="#-352-ai-providers--154-catalog-marked-free">🌐 Providers</a></td>
<td align="center"><a href="#-357-ai-providers--152-catalog-marked-free">🌐 Providers</a></td>
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI &amp; MCP</a></td>
</tr>
<tr>
@@ -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 54 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>
@@ -518,9 +542,9 @@ Pix copia-e-cola:
## 📡 OmniRoute Radar
The main free-tier headline remains **~1.47B tokens/month** from the documented,
The main free-tier headline remains **~1.62B 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.22B**. 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.
@@ -605,13 +629,13 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
<td align="center" width="76"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/goose.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/goose.svg" width="40" alt="Goose"/></picture><br/><sub><b>Goose</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Open Interpreter"/><br/><sub><b>Open Interpreter</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Warp AI"/><br/><sub><b>Warp AI</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Agent Deck"/><br/><sub><b>Agent Deck</b></sub><br/><sub>                           </sub></td>
<td align="center" width="76"><a href="https://deyin.ai"><img src="./public/deyin.svg" width="40" alt="deyin.ai"/><br/><sub><b>deyin.ai</b></sub><br/><sub>                           </sub></a></td>
</tr>
</table>
</div>
<div align="center">
<b> also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
<b> also works with</b> · Agent Deck · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
</div>
<sub>📖 Per-tool setup for all 36 tools (26 CLI Code's + 10 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
@@ -644,11 +668,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
<div align="center">
## 🌐 352 AI Providers — 152 Catalog-Marked Free
## 🌐 357 AI Providers — 152 Catalog-Marked Free
</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).
> **357 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 **491 per-model rows**, **35 recurring pools** and **54 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">
@@ -701,7 +725,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
<td align="center" width="150"><img src="./public/providers/pollinations.svg" width="42" alt="Pollinations"/><br/><b>Pollinations</b><br/><sub>GPT, Llama, Claude<br/>No key needed</sub></td>
<td align="center" width="150"><img src="./public/providers/cloudflare.svg" width="42" alt="Cloudflare AI"/><br/><b>Cloudflare AI</b><br/><sub>50+ models<br/>10K neurons/day</sub></td>
<td align="center" width="150"><img src="./public/providers/nvidia.svg" width="42" alt="NVIDIA NIM"/><br/><b>NVIDIA NIM</b><br/><sub>GLM, MiniMax<br/>~40 RPM free</sub></td>
<td align="center" width="150"><img src="./public/providers/cerebras.svg" width="42" alt="Cerebras"/><br/><b>Cerebras</b><br/><sub>GLM 4.7, GPT-OSS<br/>1M tokens/day</sub></td>
<td align="center" width="150"><img src="./public/openference.svg" width="42" alt="Openference"/><br/><b>Openference</b><br/><sub>Qwen3.8 27B, Llama 3.2<br/>Free tier</sub></td>
<td align="center" width="150"><img src="./public/providers/openrouter.svg" width="42" alt="OpenRouter"/><br/><b>OpenRouter</b><br/><sub>:free models<br/>+$10 → higher RPM</sub></td>
</tr>
</table>
@@ -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, 171 migrations</td></tr>
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 178 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: 35 documented recurring pools / 489 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

@@ -218,6 +218,8 @@ These rules are enforced by tooling and reviewers:
## Supply-chain scanner findings (Socket.dev / Snyk / similar)
> **Scope note:** `socket.yml` at the repository root only shapes `projectIgnorePaths` for Socket.dev's registry-side post-publish scan of the published npm artifact — it is not an enforced CI/PR merge gate. No workflow in `.github/workflows`, no `package.json` script, and no `Makefile` target invokes Socket.dev.
The published `omniroute` npm artifact bundles the Next.js `output: "standalone"`
build, which means every route handler — including documented privileged
features (MITM, Zed import, Cloud Sync, embedded service supervisor) — ends

582
bin/antigravity-bridge.mjs Executable file
View File

@@ -0,0 +1,582 @@
#!/usr/bin/env node
/**
* OmniRoute Antigravity Bridge Proxy
*
* Intercepts Antigravity CLI and IDE requests:
* - Directs Gemini 3.8 models directly to Google backend (100% native, untouched).
* - Directs other models (Claude Sonnet 4.5/4.6, Opus, Gemini 3.7, GPT-OSS, etc.) to OmniRoute /v1/antigravity.
* - Passes all non-model Google requests (auth, onboarding, telemetry) directly to Google backend.
* - Transparently forwards all other non-target internet traffic.
*/
import net from "node:net";
import http from "node:http";
import https from "node:https";
import tls from "node:tls";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const PORT = parseInt(process.env.BRIDGE_PORT || "20129", 10);
const ROUTER_URL = process.env.ROUTER_URL || "http://127.0.0.1:20128/v1/antigravity";
const ROUTER_API_KEY =
process.env.ROUTER_API_KEY || process.env.OMNIROUTE_API_KEY || "sk-omniroute-bridge-local";
// Connection pool agents with TCP keep-alive
const httpAgent = new http.Agent({
keepAlive: true,
keepAliveMsecs: 60000,
maxSockets: 64,
maxFreeSockets: 16,
timeout: 120000,
});
const httpsAgent = new https.Agent({
keepAlive: true,
keepAliveMsecs: 60000,
maxSockets: 64,
maxFreeSockets: 16,
timeout: 120000,
});
let cachedSslOptions = null;
function getSslOptions() {
if (cachedSslOptions) return cachedSslOptions;
const certDir =
process.env.CERT_DIR || path.join(process.env.HOME || process.cwd(), ".omniroute", "mitm");
const serverKey = path.join(certDir, "server.key");
const serverCrt = path.join(certDir, "server.crt");
if (!fs.existsSync(serverKey) || !fs.existsSync(serverCrt)) {
console.error("❌ Certificate files not found in", certDir);
process.exit(1);
}
cachedSslOptions = {
key: fs.readFileSync(serverKey),
cert: fs.readFileSync(serverCrt),
};
return cachedSslOptions;
}
const TARGET_HOSTS = new Set([
"cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.googleapis.com",
"daily-cloudcode-pa.sandbox.googleapis.com",
"autopush-cloudcode-pa.sandbox.googleapis.com",
"preprod-daily-cloudcode-pa.sandbox.googleapis.com",
"antigravity-unleash.goog",
]);
function isGenerationRequest(url) {
if (!url) return false;
return (
url.includes(":generateContent") ||
url.includes(":streamGenerateContent") ||
url.includes("/GenerateChat") ||
url.includes("/StreamGenerateChat") ||
url.includes("/GenerateCode") ||
url.includes("/CompleteCode")
);
}
function extractModel(body, url) {
if (body && typeof body === "object") {
if (typeof body.model === "string" && body.model) return body.model;
if (body.request && typeof body.request.model === "string" && body.request.model) {
return body.request.model;
}
}
if (url) {
try {
const parsed = new URL(url, "https://cloudcode-pa.googleapis.com");
const m = parsed.searchParams.get("model");
if (m) return m;
} catch {}
}
return null;
}
const MODEL_ROUTING_MAP = {
// Official OmniRoute Auto Groups
"auto/best-fast": "groq/openai/gpt-oss-120b",
"auto/best-coding": "mistral/codestral-latest",
"auto/best-reasoning": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"auto/best-free": "groq/qwen/qwen3.8-27b",
"auto/best-vision": "nvidia/meta/llama-3.2-90b-vision-instruct",
"auto/coding:pro": "mistral/codestral-latest",
"auto/coding:fast": "groq/openai/gpt-oss-120b",
"auto/coding:free": "groq/qwen/qwen3.8-27b",
"auto/coding:reliable": "mistral/codestral-latest",
"auto/reasoning:pro": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"auto/smart": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"auto/claude-sonnet": "mistral/codestral-latest",
"auto/claude-opus": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"auto/gemini": "gemini/gemini-2.5-flash",
"auto/llama": "groq/openai/gpt-oss-120b",
"auto/gemma": "groq/qwen/qwen3.8-27b",
// Human-readable Display Names (in case CLI sends displayName in envelope)
"Auto: Best Fast (OmniRoute)": "groq/openai/gpt-oss-120b",
"Auto: Best Coding (OmniRoute)": "mistral/codestral-latest",
"Auto: Best Reasoning (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"Auto: Best Free (OmniRoute)": "groq/qwen/qwen3.8-27b",
"Auto: Best Vision (OmniRoute)": "nvidia/meta/llama-3.2-90b-vision-instruct",
"Auto: Coding Pro (OmniRoute)": "mistral/codestral-latest",
"Auto: Coding Fast (OmniRoute)": "groq/openai/gpt-oss-120b",
"Auto: Coding Free (OmniRoute)": "groq/qwen/qwen3.8-27b",
"Auto: Coding Reliable (OmniRoute)": "mistral/codestral-latest",
"Auto: Reasoning Pro (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"Auto: Smart (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"Auto: Claude Sonnet (OmniRoute)": "mistral/codestral-latest",
"Auto: Claude Opus (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
"Auto: Gemini (OmniRoute)": "gemini/gemini-2.5-flash",
"Auto: Llama (OmniRoute)": "groq/openai/gpt-oss-120b",
"Auto: Gemma (OmniRoute)": "groq/qwen/qwen3.8-27b",
// Fail-safe self-healing for dead/retired models
"nvidia/deepseek-ai/deepseek-v4-pro-0813": "groq/openai/gpt-oss-120b",
"deepseek-ai/deepseek-v4-pro-0813": "groq/openai/gpt-oss-120b",
"NVIDIA: DeepSeek V4 Pro": "groq/openai/gpt-oss-120b",
"nvidia/openai/gpt-oss-120b": "groq/openai/gpt-oss-120b",
"openai/gpt-oss-120b": "groq/openai/gpt-oss-120b",
"groq/llama-3.3-70b-versatile": "groq/openai/gpt-oss-120b",
"llama-3.3-70b-versatile": "groq/openai/gpt-oss-120b",
};
function resolveTargetModel(model) {
if (!model) return "groq/openai/gpt-oss-120b";
if (MODEL_ROUTING_MAP[model]) return MODEL_ROUTING_MAP[model];
const clean = model.replace(/^models\//, "").trim();
if (MODEL_ROUTING_MAP[clean]) return MODEL_ROUTING_MAP[clean];
for (const [k, v] of Object.entries(MODEL_ROUTING_MAP)) {
if (k.toLowerCase() === model.toLowerCase() || k.toLowerCase() === clean.toLowerCase()) {
return v;
}
}
if (
clean.includes("deepseek-v4-pro") ||
(clean.startsWith("nvidia") && clean.includes("gpt-oss-120b")) ||
clean.includes("llama-3.3-70b-versatile")
) {
return "groq/openai/gpt-oss-120b";
}
return clean;
}
const OMNIROUTE_BUILTIN_GROUPS = [
{
id: "auto/best-coding",
displayName: "Auto: Best Coding (OmniRoute)",
descriptionText:
"OmniRoute dynamic routing to the highest benchmark coding model available (Mistral Codestral)",
},
{
id: "auto/best-reasoning",
displayName: "Auto: Best Reasoning (OmniRoute)",
descriptionText:
"OmniRoute dynamic routing to the highest benchmark reasoning model available (Nemotron 3 Super 120B)",
},
{
id: "auto/best-fast",
displayName: "Auto: Best Fast (OmniRoute)",
descriptionText: "OmniRoute sub-second lowest latency high-throughput model (Groq LPUs)",
},
{
id: "auto/best-vision",
displayName: "Auto: Best Vision (OmniRoute)",
descriptionText: "OmniRoute multimodal & computer vision routing",
},
{
id: "auto/best-free",
displayName: "Auto: Best Free (OmniRoute)",
descriptionText: "OmniRoute 100% unmetered free tier model routing (Qwen 3.8 27B)",
},
{
id: "auto/coding:pro",
displayName: "Auto: Coding Pro (OmniRoute)",
descriptionText: "OmniRoute frontier pro-tier coding model (Codestral)",
},
{
id: "auto/coding:fast",
displayName: "Auto: Coding Fast (OmniRoute)",
descriptionText: "OmniRoute fast sub-second daily coding model (Groq 120B)",
},
{
id: "auto/coding:free",
displayName: "Auto: Coding Free (OmniRoute)",
descriptionText: "OmniRoute zero-cost free coding model",
},
{
id: "auto/coding:reliable",
displayName: "Auto: Coding Reliable (OmniRoute)",
descriptionText: "OmniRoute maximum uptime and reliability coding model",
},
{
id: "auto/reasoning:pro",
displayName: "Auto: Reasoning Pro (OmniRoute)",
descriptionText: "OmniRoute deep reasoning frontier model",
},
{
id: "auto/smart",
displayName: "Auto: Smart (OmniRoute)",
descriptionText: "OmniRoute highest intelligence general-purpose model",
},
{
id: "auto/claude-sonnet",
displayName: "Auto: Claude Sonnet (OmniRoute)",
descriptionText: "OmniRoute automated routing across Claude Sonnet providers",
},
{
id: "auto/claude-opus",
displayName: "Auto: Claude Opus (OmniRoute)",
descriptionText: "OmniRoute automated routing across Claude Opus providers",
},
{
id: "auto/gemini",
displayName: "Auto: Gemini (OmniRoute)",
descriptionText: "OmniRoute automated routing across Gemini providers",
},
{
id: "auto/llama",
displayName: "Auto: Llama (OmniRoute)",
descriptionText: "OmniRoute automated routing across Llama providers",
},
{
id: "auto/gemma",
displayName: "Auto: Gemma (OmniRoute)",
descriptionText: "OmniRoute automated routing across Gemma providers",
},
// Active, verified provider models
{
id: "groq/openai/gpt-oss-120b",
displayName: "Groq: GPT-OSS 120B (Ultra-Fast 0.02s)",
descriptionText: "Ultra-fast inference on Groq LPUs at sub-second speeds",
},
{
id: "groq/qwen/qwen3.8-27b",
displayName: "Groq: Qwen 3.8 27B",
descriptionText: "High-speed Qwen 3.8 27B model on Groq",
},
{
id: "mistral/codestral-latest",
displayName: "Mistral: Codestral Latest",
descriptionText: "Mistral flagship frontier code reasoning model",
},
{
id: "nvidia/nvidia/nemotron-3-super-120b-a12b",
displayName: "NVIDIA: Nemotron 3 Super 120B",
descriptionText: "Nemotron 3 Super 120B Deep Reasoning model on NVIDIA NIM",
},
{
id: "gemini/gemini-2.5-flash",
displayName: "Gemini: Gemini 2.5 Flash (AI Studio)",
descriptionText: "Google AI Studio direct Gemini 2.5 Flash route",
},
{
id: "gemini/gemini-2.5-pro",
displayName: "Gemini: Gemini 2.5 Pro (AI Studio)",
descriptionText: "Google AI Studio direct Gemini 2.5 Pro route",
},
];
const OMNIROUTE_CUSTOM_MODELS = new Set([
...OMNIROUTE_BUILTIN_GROUPS.map((g) => g.id),
...Object.keys(MODEL_ROUTING_MAP),
]);
function shouldInterceptToOmniRoute(model, url) {
if (!model) return false;
// Never intercept non-streaming unary RPCs (Antigravity expects raw JSON/Protobuf, not SSE)
const isStreaming =
url.includes("streamGenerateContent") ||
url.includes("StreamGenerateChat") ||
url.includes("alt=sse");
if (!isStreaming) return false;
// Never intercept native Google/Gemini models (used by Antigravity core, subagents, websearch, grounding)
if (model.startsWith("gemini-") || model.startsWith("models/gemini-")) {
return false;
}
// Never intercept native Google CloudCode PA hosted models
if (
model === "claude-sonnet-4-6" ||
model === "claude-opus-4-6" ||
model === "gpt-oss-120b-medium"
) {
return false;
}
// Intercept any OmniRoute auto group, provider model, or mapped alias
const clean = model.replace(/^models\//, "").trim();
if (
clean.startsWith("auto/") ||
clean.toLowerCase().includes("omniroute") ||
clean.includes("/") ||
OMNIROUTE_CUSTOM_MODELS.has(model) ||
OMNIROUTE_CUSTOM_MODELS.has(clean) ||
Boolean(MODEL_ROUTING_MAP[model]) ||
Boolean(MODEL_ROUTING_MAP[clean])
) {
return true;
}
return false;
}
const internalApp = http.createServer(async (req, res) => {
const host = (req.headers.host || "cloudcode-pa.googleapis.com").split(":")[0];
const url = req.url || "/";
// Collect request body
const chunks = [];
for await (const chunk of req) {
chunks.push(chunk);
}
const bodyBuffer = Buffer.concat(chunks);
let bodyJson = null;
if (bodyBuffer.length > 0) {
try {
bodyJson = JSON.parse(bodyBuffer.toString("utf-8"));
} catch {}
}
const model = extractModel(bodyJson, url);
const shouldIntercept = shouldInterceptToOmniRoute(model, url);
if (shouldIntercept) {
const resolvedModel = resolveTargetModel(model);
console.log(
`[Bridge] 🔀 INTERCEPTING -> OmniRoute: "${model || "default"}" => "${resolvedModel}" (${url})`
);
let outgoingBuffer = bodyBuffer;
if (bodyJson) {
const cloned = JSON.parse(JSON.stringify(bodyJson));
cloned.model = resolvedModel;
if (cloned.request && typeof cloned.request === "object") {
cloned.request.model = resolvedModel;
}
outgoingBuffer = Buffer.from(JSON.stringify(cloned), "utf-8");
}
// Forward to OmniRoute /v1/antigravity
try {
const forwardHeaders = {
"Content-Type": "application/json",
"Content-Length": Buffer.byteLength(outgoingBuffer),
Authorization: `Bearer ${ROUTER_API_KEY}`,
"x-omniroute-source": "agent-bridge",
"x-omniroute-agent": "antigravity",
"x-omniroute-skip-usage": "true", // Skip usage tracking for default models
};
const upstreamReq = http.request(
ROUTER_URL,
{
method: "POST",
headers: forwardHeaders,
agent: httpAgent,
},
(upstreamRes) => {
res.writeHead(upstreamRes.statusCode || 200, upstreamRes.headers);
upstreamRes.pipe(res);
}
);
upstreamReq.setNoDelay(true);
upstreamReq.on("error", (err) => {
console.error(`[Bridge] ❌ Error forwarding to OmniRoute: ${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { message: `OmniRoute bridge error: ${err.message}` } }));
}
});
upstreamReq.write(outgoingBuffer);
upstreamReq.end();
return;
} catch (err) {
console.error(`[Bridge] ❌ Failed to invoke OmniRoute: ${err.message}`);
}
}
// Otherwise: Passthrough directly to Google upstream
console.log(`[Bridge] ⏩ PASSTHROUGH -> Google: ${model || "non-model"} (${url})`);
const upstreamHeaders = { ...req.headers };
delete upstreamHeaders["host"]; // Let https.request set the correct Host
upstreamHeaders["host"] = host;
if (url.includes("fetchAvailableModels")) {
delete upstreamHeaders["accept-encoding"];
}
const googleReq = https.request(
{
hostname: host,
port: 443,
path: url,
method: req.method,
headers: upstreamHeaders,
agent: httpsAgent,
},
(googleRes) => {
if (url.includes("fetchAvailableModels")) {
const respChunks = [];
googleRes.on("data", (chunk) => respChunks.push(chunk));
googleRes.on("end", () => {
const respBuffer = Buffer.concat(respChunks);
let finalBuffer = respBuffer;
try {
const data = JSON.parse(respBuffer.toString("utf-8"));
if (data && data.models) {
// Inject OmniRoute built-in auto groups and models
const baseTemplate =
data.models["claude-sonnet-4-6"] ||
data.models["gpt-oss-120b-medium"] ||
Object.values(data.models)[0] ||
{};
const injectedIds = [];
for (const group of OMNIROUTE_BUILTIN_GROUPS) {
data.models[group.id] = {
...baseTemplate,
id: group.id,
name: group.id,
displayName: group.displayName,
descriptionText: group.descriptionText,
};
injectedIds.push(group.id);
}
// Prepend OmniRoute groups to agentModelSorts recommended group
if (
Array.isArray(data.agentModelSorts) &&
data.agentModelSorts[0]?.groups?.[0]?.modelIds
) {
const existing = data.agentModelSorts[0].groups[0].modelIds;
data.agentModelSorts[0].groups[0].modelIds = [
...injectedIds,
...existing.filter((id) => !injectedIds.includes(id)),
];
}
finalBuffer = Buffer.from(JSON.stringify(data), "utf-8");
console.log(
`[Bridge] 🌟 Injected custom models into fetchAvailableModels (${finalBuffer.length} bytes)`
);
}
} catch (err) {
console.error(`[Bridge] ⚠️ Error modifying fetchAvailableModels: ${err.message}`);
}
const headers = { ...googleRes.headers };
delete headers["content-length"];
delete headers["content-encoding"];
headers["content-length"] = String(finalBuffer.length);
res.writeHead(googleRes.statusCode || 200, headers);
res.end(finalBuffer);
});
return;
}
res.writeHead(googleRes.statusCode || 200, googleRes.headers);
googleRes.pipe(res);
}
);
googleReq.setNoDelay(true);
googleReq.on("error", (err) => {
console.error(`[Bridge] ❌ Google upstream error: ${err.message}`);
if (!res.headersSent) {
res.writeHead(502, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { message: `Google upstream error: ${err.message}` } }));
}
});
if (bodyBuffer.length > 0) {
googleReq.write(bodyBuffer);
}
googleReq.end();
});
internalApp.keepAliveTimeout = 65000;
internalApp.headersTimeout = 66000;
// Proxy server listening on HTTP port
const proxyServer = http.createServer((req, res) => {
// Plain HTTP request (non-CONNECT)
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OmniRoute Antigravity Bridge Proxy Active\n");
});
proxyServer.keepAliveTimeout = 65000;
proxyServer.headersTimeout = 66000;
proxyServer.on("connect", (req, clientSocket, head) => {
clientSocket.setNoDelay(true);
const [targetHost, targetPortStr] = (req.url || "").split(":");
const targetPort = parseInt(targetPortStr || "443", 10);
if (TARGET_HOSTS.has(targetHost)) {
// Target host: Terminate TLS locally and route via internalApp
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
const ssl = getSslOptions();
const tlsSocket = new tls.TLSSocket(clientSocket, {
isServer: true,
key: ssl.key,
cert: ssl.cert,
});
tlsSocket.setNoDelay(true);
tlsSocket.on("error", (err) => {
// Client closed or TLS error
clientSocket.destroy();
});
internalApp.emit("connection", tlsSocket);
} else {
// Non-target host: Transparent raw TCP tunnel
const upstreamSocket = net.connect(targetPort, targetHost, () => {
upstreamSocket.setNoDelay(true);
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
if (head && head.length > 0) {
upstreamSocket.write(head);
}
upstreamSocket.pipe(clientSocket);
clientSocket.pipe(upstreamSocket);
});
const cleanup = () => {
clientSocket.destroy();
upstreamSocket.destroy();
};
upstreamSocket.on("error", cleanup);
clientSocket.on("error", cleanup);
}
});
export {
resolveTargetModel,
MODEL_ROUTING_MAP,
shouldInterceptToOmniRoute,
extractModel,
OMNIROUTE_BUILTIN_GROUPS,
proxyServer,
internalApp,
};
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
if (isMain) {
proxyServer.listen(PORT, "127.0.0.1", () => {
console.log(`🚀 OmniRoute Antigravity Bridge listening on 127.0.0.1:${PORT}`);
console.log(` Routing non-Gemini 3.8 model traffic -> ${ROUTER_URL}`);
console.log(` Preserving Gemini 3.8 native traffic -> Google`);
});
}

View File

@@ -79,7 +79,8 @@ export async function runChatCommand(promptArg, opts, cmd) {
const data = await response.json();
const text = extractText(data, opts.responsesApi);
if (!opts.noHistory) {
// Commander stores `--no-history` as `history === false`, never as `noHistory`.
if (opts.history !== false && opts.noHistory !== true) {
appendHistory({ prompt, model: opts.model, latencyMs, usage: data.usage, response: text });
}

View File

@@ -248,7 +248,9 @@ export function registerContexts(program) {
.option("--no-secrets", "Omit API keys from export")
.action(async (opts, cmd) => {
const cfg = loadContexts();
const out = opts.noSecrets ? redactContextSecrets(cfg) : JSON.parse(JSON.stringify(cfg));
// Commander stores `--no-secrets` as `secrets === false`, never as `noSecrets`.
const redact = opts.secrets === false || opts.noSecrets === true;
const out = redact ? redactContextSecrets(cfg) : JSON.parse(JSON.stringify(cfg));
const json = JSON.stringify(out, null, 2);
if (opts.out) {
const { writeFileSync } = await import("node:fs");

View File

@@ -10,6 +10,7 @@ import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
import { printHeading } from "../io.mjs";
import { t } from "../i18n.mjs";
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
import { getCrashLogPath } from "../runtime/processSupervisor.mjs";
const STATIC_SALT = "omniroute-field-encryption-v1";
const KEY_LENGTH = 32;
@@ -380,6 +381,33 @@ function checkMemory() {
});
}
// #13538: surfaces the supervisor's give-up crash record (persisted by
// ServerSupervisor.persistCrashLog(), bin/cli/runtime/processSupervisor.mjs)
// so a user whose `--tray` worker died silently (detached, stdio:"ignore")
// has something concrete `doctor` can point at without needing `--log`.
function checkCrashLog() {
const crashLogPath = getCrashLogPath();
if (!fs.existsSync(crashLogPath)) {
return ok("Crash log", "No supervisor crash record found", { crashLogPath });
}
try {
const stat = fs.statSync(crashLogPath);
const contents = fs.readFileSync(crashLogPath, "utf8");
const lastEntry = contents.split("\n").filter(Boolean).slice(-6).join("\n");
return warn(
"Crash log",
`Supervisor recorded a give-up crash at ${crashLogPath} (last modified ${stat.mtime.toISOString()})`,
{ crashLogPath, modifiedAt: stat.mtime.toISOString(), tail: lastEntry }
);
} catch (error) {
return warn("Crash log", `Crash record exists at ${crashLogPath} but could not be read`, {
crashLogPath,
error: error instanceof Error ? error.message : String(error),
});
}
}
async function fetchWithTimeout(url, options = {}) {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
@@ -579,6 +607,7 @@ export async function collectDoctorChecks(context = {}, options = {}) {
checks.push(await checkNodeRuntime(rootDir));
checks.push(await checkNativeBinary(rootDir));
checks.push(checkMemory());
checks.push(checkCrashLog());
if (!options.skipLiveness) {
checks.push(await checkServerLiveness(options));

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

@@ -352,10 +352,24 @@ export async function runKeysRegenerateCommand(id, opts = {}) {
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, {
const encodedId = encodeURIComponent(id);
let res = await apiFetch(`/api/v1/registered-keys/${encodedId}/regenerate`, {
method: "POST",
retry: false,
acceptNotOk: true,
});
// `keys` predates the split between registered keys and the dashboard's
// ordinary API keys. IDs shown by `keys list`/the dashboard belong to
// `/api/keys`, while deployment/registered-key IDs belong to
// `/api/v1/registered-keys`. Try the ordinary-key route when the ID is not
// present in the registered-key store so the command works with either ID.
if (isRouteUnavailableStatus(res.status)) {
res = await apiFetch(`/api/keys/${encodedId}/regenerate`, {
method: "POST",
retry: false,
acceptNotOk: true,
});
}
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
@@ -410,9 +424,17 @@ export async function runKeysRevealCommand(id, opts = {}) {
return 1;
}
try {
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
const encodedId = encodeURIComponent(id);
let res = await apiFetch(`/api/v1/registered-keys/${encodedId}/reveal`, {
retry: false,
acceptNotOk: true,
});
if (isRouteUnavailableStatus(res.status)) {
res = await apiFetch(`/api/keys/${encodedId}/reveal`, {
retry: false,
acceptNotOk: true,
});
}
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;

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

@@ -6,7 +6,8 @@ export function registerRestart(program) {
program
.command("restart")
.description(t("restart.description"))
.option("--port <port>", t("serve.port"), "20128")
// No Commander default: runServe() falls back to PORT, then 20128 (#7049).
.option("--port <port>", t("serve.port"))
.action(async (opts) => {
const exitCode = await runRestartCommand(opts);
if (exitCode !== 0) process.exit(exitCode);

View File

@@ -4,7 +4,13 @@ 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,
findListeningPids,
resolveReadyTimeoutMs,
} from "../utils/pid.mjs";
import {
ServerSupervisor,
detectMitmCrash,
@@ -58,6 +64,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") ||
@@ -230,6 +241,16 @@ export async function runServe(opts = {}) {
process.exit(1);
}
// Refuse to start a second instance on a port something else already owns,
// BEFORE any pid file is written or any child is spawned. Otherwise the
// doomed child's EADDRINUSE arrives only after this process has rewritten
// the pid files of the healthy instance that actually owns the port.
const busyPids = await findListeningPids(dashboardPort);
if (busyPids.length > 0) {
reportPortInUse(dashboardPort, busyPids);
process.exit(1);
}
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
// #5172/#5160/#5152: default the V8 heap to ~35% of physical RAM (clamped
@@ -276,7 +297,8 @@ export async function runServe(opts = {}) {
return runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort);
}
if (opts.noRecovery) {
// Commander stores `--no-recovery` as `recovery === false`, never as `noRecovery`.
if (opts.recovery === false || opts.noRecovery === true) {
return runWithoutRecovery(
serverJs,
env,
@@ -299,10 +321,29 @@ export async function runServe(opts = {}) {
opts.maxRestarts ?? 2,
startedAt,
useTray,
{ trayReadyPort: opts.trayReadyPort, trayReadyToken: opts.trayReadyToken }
{
trayReadyPort: opts.trayReadyPort,
trayReadyToken: opts.trayReadyToken,
readyTimeoutMs: resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout }),
}
);
}
/**
* Explain a port conflict in terms the operator can act on: who owns the port,
* and the two ways out. Exported for unit tests.
*/
export function reportPortInUse(port, pids = []) {
const owner = pids.length === 1 ? `PID ${pids[0]}` : `PIDs ${pids.join(", ")}`;
console.error(`\n\x1b[31m✖ Port ${port} is already in use by ${owner}.\x1b[0m`);
console.error(
` Another OmniRoute is most likely already serving there, so open` +
` ${urlScheme}://localhost:${port} before starting a second one.`
);
console.error(` To replace it: \x1b[36momniroute stop\x1b[0m, then start again`);
console.error(` To run alongside: \x1b[36momniroute serve --port <other-port>\x1b[0m\n`);
}
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
@@ -413,7 +454,7 @@ async function runWithSupervisor(
maxRestarts,
startedAt,
useTray = false,
{ trayReadyPort, trayReadyToken } = {}
{ trayReadyPort, trayReadyToken, readyTimeoutMs = resolveReadyTimeoutMs() } = {}
) {
if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1";
writePidFile("supervisor", process.pid);
@@ -452,7 +493,12 @@ async function runWithSupervisor(
});
if (!showLog) {
waitForServer(dashboardPort, 60000).then(async (up) => {
let lastProbeOutcome = null;
waitForServer(dashboardPort, readyTimeoutMs, {
onOutcome: (outcome) => {
lastProbeOutcome = outcome;
},
}).then(async (up) => {
if (up) {
if (useTray) {
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
@@ -476,7 +522,7 @@ async function runWithSupervisor(
}
onReady(dashboardPort, apiPort, noOpen, startedAt);
} else {
reportReadinessTimeout(dashboardPort, supervisor);
reportReadinessTimeout(dashboardPort, supervisor, lastProbeOutcome);
}
});
}
@@ -488,11 +534,31 @@ async function runWithSupervisor(
// stuck (issue reports show the server sometimes actually comes up later, or is
// 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) {
export function reportReadinessTimeout(dashboardPort, supervisor, lastProbeOutcome = null) {
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.`
);
// The last probe classification separates a real boot failure (nothing ever
// bound the port, so the buffered output below is the reason) from a server
// that IS listening and merely did not answer the health route in time:
// very likely usable already, with only the readiness signal timed out.
if (lastProbeOutcome === "hanging" || lastProbeOutcome === "fast-reject") {
console.error(
` Port ${dashboardPort} IS accepting connections, so the server is probably up and` +
` still warming up. Check the dashboard before restarting it.`
);
} else if (lastProbeOutcome === "not-listening") {
console.error(
` Nothing is listening on port ${dashboardPort}, so the server never bound it and the` +
` output below is the reason.`
);
}
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);
});
}

View File

@@ -187,7 +187,9 @@ export async function runUpdateCommand(opts = {}) {
}
if (dryRun) {
console.log("\n [DRY RUN] Would run: npm install -g omniroute@latest --include=optional");
console.log(
"\n [DRY RUN] Would run: npm install -g omniroute@latest --include=optional --legacy-peer-deps"
);
if (!skipBackup) console.log(" [DRY RUN] Would create backup in ~/.omniroute/backups/");
return 0;
}
@@ -221,7 +223,9 @@ export async function runUpdateCommand(opts = {}) {
const { execSync } = await import("child_process");
// --include=optional keeps the optionalDependencies (better-sqlite3, keytar,
// tls-client, llmlingua SLM stack) on update so an omit=optional config can't drop them.
execSync("npm install -g omniroute@latest --include=optional", { stdio: "inherit" });
execSync("npm install -g omniroute@latest --include=optional --legacy-peer-deps", {
stdio: "inherit",
});
// Trust-but-verify: `npm install -g` exits 0 even when a shadowing local install
// (e.g. ~/node_modules/omniroute ahead of the global prefix on PATH) means the
// binary the user actually runs was not touched. Re-read the running binary's

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

Some files were not shown because too many files have changed in this diff Show More