Commit Graph

184 Commits

Author SHA1 Message Date
Jan Leon
a99c795a67 Add native ChatGPT Web provider for Codex clients (#8949)
* Bypass proxy compaction for native Codex context

* Add native ChatGPT Web provider pipeline

* Add managed browser and tunnel deployment

* Add ChatGPT Web setup and doctor UI

* Document and test ChatGPT Web integration

* fix(security): register chatgpt-web-codex-doctor in LOCAL_ONLY_API_PATTERNS

The diagnostic route under /api/providers/{id}/chatgpt-web-codex-doctor
was not registered in the spawn-capable route guard. Adding it for
parity with the existing /login pattern.

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

* fix(providers): route chatgpt-web-codex admin routes through a service boundary

The provider CRUD/doctor routes imported chatgpt-web-codex helpers
(finalizeValidatedChatGptWebCodexSecrets, encode/decodeChatGptWebCodexSecrets,
getChatGptWebCodexDoctorStatus) directly from open-sse/executors/**, which
no-restricted-imports (EXECUTOR_IMPORT_RESTRICTION) forbids for src/app/**
files — executor implementations must stay behind an open-sse handler or
service boundary.

Add open-sse/services/chatgptWebCodexAdmin.ts as a thin re-export boundary
(mirroring the existing tokenRefresh.ts re-export pattern) and import from
there instead. No behavior change.

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-08-11 09:53:39 -03:00
Arul Kumaran
aa4e72097a fix(bun): make server child and outbound fetch Bun-safe (#9761)
* chore(changelog): v3.8.49 reconciliation — 200 missing bullets + 22 restored credits

Phase 0a of /generate-release. Measured commit<->CHANGELOG coverage over the real
cycle range (2c62333b0..HEAD, 933 non-merge commits) instead of the last tag: 180
merged PRs had no bullet at all (they landed without a changelog.d fragment) and a
further 19 were invisible because the merge-train landed them under a generic
'Train 1D: merge via --admin' subject that carries no PR reference.

- +200 bullets, all with PR back-reference and author attribution (1179 -> 1379)
- 🙌 Contributors 156 -> 178; credits @terrafirmbot-source for #7904, which shipped
  through the conflict-resolved #8685 without any attribution
- closed-PR credit audit over the 32 human PRs closed unmerged this cycle: 12 had
  already landed under the author's own follow-up PR and were verified credited
- rollup bullet for the direct release-branch maintenance (merge-train landings,
  ratchet re-pins, base-red sweeps) that carries no PR of its own
- [3.8.49] header dated 2026-07-28 (was TBD) in the root file and the 42 i18n mirrors

Coverage after: 0 commits uncovered.

* chore(quality): v3.8.49 pre-flight — clear 4 base-reds, absorb cycle drift

Pre-flight sweep (Phase 0). Test suites ran on the dedicated 32-core box so the
self-inflicted load of `node --test` could not fabricate timing flakes.

Base-reds fixed (all real, all from merged cycle PRs that did not update their
characterization tests):

- providers-constants-split / quota-plan-registry / provider-translate-path GOLDEN:
  #8861 added the Xiaomi MiMo Token Plan provider, so APIKEY_PROVIDERS is 195 (was
  194), knownProviders() is 12 (was 11) and the translate-path snapshot gains one
  purely additive entry. Counts aligned to the shipped catalog, never relaxed.
- agent-skills-content: skills/config-codex-cli/ was added by #8709 with a custom
  block, so the custom-block set is 13, not 12.
- chatcore-compression-integration: #8595/#8560 deliberately decoupled REACTIVE
  context compaction from the `enabled` master switch, so a body above 70% of the
  window is pruned even with compression off. The test was sized above that
  threshold, which made it assert against intended behavior; it now stays below it
  and keeps testing the invariant it was written for (resolveBasePlan short-circuits
  to "off" before reading comboOverrides).

Static gates:

- 3 shellcheck directives were malformed (`# shellcheck disable=SC2086 — text`; the
  em-dash makes shellcheck reject the whole directive as SC1125) in ci.yml and
  nightly-release-green.yml — the comment now sits on its own line.
- gitleaks: 2 new generic-api-key false positives allowlisted with justification —
  a localStorage key for the sponsor banner (#8723) and the PUBLIC Adobe Firefly
  web x-api-key, whose only literals are in JSDoc (the runtime reads it through
  resolvePublicCred, per Hard Rule #11). secretFindings back to 0.
- zizmor 176 -> 189 and bundleSize 6762 -> 7666 rebaselined with the measurement and
  the reason; both are ordinary cycle drift absorbed at release.

Environment-dependent failures classified out, not silenced: the two tproxy tests
assert the native addon is unavailable/unprivileged and therefore fail when the
suite runs as root on the build box (they pass as a normal user), and the
consoleInterceptor rate-limit test is a 4s-timing flake under load (6/6 isolated).

* test(codex): align the Responses HTTP e2e to the #8507 input-item contract

Fifth and last base-red of the v3.8.49 pre-flight. #8507 (#8083) deliberately sets
`status: "completed"` on Responses input items so strict upstream validators accept
them; codex-chat-reasoning-http-e2e still asserted the pre-#8507 shape, so it failed
against intended behavior. Expectation updated with the reason inline — the assertion
is not relaxed, it now pins the current contract.

The test was never reached in the first pre-flight sweep (the run was interrupted
during the integration phase, and this file sorts after the one that failed).

* docs(release): v3.8.49 feature-documentation sync

Phase 1 step 6b. Swept the cycle's 284 New Features bullets against the existing
docs before writing anything: nearly every large theme (Kimi, xAI OAuth, session
affinity, bun:sqlite, Firecrawl, Opus 5, omniglyph, GCF v3.2, homologation suite)
was already covered. Six real gaps were left undocumented by the PRs that shipped
them, each verified in source before being written up:

- CredentialMaskerGuardrail (#7683) is registered in guardrails/registry.ts but the
  GUARDRAILS table listed only 3 of the 4 guardrails
- the cacheAffinity scoring factor and the cache-optimized combo strategy (#8008):
  the docs still said 12 factors / 18 strategies, the code has 13 / 19
- the optional dashboard OIDC login gate (#6973) — /api/auth/oidc/{login,callback}
  had no mention in AUTHZ_GUIDE
- GET /api/usage/cache-health (#8827) and GET /api/usage/model-latency-stats (#6873)
  were missing from the API reference

README "What's New" gains one bullet (routing transparency) and merges two others
rather than growing a second changelog. PROVIDER_REFERENCE regenerated with the
generator (Firecrawl reclassified to Search, Xiaomi MiMo added by #8861).

check:docs-all green: 134 docs, 813 internal links, no fabricated API/env/CLI
references. Known pre-existing drift left alone and reported: stale nominal counts
in ARCHITECTURE/CODEBASE_DOCUMENTATION (soft), the 9-factor mentions scattered in
AUTO-COMBO, and the auto-combo diagram SVG (the renderer needs a browser this
environment does not have — the .mmd source is updated and the .md says so).

* chore(release): v3.8.49 — clear the release-PR CI in one pass

Every finding from the first full ci.yml run on the release PR, fixed or justified
together so a single re-push clears the board.

Lint / check:route-validation:t06 — three routes read request.json() with no visible
Zod validation. The two proxy-subscriptions routes validated with a hand-rolled
parsePayload(); they now use real Zod schemas (src/lib/proxySubscription/schema.ts)
reproducing the same acceptance rules, error strings and status codes. chat/completions
is the proxy's hottest path and parses the body ONCE on purpose (#4380 OOM crash-loop),
so it now safeParses the ALREADY-PARSED object against a deliberately permissive
structural schema — proven not to change behavior: absent model and model:null still
pass through, role "developer" still reaches 200, a ~300 KB payload is accepted, and
the body is still read exactly once. 25 new tests.

i18n UI value drift — 13 English strings rewritten during the cycle left stale
translations in up to 41 locales (317 pairs). Eleven are genuine rewrites and now carry
the pipeline's __MISSING__:<english> marker so the runtime serves corrected English until
translation catches up; vi forbids that marker by test, so it got a real translation.

PR Test Policy — 33 files flagged. Each was verified against the SOURCE, not the diff:
26 assert reductions are legitimate (mostly the #7866 Qwen OAuth provider removal and the
#8013 Antigravity refactor deleting the surface under test) and are allowlisted with the
PR and the evidence; 5 deleted files have verified replacements. One was NOT legitimate:
#7528's GraphQL->WebSocket migration dropped four muse-spark continuation scenarios whose
logic is still live — connection isolation, cache eviction after a failed turn (the commit
itself says "was missing"), parallel-chat cache collision, and the empty-content guard.
All four are restored against the new transport and each was verified to fail when the
corresponding production mechanism is broken.

Quality Ratchet / openapiCoverage — 36.6% against a baseline of 38: the cycle added routes
faster than the spec. Eight real endpoints are now documented from their route.ts
(usage cache-health and model-latency-stats, the two OIDC endpoints, and the five
proxy-subscriptions paths), bringing it to 38.1%.

Quality Gates (Extended) / zizmor — the runner measures 190 where the devbox measures 189
on the same commit, a delta already recorded in this baseline's history. Baselined to the
runner's number.

Also: the driverFactory better-sqlite3 guard moved from a mid-body t.skip() to a declared
{ skip: <condition> } test option. Same behavior for the optional native dependency, but
the skip now shows up in the report and is distinguishable from a test.skip() that silences
a test outright. Verified under both runners: 15/15 on Node, 14/14 on Bun.

SonarCloud Code Analysis stays red and is not a blocker: sonar.qualitygate.wait=false since
#7038 makes the job informative, the built-in gate cannot be swapped on the FREE plan, and
main has no branch protection.

* chore(quality): close the last two release-PR reds

test-masking — I had missed one of the 34 flagged files: my first pass grepped only
paths under tests/, so open-sse/services/__tests__/tierResolver.test.ts was invisible.
Same #7866 cause as the other eight qwen-driven reductions: the "classifies Qwen as
free" case and qwen's entry in the batch list went with the removed provider, and the
batch indices dropped from 10 to 9 (61→59). Allowlisted with that evidence.

dast-smoke — all four Schemathesis findings are on the two OIDC endpoints documented
in the previous commit, and none is a defect. /api/auth/oidc/* is a BROWSER redirect
flow: it answers 302 to the IdP and 302 back to /login?oidc_error=... on every failure,
which Schemathesis reads as "accepted a schema-violating request", and it answers 400
when OIDC is not configured, which it reads as "rejected a schema-compliant request".
Keeping the endpoints in the spec is right — operators need them, and they are what
brought openapi coverage back over the baseline — so the flow is excluded from the fuzz
instead, with the reason inline in the workflow. The rest of /api/auth and /api/keys
stays in scope.

* test(db): reword the driverFactory skip comment so the gate stops counting it

The anti-test-masking gate greps text, not code: my explanation of WHY the
better-sqlite3 guard moved out of the test body spelled the runner API out
literally, and those two mentions inside a comment were counted as two new skip
markers — the exact signal the previous commit set out to clear. Same explanation,
phrased without the call syntax.

Verified with the gate's own exported helpers against the merge-base: 0 modified-file
violations, 0 deletion violations. Test still 15/15.

* fix(dashboard): unbreak the vitest:ui gate — 2 real production bugs + the i18n test seam

The Vitest job is a BLOCKING gate that had not run to completion once in this whole
release: rounds 1-3 cancelled it via cancel-in-progress on each successive fix push,
so its red was indistinguishable from green. Round 4 finally ran it and the suite was
broken cycle-wide.

Root cause of the suite: #7935 instrumented ~180 shared/dashboard components with
next-intl's useTranslations/useLocale without updating the tests that mount them, so
every one of them threw "context from NextIntlClientProvider was not found". Fixed at
the shared seam (tests/_setup/vitestUiPolyfills.ts) rather than per file: a translator
built from the REAL en.json via next-intl's own createTranslator, memoized per
namespace — the naive version returns a fresh function each call and any component
whose useCallback/useEffect depends on t spins forever, which reads as a hang, not a
failure. A local mock still wins over the default. 22 files fixed by the seam alone,
15 realigned to the real strings; no assert removed or weakened.

Two production bugs the suite was hiding, both pre-existing and both with a failing
regression test already in the tree:

- RequestLoggerDetail crashed on a structured error object. #7920 gave the component
  formatErrorForDisplay for exactly this case, then #8213's combo-503 / cooldown
  checks went to the raw field and called .toLowerCase() on it. Both paths now use
  the helper.
- The logs detail modal reopened on first close again. #6830 fixed that by reading the
  deep-link id ONCE; the #8354 page rewrite regressed it by reading the live
  searchParams every render, so the prop flips mid-session and re-fires the child's
  deep-link effect exactly as the modal closes. Frozen at mount again.

Also tightens i18nUiCoverage 75.5 -> 99, which the ratchet demanded under
--require-tighten: the metric genuinely improved as the async translation workflow
paid off the debt that the v3.8.39/.44/.47 rebaselines had been recording. The
collector subtracts placeholders, so this release's 317 __MISSING__ markers are
already netted out of the 99.

Two UI files still fail locally under 20-worker concurrency (combos-page-smoke,
evals-tab-smoke) — cold-import flakes that pass isolated and with a larger timeout.

* test(e2e): repair the four shards the first green Build finally exercised

test-e2e has `needs: [build]`, and the release PR's Build died on every round
until now — so the 9-shard matrix produced ZERO signal for this whole cycle
while ~200 PRs merged. The first successful Build surfaced four independent
breakages, each traced to the commit that caused it:

- providers-management (#7361): the single-connection delete moved from
  window.confirm() to a ConfirmModal, so page.once("dialog") never fired and
  the DELETE was never sent (deleteCalls stayed 0). Click the modal instead.
- providers-bailian-coding-plan (#7882): the free-text Base URL field was
  deliberately replaced by a region step whose choice resolves the endpoint
  (global-sg -> coding-intl.dashscope, china-beijing -> coding.dashscope).
  Both cases rewritten against the region step; the invalid-URL case is
  unreachable from this modal now, so it covers the CN choice instead.
- group-b-activity-feed: the stack-trace guard ran against page.content(),
  which embeds the serialized i18n payload — zenmux's "endpoint at
  /api/v1/chat/completions" is prose, not a leak. Assert on rendered
  innerText and require the :line:col every real stack frame carries.
- navigation (#8292): APP_ROUTE_PATTERN accepted only /login and /dashboard,
  but the new prefetch spec is the sole caller passing /home, so waitForURL
  never resolved and the retry loop burned the full 180s timeout.

E2E is green on main (9/9 on 07-22 and 07-23), so all four are cycle
regressions, not pre-existing debt. Tests only — no production code touched.

* fix(dashboard): stop the /home quick-start cards from prefetching too

#8292 fixed half the RSC prefetch storm: it added prefetch={false} to the
sidebar's navigation and logo links, but /home — the landing route, and the
one its own e2e guard visits — renders five more internal Links in the
quick-start cards. First paint still fired 12 speculative RSC requests for
/dashboard/{analytics,logs,providers,api-manager} and /docs.

That PR shipped the test that would have caught this, but the test never got
to its assertion: gotoDashboardRoute("/home") hung because APP_ROUTE_PATTERN
accepted only /login and /dashboard, so the retry loop burned the whole 180s
timeout with no assertion error. With that helper repaired in the previous
commit, navigation.spec.ts finally ran and reported the 12 requests.

Validated both ways, per Hard Rule #18:
- tests/unit/sidebar-prefetch-policy-8281.test.ts extended to /home — red on
  the parent commit (5 internal Links, 5 without prefetch={false}), green here.
- the e2e assertion expect(speculativeRequests).toEqual([]) is the end-to-end
  guard; it is what surfaced the defect in the first place.

* refactor(dashboard): shrink HomePageClient back under the size gate

The prefetch fix in the parent commit tripped check:file-size — the frozen
budget for this file is 1377 lines and a naive fix measured 1391, because
`href` + `prefetch={false}` + `className` no longer fits Prettier's 100-column
budget, so three one-line <Link> elements each expanded to five.

Followed the gate's own first suggestion (extract/DRY) before touching the
baseline: the quick-start links repeated the same className literal four
times, and the docs link carried a 180-char one inline. Hoisting both into
INLINE_LINK / DOCS_LINK collapses five wrapped <Link> blocks back to a single
line each and removes the duplication — 1391 -> 1381.

The remaining +4 over the frozen budget is the five prefetch attributes
themselves, which cannot be expressed in fewer lines. Rebaselined to 1381
with the rationale recorded in file-size-baseline.json under
_rebaseline_2026_07_29_8281_home_quickstart_prefetch.

tests/unit/sidebar-prefetch-policy-8281.test.ts still passes (2/2): it matches
whole <Link ...> blocks, so it is indifferent to the wrapping and only checks
that every internal link opts out of prefetch.

* fix(bun): use native fetch for direct outbound requests

* test(bun): cover native direct fetch path

* fix(bun): preload polyfill for next build workers

* fix(bun): expose AsyncLocalStorage globally

* fix(bun): filter non-page Fumadocs metadata

* fix(bun): defer docs-only route dependencies

* chore(skills): sync generated OmniRoute agent skill docs

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-11 09:16:23 -03:00
Diego Rodrigues de Sa e Souza
3835f318d0 fix(cli): use process.execPath for macOS launchd autostart (#9156)
Refs: base-red #9737
2026-08-08 13:51:51 -03:00
SAMUEL AUGUSTO GUIMARAES LOPES
236aad07c7 fix(mcp): stop DB init logging from corrupting the stdio JSON-RPC stream (#9281)
Merge-train validated (tip 6ce4effef8). Vitest failures confirmed as base-red (#9679).
2026-08-07 20:52:26 -03:00
Diego Rodrigues de Sa e Souza
cfeea5dc5b fix(cli): enable systray2 on Windows for Norton-friendly tray (#8609)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:08:17 -03:00
Diego Rodrigues de Sa e Souza
d12c3b37da fix: resolve two macOS-only test/script failures in unit suite (#8577)
bin/restore-policies.sh used readarray (bash 4+), which fails on macOS
bash 3.2. Replace with a compatible while-read loop.

machineId.test.ts disableWindowsRegistryStrategy() did not neutralize
the macOS ioreg strategy, so mocked os.hostname() was never reached
on macOS and both ladder tests failed. Stub execSync for ioreg commands
so the fallback chain reaches os.hostname() as intended.

Production src/shared/utils/machineId.ts is correct and unchanged.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:42 -03:00
Diego Rodrigues de Sa e Souza
1b83b337b3 fix(cli): fall back to node:sqlite when better-sqlite3 constructor throws at runtime (#8826)
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:38 -03:00
Diego Rodrigues de Sa e Souza
904e8af09a fix(opencode): prefix provider id with opencode- for auth login command (#8830)
The bundled @omniroute/opencode-plugin registers its provider under
'opencode-omniroute' (the 'opencode-' prefix is required by OpenCode
>=1.17.8's native-adapter gate on model providerID). But the CLI
instructed 'opencode auth login --provider omniroute' — the unprefixed
id — so OpenCode reported 'Unknown provider "omniroute"' because it
resolves --provider against the exact provider id the plugin registered.

Add resolveOpenCodeAuthProviderId() helper that idempotently adds the
'opencode-' prefix when absent, and use it everywhere the CLI builds
or prints the --provider argument: resolveOpenCodeAuthSpawn args,
runOpenCodeAuth ENOENT message, and runSetupOpenCodeCommand 'Run
manually'/'Next step' messages. Update the plugin README and test
assertions to match.

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-07 18:07:30 -03:00
Diego Rodrigues de Sa e Souza
e7f6b1d130 feat(radar): flag-gated signed free-model catalog overlay (#9515)
* feat(dashboard): add RADAR_ENABLED flag (default off)

* feat(db): radar feed cache + settings with encrypted supporter key

* feat(radar): signed feed sync with pinned key and version floor

- feedSchema.ts: Zod v4 schema mirroring the server feed format
  (discriminated union on budget.kind, enum constraints, etc.)
- pinnedKeys.ts: Ed25519 SPKI-DER pinned key + env override for forks
- verify.ts: signature verification over exact wire bytes, never throws
- sync.ts: full download/verify/validate/cache pipeline with injectable
  deps, feature-flag gate, opt-in gate, version floor (numeric compare),
  and sanitized error reasons (no stack traces)
- 40 tests covering: contract hash, key handling, sig verification,
  schema validation, version compare, all sync paths (disabled, opt_out,
  invalid_signature, invalid_schema, stale, updated, error), auth header
  injection, and cache-untouched assertions for every failure mode

* feat(radar): read-time overlay merge rules over the free catalog

Pure function applyFeed() merges the cached Radar feed over the static
baseline catalog at read time, honoring 4 rules:

1. Feed never overwrites a local override field.
2. enabled:false disables the entry with disabledBy:"radar" provenance.
3. User-added entry NOT in the feed survives untouched.
4. User deletion tombstone prevents feed resurrection.

getRadarCatalog() accessor in index.ts: flag off / no cache / corrupt
payload all fall back to baseline. Valid cache applies the overlay and
returns feed metadata (version, tier, fetchedAt).

TDD: 19 tests (4 rules + dedup + origin + accessor flag/cache/corrupt/
valid/bad-feed + baselineToMergedEntries converter).

* feat(dashboard): radar catalog and guided setup screens

- API routes: GET /api/radar/catalog, POST /api/radar/sync, POST /api/radar/settings
  - All gated on RADAR_ENABLED flag (404 when off)
  - Error responses via buildErrorBody(), never raw stack/message
  - Settings never echoes clear supporter key (masked omr_****<last4>)
  - Sync delegates to syncRadar() server-side, never proxies feed URL
- Dashboard pages:
  - /dashboard/radar: 4 states (flag off, opt-in pending, empty, populated)
  - /dashboard/radar/setup?provider=X: guided setup with steps, key URL, test connection
  - Uses existing Card component and next-intl patterns
- Sidebar: radar entry in costs group with icon
- i18n: pt-BR and en keys for radarPage and radarSetupPage namespaces
- Tests:
  - radar-api-routes.test.ts: 11 tests (flag-off 404, flag-on shape, error sanitization)
  - radar-page-state.test.ts: 5 tests (pure state logic)
  - All 90 radar tests pass (including prior 74)

* docs(radar): module doc and flag-off inertia test

Add docs/frameworks/RADAR.md covering the flag gate, the separate data-sync
opt-in and privacy promise, the Ed25519 signature/pinned-key security model,
tiers, the read-time overlay merge rules, and the self-hosting env vars —
plus index entries in CLAUDE.md/AGENTS.md/docs/README.md/REPOSITORY_MAP.md.

Document RADAR_FEED_URL and RADAR_FEED_PUBKEY in .env.example and
docs/reference/ENVIRONMENT.md to satisfy check:env-doc-sync, which was
failing on this branch since the sync.ts commit added the reads.

Add tests/unit/radar-inertia.test.ts as the single canonical place asserting
the "RADAR_ENABLED off => zero behavioral delta" claim end to end: the three
/api/radar/* routes 404, the flag resolves to the definition default with no
override, getRadarCatalog() returns exactly the baseline without touching the
cache, and computeFreeModelTotals() keeps its pinned values with the Radar
module imported alongside it.

* fix(db): renumber radar migration to 135 after collision with 134

The base branch introduced 134_proxy_logs_egress_ip while this branch carried
134_radar_cache_settings; the migration runner rejects duplicate numeric prefixes.
This migration has never been applied to a real database (the PR is unmerged), so
no retroactive isSchemaAlreadyApplied guard is needed.

* i18n(radar): translate radar catalog and setup strings to all locales

The UI-coverage ratchet measures (present - placeholder) / total_en, so the
__MISSING__ sentinels that i18n:sync-ui writes do not count as covered — only
real translations restore the metric. Scoped to this PR's namespaces
(radarPage, radarSetupPage, sidebar.radar*) instead of a bulk sync, which would
have pulled ~978 unrelated pending keys into this diff.

Placeholders and code identifiers verified preserved across all 1682 strings.

* fix(radar): trust the served-tier header instead of the signed body field

The signed feed body always carries tier:"live" by design (one signed
artifact per version — rewriting the field server-side per request
would break the exact-bytes Ed25519 signature). The server now returns
the tier ACTUALLY served via the x-omniroute-feed-tier response
header, so free users on a delayed community snapshot no longer see
"Ao vivo (tempo real)" in the UI.

sync.ts now reads and validates that header (falling back to the
body's tier only when the header is absent or holds an unrecognized
value) and stores the served tier in the cache; index.ts already
surfaces cache.tier to the UI unchanged.

* test(combo): shorten an assert message that exceeded the line limit

The assertion added by #9507 was 104 chars, so prettier reformatted it into
five lines on the next commit that touched the file, pushing it past its
frozen size (3449) and failing check:file-size. The message is shortened
(the issue reference stays in the comment directly above); the assertion
itself is unchanged, and the file is back to 3448 lines and prettier-clean.

* i18n(radar): use the canonical zh-TW glossary terms

The machine translation produced retired renderings the glossary gate blocks:
供應商 for provider (canonical 提供者) and 文檔 for documentation (canonical 文件).
Fixed across the 11 affected radar strings; tests/unit/i18n-glossary-consistency-check.test.ts
is back to 17/17.

* fix(radar): point the default feed URL at the domain that exists

radar.omniroute.dev was a placeholder for a domain that was never registered,
so an out-of-the-box sync would fail DNS resolution for every user. The live
feed is served from radar.omniroute.online (the subdomain the design always
specified), now behind Cloudflare TLS. Forks still override it via
RADAR_FEED_URL.

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
2026-08-06 05:58:58 -03:00
Diego Rodrigues de Sa e Souza
103bab99d5 fix(cli): prefer IPv4 DNS for spawned servers (#9209)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
2026-08-05 22:40:06 -03:00
Diego Rodrigues de Sa e Souza
cfd9210bfc feat(cli): deliver the Antigravity credential straight to the remote install (#8834)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates — only pre-existing audit.test.ts flake).
2026-08-05 22:39:39 -03:00
Chedrian07
fd62cb1152 fix(docker): publish the Redis sidecar on loopback instead of 0.0.0.0 (#9286)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:45:56 -03:00
Zius
a5e0a96f8a fix: prevent false 'Failed to save connection' error when adding providers (#8912)
Validated in local merge-train (devbox-vm-06-dev002) @ combined-tip (FAST gates green: static + changed tests + vitest — only pre-existing audit.test.ts flake). Evidence: /home/diegosouzapw/dev/proxys/OmniRoute/.claude/worktrees/merge-train-20260805-213228-suite.log
2026-08-05 21:41:33 -03:00
Diego Rodrigues de Sa e Souza
c2bf8d5492 fix(cli): route claude-code OAuth to the Anthropic claude browser-PKCE flow instead of the unrelated command-code provider (#9474)
Closes #9474
2026-08-05 16:49:51 -03:00
Diego Rodrigues de Sa e Souza
e64eecf852 fix(cli): probe PATH for claude.exe/codex.exe on Windows before falling back to the .cmd shim (#9454)
Closes #9454
2026-08-05 16:47:54 -03:00
Diego Rodrigues de Sa e Souza
d2a9378afb fix(cli): re-verify running binary version after update install and warn on shadowing local install (#9475)
Closes #9475
2026-08-05 16:47:38 -03:00
Diego Rodrigues de Sa e Souza
08d7305af0 fix(cli): stop the supervisor before the child so omniroute stop no longer reports success while the supervisor respawns the server (#9455)
Closes #9455
2026-08-05 16:47:23 -03:00
Suman
8e0d7e4ddd fix(cli): escape codex args and stop aborting on exit in launch-codex (#8856)
* fix(cli): escape codex args and stop aborting on exit in launch-codex

`launch-codex` spawns `codex.cmd` with `shell: true` on Windows, so Node joins
argv with plain spaces and no escaping (DEP0190). This mangles every Windows
invocation, not only the ones with a multi-word user argument, because the
injected `-c` provider flags carry quoted TOML values:

  ["-c","model_provider=omniroute", ...,
   "model_providers.omniroute.base_url=http://localhost:20128/v1",
   "fix","the","bug"]

cmd.exe strips the TOML quotes (`model_provider=omniroute` no longer parses as
a TOML string), splits multi-word arguments, and swallows everything after an
unquoted `&`. The same defect was fixed for `launch` in #8837; this ports it to
`launch-codex`, which that PR disclosed but left unfixed.

- extract the escaping into `bin/cli/utils/winShellArgs.mjs` and reuse it from
  both launchers instead of keeping a private copy in `launch.mjs`
- quote the codex argv (provider flags + profile + pass-through args) on the
  win32 shell path; argv is untouched off Windows, where no shell is involved
- replace `process.exit()` in the command action with `process.exitCode`: on any
  non-zero child exit it aborted with the libuv `!(handle->flags &
  UV_HANDLE_CLOSING)` assertion while the inherited stdio handles were closing

Test: `tests/unit/cli/launch-codex-windows-spawn-args.test.ts` pins the exact
encoding with golden strings (the cmd.exe round-trip is Windows-only and skips
on Linux CI, so without goldens CI would guard nothing) and round-trips the real
provider flags through a probe `.cmd` shim that forwards `%*`.

* docs(changelog): add fragment for #8856

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-28 11:50:00 -03:00
Suman
2675b650b5 fix(cli): escape claude args and stop aborting on exit in omniroute launch (#8837)
* fix(cli): escape claude args and stop aborting on exit in omniroute launch

Windows launches go through spawn(..., { shell: true }), which joins argv
with plain spaces and no escaping (Node DEP0190). Any argument containing a
space was split, so `omniroute launch -p "two words"` reached claude as
`-p two` plus stray positionals, and the prompt was silently truncated.

Escape each argument for cmd.exe instead: CRT argv rules first (double the
backslashes preceding a quote, escape embedded quotes, wrap in quotes), then
cmd metacharacters caret-escaped twice. The second pass is required because
claude.cmd is an npm shim that re-parses %* on the way to node; with a single
pass arguments still truncated at the first `&` or `|`.

The command action also called process.exit() on any non-zero exit. That tore
the loop down while the exited child's inherited stdio handles were still
closing and aborted the process with a libuv assertion
(!(handle->flags & UV_HANDLE_CLOSING), src/win/async.c:94, exit 0xC0000409)
instead of returning claude's exit code. Set process.exitCode and let the
loop drain.

Extracts resolveClaudeSpawn() alongside the existing resolveCodexSpawn()
precedent so both the platform choice and the escaping are unit-testable.

Tests: tests/unit/cli/launch-windows-spawn-args.test.ts (new, 7 tests).
Four pure-function tests plus golden strings pin the exact encoding on every
platform; a Windows-only test round-trips argv through a real npm-style .cmd
shim and asserts embedded quotes, `&`, `|`, `%PATH%`, `^`, `!`, a trailing
backslash and an empty string all arrive byte-identical.

Note: bin/cli/commands/launch-codex.mjs carries the identical defect (same
shell:true concatenation, same process.exit) and is left unchanged here.

* docs(changelog): add fragment for #8837

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-28 10:24:54 -03:00
AmirHossein Rezaei
df9550fce9 fix(cli): prepare Next.js cache dir on Android/Termux before serve (#8593)
* fix(cli): ensure `~/.cache` is created and `XDG_CACHE_HOME` is set before Next.js loads on Android/Termux to prevent silent HTTP 500 errors due to instrumentation hook failures (#8519)

* chore(quality): ignore XDG_CACHE_HOME in the env/docs contract scanner

XDG_CACHE_HOME is an XDG Base Directory spec variable set by the OS or the
operator, never OmniRoute product config — the same reason XDG_CONFIG_HOME is
already ignored. The Android/Termux cache-dir preparation added here reads it
to honor an operator-set cache location, which made check-env-doc-sync demand
an .env.example/ENVIRONMENT.md entry for a variable we do not own.

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

* build(pack): require bin/cli/utils/ensureAndroidCacheDir.mjs in the tarball

bin/omniroute.mjs imports this module at startup to prepare the Next.js cache
dir before serve on Android/Termux. bin/cli/ is only an allowlist PREFIX, so a
file missing from the tarball would not fail the unexpected-paths check — it
would ship a CLI that throws ERR_MODULE_NOT_FOUND on the very platform this
change targets. Registering it makes the absence loud, same guard class as
storageKeyProvision.mjs and versionFastPath.mjs.

Caught by tests/unit/pack-artifact-entrypoint-closures.test.ts in the v3.8.49
merge-train.

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

---------

Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-27 19:07:19 -03:00
Harvey Doan
a095ebc43d feat: read INITIAL_PASSWORD env var during setup (#8439)
* feat: read INITIAL_PASSWORD env var during setup

Allow users to set the admin password via the INITIAL_PASSWORD
environment variable instead of requiring the --password CLI flag
or interactive prompt. Falls between --password flag and interactive
prompt in resolution priority.

* test(cli): cover INITIAL_PASSWORD env var in setup resolvePassword

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

---------

Co-authored-by: linh.doan <linh.doan@be.com.vn>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-26 03:53:28 -03:00
Dizzle
ca79344b40 fix(cli): backup create/auto enable — remove option-shadowing legacy fallback (#8512) (#8516)
Co-authored-by: Max <maxmad64@gmail.com>
2026-07-26 03:53:25 -03:00
Diego Rodrigues de Sa e Souza
13cc128dae fix(cli): fall back to node:sqlite when better-sqlite3 is unavailable in omniroute doctor (#7586) (#8501)
bin/cli/sqlite.mjs::loadSqlite() had no fallback beyond better-sqlite3, unlike
the real server's driver cascade (src/lib/db/adapters/driverFactory.ts::tryOpenSync,
which tries bun:sqlite -> better-sqlite3 -> node:sqlite). On machines without a
working better-sqlite3 native binary, every `omniroute doctor` DB check reported
a false FAIL even when the actual server was healthy via its own driver cascade.

openSqliteDatabase() now falls back to tryOpenSync() when better-sqlite3 fails
to import, reusing the same already-tested cascade the real server uses instead
of re-deriving a second one.

Co-authored-by: ikelvingo <im.kelvinwong@gmail.com>
2026-07-25 04:57:00 -03:00
Austin Liu
d3cdd489be fix(cli): resolve claude.cmd on Windows in omniroute launch (#8246) (#8283)
spawn('claude') without shell:true cannot resolve the .cmd shim
that npm installs on Windows, causing ENOENT and a misleading
'not found in PATH' error. Use claude.cmd + shell:true + windowsHide
on win32, matching the pattern already used in launch-codex.mjs.

Co-authored-by: Austin Liu <austinliu@Austins-MacBook-Air-3.local>
2026-07-23 11:06:28 -03:00
Diego Rodrigues de Sa e Souza
f23d7770ec feat: zh-CN terminology glossary + consistency gate + normalization pass (#8038) (#8166)
One-shot 提供商->提供者 normalization across src/i18n/messages/zh-CN.json
(679 substitutions) and bin/cli/locales/zh-CN.json (54), mirroring #8024's
zh-TW pass. Adds a versioned terminology glossary
(scripts/i18n/glossary/zh-CN.json), a protected-names list
(scripts/i18n/glossary/protected-terms.json), and a pure-function
consistency check (scripts/i18n/check-glossary-consistency.mjs,
npm run i18n:check-glossary) wired into CI as the i18n-glossary-zhcn job.
zh-CN added to the visual-QA harness default locales. Complements the
existing parity (check-ui-keys-coverage.mjs) and ICU (validate_translation.py)
gates without replacing them.
2026-07-22 15:54:26 -03:00
Diego Rodrigues de Sa e Souza
40bd70c9cd fix(cli): surface the real spawn error in process supervisor (#8091) (#8158) 2026-07-22 11:28:20 -03:00
Diego Rodrigues de Sa e Souza
6302a78657 fix(db): register SIGHUP handler and stop force-killing server on win32 stop paths (#8045) (#8148)
Windows console-window close delivers CTRL_CLOSE_EVENT, which Node/libuv maps
to a JS-visible SIGHUP event. initGracefulShutdown() only listened for
SIGTERM/SIGINT, so closing the window never ran cleanup() (WAL checkpoint +
closeDbInstance()), leaving storage.sqlite's WAL un-checkpointed for the next
launch.

Separately, process.kill(pid, "SIGTERM") on win32 unconditionally
force-terminates the target process instead of delivering an interceptable
signal. The CLI's own stop paths (ServerSupervisor.stop() and
runStopCommand()) sent it immediately on every stop, racing and beating the
child's own async graceful shutdown before the WAL checkpoint could run.

Fix:
- src/lib/gracefulShutdown.ts: register a SIGHUP handler alongside
  SIGTERM/SIGINT.
- src/shared/platform/windowsProcess.ts (new): stopProcessGracefully() skips
  the immediate SIGTERM on win32 (letting the target's own CTRL_C/CTRL_CLOSE
  handling run) and polls before escalating to SIGKILL; unchanged immediate
  SIGTERM behavior on POSIX.
- bin/cli/runtime/processSupervisor.mjs and bin/cli/commands/stop.mjs: use
  stopProcessGracefully() instead of an unconditional process.kill(SIGTERM).

Regression tests: tests/unit/graceful-shutdown-sighup-8045.test.ts (reuses
the RED probe from the triage analysis) and
tests/unit/windows-process-stop-8045.test.ts.
2026-07-22 11:27:49 -03:00
lunkerchen
2e6dfda90d i18n(zh-TW): complete Traditional Chinese (Taiwan) translation overhaul (#8024)
- UI messages: 100% coverage (was ~78%). Translated 2871 missing keys,
  eliminated all 420 __MISSING__ placeholders. 0 remaining.
- Terminology: 提供商→提供者 (493 fixes), 令牌→權杖 (88 fixes),
  激活→啟用 (1 fix), 配置→設定 (13 context-aware fixes) in UI messages
- CLI locale: same terminology pass (69 fixes)
- Docs: translated all 26 zh-TW docs (was 3/26). USER_GUIDE, ARCHITECTURE,
  API_REFERENCE, ENVIRONMENT and 20 more now in Traditional Chinese.
- Preserved variable placeholders, ICU plurals, markdown, code blocks

Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
2026-07-22 06:43:46 -03:00
Arul Kumaran
edbce2e35b feat: support Bun bundled SQLite runtime (#7878)
* feat: support Bun bundled SQLite runtime

* fix: harden Bun SQLite backups and params

* docs: keep Node as the only supported runtime; document bun:sqlite as best-effort compatibility path

Co-authored-by: Arul Kumaran <arul@luracast.com>

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-21 16:30:26 -03:00
ToastedPatatas
448257f8f7 fix(autostart): adopt 9Router VBS startup to suppress console flash on Windows (#7925)
Windows auto-start previously wrote a HKCU\Run registry entry that
launched node.exe directly, causing a visible console window at logon.
Closing that window also killed the background server.

Adopt the same approach as 9Router: write a OmniRoute.vbs script to
the Windows Startup folder that calls WScript.Shell.Run with SW_HIDE (0)
so OmniRoute starts fully hidden. The VBS runs serve --no-open --tray
via the existing buildServeExecLine helper.

Legacy migration — enableWin() removes the old HKCU\Run entry so stale
entries don't linger, and isEnabledWin() falls back to checking the
registry for users who enabled autostart before this change.

Co-authored-by: tientien17 <tientien17@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-07-21 11:49:57 -03:00
Diego Rodrigues de Sa e Souza
3f2d88b679 fix(cli): spawn opencode.cmd shim with shell:true on win32 (#7913) (#7964)
* fix(cli): spawn opencode.cmd shim with shell:true on win32 (#7913)

runOpenCodeAuth spawned the opencode.cmd shim with shell:false on win32,
which throws spawnSync EINVAL under Node's hardened child_process
handling (post CVE-2024-27980). Mirrors the fix already applied to
codex (resolveCodexSpawn in launch-codex.mjs, #6263) and
qodercli/Auggie (#6263/#6304): shell:isWin, unchanged elsewhere.

Exported runOpenCodeAuth for testability and added a regression test
covering both the win32 shell:true path and the non-regressing
linux/darwin bare-binary path.

* fix(cli): make opencode --auth win32 spawn testable without module mocks (#7913)

The regression test used t.mock.module, which needs --experimental-test-module-mocks
and fails to even run in CI (TypeError: t.mock.module is not a function → the fix was
unvalidated). Extract a pure resolveOpenCodeAuthSpawn(providerId, platform) helper and
test it directly (win32 → shell:true, linux/darwin → shell:false). Production behavior
of runOpenCodeAuth is unchanged.
2026-07-21 09:15:39 -03:00
Diego Rodrigues de Sa e Souza
68a883f0c6 fix(cli): translate missing sqlite bindings error into actionable guidance (#7868) (#7963)
createSqliteNativeError() in bin/cli/sqlite.mjs only recognized the ABI-mismatch
native error class (NODE_MODULE_VERSION/ERR_DLOPEN_FAILED). It silently passed through
the 'Could not locate the bindings file' class thrown by the bindings package when the
better-sqlite3 native addon was never built/downloaded - the exact case hit by
'npx omniroute reset-password', since npx runs a fresh ephemeral install that never
builds the addon. Users got a raw multi-line path dump instead of guidance.

Widen the condition to also match 'Could not locate the bindings file',
MODULE_NOT_FOUND, and "Cannot find module 'better-sqlite3'", and point users at the
existing self-heal command `omniroute runtime repair`, same as the ABI-mismatch branch
already does.

Regression test: tests/unit/cli-sqlite-bindings-not-found-7868.test.ts - RED against the
reporter's exact error text before the fix, GREEN after.
2026-07-21 08:39:43 -03:00
backryun
65e0aeda79 [Part 1/3]refactor(qwen): replace legacy Qwen Code and remove OAuth provider (#7866)
* refactor(cli): remove legacy Qwen Code integration

* refactor(qwen): remove deprecated Qwen OAuth provider

* feat(cli): rebuild Qwen Code integration for upstream V4

* fix(qwen): clear stale CLI auth on reset

* test(qwen): align retired provider coverage

* fix(db): renumber qwen-cleanup migration 129 -> 130

release/v3.8.49 tip took slot 129 via #7843 (usage_history_codex_strong_identity,
itself renumbered from 128 during the #7838/#7840 base-red cleanup) after this
branch forked; renumber remove_unregistered_qwen_data to 130.

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

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
2026-07-20 17:53:04 -03:00
ToastedPatatas
7d82b72def fix(cli): use rundll32 instead of cmd.exe for Windows browser fallback in dashboard command (#7844)
* fix(cli): use rundll32 instead of cmd.exe for Windows browser fallback in dashboard command

Extract resolveOpenCommand(platform, url) as an exported pure function so tests import the actual production code instead of duplicating logic.

The openFallback function in bin/cli/commands/dashboard.mjs used cmd /c start to open the dashboard URL on Windows, spawning an unnecessary cmd.exe process. Replaces with rundll32 url.dll,FileProtocolHandler which opens the URL directly through the Windows shell handler API without any shell wrapper.

Tests: 5 tests importing the actual resolveOpenCommand function, covering all platform branches (darwin, win32, linux) and URL pass-through. Changelog fragment included.

* chore: rename changelog fragment 7842->7844 to match actual PR number

---------

Co-authored-by: tientien17 <tientien17@users.noreply.github.com>
2026-07-20 15:56:08 -03:00
Rafael Dias Zendron
d813bedf5c fix(cli): register ESM alias resolver for @/ paths under global install (#7808)
* chore(ci): add .mergify.yml to main — Mergify only reads config from the default branch (#7168)

* fix(ci): add the auto-enqueue pull_request_rule to the Mergify config (queue_conditions alone are eligibility-only) (#7179)

* fix(ci): migrate Mergify auto-enqueue to merge_protections_settings.auto_merge_conditions (rules-based path is EOL 2026-07-16) (#7216)

* fix(ci): drop Mergify batch settings (batching is a paid-tier feature; free plan queue is serial) (#7220)

* fix(ci): merge queue tolerates the advisory dast-smoke failure (its GH-hosted build hang dequeued every attempt) (#7225)

* test(ci): make the #6634 selfref guard hermetic — main's copy hard-fails every PR (#7341)

main's copy of this test still does git I/O inside a unit test:

    const baseSrc = git(['show', 'origin/main:' + FILE]);

Runners check out a shallow single ref, so origin/main does not resolve and the
test dies with 'fatal: invalid object name origin/main'. Every PR into main
fails Unit Tests (7/8) on it — today that is #7313, #7315, #7316, #7334, #7336
and #7337, six PRs red on a defect none of them introduced. #7313 has no other
red at all.

release/v3.8.49 already carries a fix (2e42b8efc, #7174: try/catch, fetch
origin/main on demand, t.skip() when unreachable), but it only reaches main at
release time — so main stays broken for the whole cycle. Cherry-picking it would
also import a new problem: PR Test Policy classifies t.skip() as a silenced
assertion, which we watched it correctly catch on #7300 today.

This is the hermetic version instead (ported from #7327, which does the same for
the release branch): read the file straight off disk, compare against an empty
base so baseTaut/baseExtTaut are 0 — the strictest possible comparison point —
and call evaluateMasking() directly. No git ref, no fetch, no skip, nothing the
runner's checkout depth can break.

The #6634 regression stays covered: the guard's logic lives in
SELF_TEST_FIXTURE_RE (check-test-masking.mjs:337), not in the test. Proven both
ways on main before committing — neutralise SELF_TEST_FIXTURE_RE to /$^/ and
the test FAILS; restore it and it passes 2/2, with check-test-masking.mjs left
byte-identical.

Co-authored-by: growab <nekron@icloud.com>

* chore(quality): tighten main's coverage baseline to the CI's real numbers (#7347)

main's ratchet had been failing --require-tighten on every PR: 11 metrics
improved but the baseline was never tightened. Same class as the #6634
selfref guard — an infra fix that lands only on the release branch leaves
main red for the whole cycle, and every PR into main pays for it.

Values are the merged-coverage numbers from a run on main itself (a local
run measures ~68% vs CI's ~80%; the baseline's own note warns about that
gap). Only the 11 coverage values change — gitleaks and semgrepFindings
keep main's own state.

No changelog fragment: #7326 carries it on release/v3.8.49, and a second
one here would double the entry at release time.

* Add cliproxy provider exposure controls and manifest injection (#7329)

* feat(fusion): let judge use its own knowledge and override the panel (#6804)

The judge prompt said to write an answer 'grounded in that analysis',
implicitly capping output at the panel's union. When all panel members
miss or are collectively wrong on something, the judge should apply its
own reasoning as a full participant and override consensus, while keeping
an honesty guard against fabrication. Adds a regression test.

Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>

* fix(api): raise provider apiKey cap for cookie-based web providers (#6715) (#6759)

* fix(cli): fall back to settings.json when Claude Code binary is unresolvable (#6701) (#6734)

getCliRuntimeStatus() only ever answered `installed` from binary resolution
(known install paths + where/which PATH search), so a stale PATH, moved
binary, or uncatalogued install method reported "not found" even when
~/.claude/settings.json proved the CLI was installed and used before —
regressing behind upstream 9router's checkClaudeInstalled(), which already
falls back to the settings file when where/which fails.

withSettingsFallback() (new src/shared/services/cliInstallFallback.ts, kept
out of the frozen cliRuntime.ts to respect its file-size ceiling) restores
that parity: only when the binary lookup's own reason is "not_found" (never
for deliberate security rejections like unsafe/relative env overrides or
symlink escapes) and the tool's settings file exists on disk.

* fix(providers): honor explicit thinking.budget_tokens 0 in openai->gemini transform (#6813) (#6821)

The transform forwarded the Claude-style thinking.budget_tokens into
generationConfig.thinkingConfig.thinkingBudget, but the presence check was
truthy (&& thinking.budget_tokens). An explicit budget_tokens: 0 — the
natural way to disable thinking — is falsy, so it was dropped and the
request fell through to the default thinkingConfig injection, making the
model think despite an explicit request for zero. Use an explicit numeric
check so 0 is honored as thinkingBudget 0; includeThoughts is only set for
a non-zero budget.

* fix(compression): reconcile outer vs per-engine token counts (#6488) (#6741)

* fix(compression): reconcile outer vs per-engine token counts on degenerate output (#6488)

Outer originalTokens/compressedTokens (real tiktoken counter over extracted
message text) diverged from engineBreakdown[0]'s counts (a crude
JSON.stringify(requestBody).length/4 estimate), worst on small/degenerate
inputs where JSON structural overhead dominates. A single-engine breakdown
entry represents the exact same before/after transformation as the overall
response, so reconcileSingleEngineTokens() now overwrites that one entry's
counts with the outer, more accurate figures; multi-step pipeline
breakdowns are left untouched.

* chore(6741): resolve release sync — CHANGELOG.md restored to release tip, entry moved to changelog.d fragment (fragments-first)

* fix(api): accept enableRenderers in RTK compression config schema (#6703) (#6757)

* fix(db): break probe-failed/restore loop on large storage.sqlite (#6632)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

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

* feat(cursor): add Opus 4.8, Fable 5, and Sonnet 5 model families (#6779)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's cursor registry + test changes.

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

* fix(translator): read PDF/video file attachments for Gemini/Antigravity and Claude (#6790)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's translator + test changes.

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

* fix(codex): strip include from compact responses requests (#6805)

* fix(codex): strip include from compact responses requests

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

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

* fix(6805): move include-strip assertion to standalone test file to keep executor-codex.test.ts under frozen size cap

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

---------

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

* fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6769)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

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

* fix(bootstrap): filter empty process.env values to prevent Docker env crash loop (#6828)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
keeps only the author's bootstrap change.

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

* fix(providers): update SenseNova Token Plan support (#6330)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's constants/registry/snapshot deltas
were re-applied cleanly onto the release tip.

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

* fix(providers): classify 404 as MODEL_NOT_FOUND to stop retry storm (#6829)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's chatCore/errorClassifier deltas were re-applied cleanly onto the release tip.

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

* fix(api): accept all catalog engines on compression PUT schema (#6792)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR). Resolved the release's OmniGlyph engine addition
additively (types.ts/compression.ts kept both 'relevance' and 'omniglyph') and extended
stackedPipelineStepSchema + STACKED_PIPELINE_ENGINE_INTENSITIES with the omniglyph branch
so the ENGINE_CATALOG-parity test passes.

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

* fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717)

* fix(api): point CLI health command at /api/monitoring/health (#6677)

bin/cli/commands/health.mjs called GET /api/health, a route that was
moved to /api/monitoring/health without updating the CLI; the top-level
/api/health handler never existed on disk (only degradation/ and ping/
sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at
/api/monitoring/health and read its real payload shape
(activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage)
instead of the old nonexistent requests/breakers/cache/memory fields.

* chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first)

* chore(cursor): add Grok 4.5 effort/fast model IDs (#6774)

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

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

* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED (#6791)

* fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); keeps only the author's changes.

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

* refactor(deepseek): extract done-terminator helper to keep frozen file under cap

Extracts the FINISHED-drain scheduler and finish-once guard added for
the [DONE] terminator fix (#6777) into a new
deepseek-web-done-terminator.ts module, so deepseek-web.ts stays under
its frozen line cap (1148). Behavior is unchanged.

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

---------

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

* feat(models): add capability override UI (#6727)

* feat(models): add capability override UI

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); renumbered the migration 118 -> 119 to resolve the
collision with 118_provider_param_filters.sql already on release/v3.8.47; the author's
i18n/localDb deltas were re-applied cleanly onto the release tip.

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

* fix(6727): import model-capability-overrides DB fns directly (not via localDb barrel) to keep localDb under file-size cap; aligns with anti-barrel convention

* chore(db): satisfy known-symbols contract for modelCapabilityOverrides

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>

* fix(cursor): use Agent CLI build id for x-cursor-client-version (#6795)

* fix(cursor): use Agent CLI build id for x-cursor-client-version

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's .env.example/docs deltas were
re-applied cleanly onto the release tip.

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

* chore(changelog): re-sync CHANGELOG.md to release tip (restore #6701 bullet)

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

---------

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

* fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584) (#6718)

* fix(startup): rename reasoningControls.ts to avoid webpack casing collision (#6584)

* chore(6718): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582) (#6720)

* fix(build): suppress Turbopack over-bundling warning from agentSkills generator (#6582)

generator.ts builds outputBase from a non-literal outputDir parameter, so
Turbopack's file-tracing analyzer can't narrow it and emits an "Overly broad
patterns" warning per entry point that imports the module (603 warnings on
v3.8.46, up from 379). The fs access is legitimate and bounded, so
next.config.mjs now suppresses this specific diagnostic via turbopack.ignoreIssue,
mirroring the existing webpack.ignoreWarnings precedent in the same file.

* chore(6720): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651) (#6721)

* fix(providers): drop image_generation for Codex Spark models regardless of plan (#6651)

* chore(6721): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687) (#6722)

* fix(providers): stop quota card re-sorting Codex/GLM bars by remaining % (#6687)

QuotaCardExpanded.tsx unconditionally re-sorted quotas by remaining
percentage via sortQuotasByRemaining(), discarding the deterministic
CODEX_QUOTA_ORDER/GLM_QUOTA_ORDER window order quotaParsing.ts's
sortCodexOrder()/sortGlmOrder() had already established. A new
hasFixedQuotaOrder() + resolveQuotaDisplayOrder() skip the re-sort for
providers with a fixed window order (codex, glm family), threading
providerId from QuotaCard.tsx through to the display layer.

Regression guard: tests/unit/quota-card-expanded-fixed-order-6687.test.ts

* chore(6722): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559) (#6725)

* fix(startup): lazy-import ioredis in rateLimiter to fix MCP ERR_MODULE_NOT_FOUND (#6559)

* chore(6725): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(resilience): resolve fp-pinned combo account back to real connection id (#6696) (#6732)

* fix(resilience): resolve fp-pinned combo account back to real connection id (#6696)

* chore(6732): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561) (#6735)

* fix(api): Responses passthrough emits event-only SSE frames after filtering commentary output (#6561)

The #6199 commentary-drop `continue;` branches in stream.ts skipped the
data: line for a dropped commentary event but never cleared the
already-buffered event: line for the same frame, so the next blank line
flushed the stale event: line alone -- an event-only SSE frame that
crashes the OpenAI Python SDK's json.loads(). Both drop sites now call
clearPendingPassthroughEvent() before continue. The commentary-drop
decision was extracted into a new responsesCommentaryDrop.ts module so
the fix does not grow the frozen stream.ts.

* chore(6735): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(api): emit reasoning_content on claude-web + v0-vercel-web SSE (#6662) (#6743)

* fix(api): emit reasoning_content on claude-web + v0-vercel-web /v1/chat/completions SSE (#6662)

* chore(6743): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation (#6704)

* fix(sse): unwrap bare {function:{…}} tools in openai→claude translation

Some OpenAI-shape clients send a tool as a bare `{ function: {...} }`
object, omitting the spec-required `type: "function"` parent wrapper.
The tools-mapping in openai-to-claude.ts (~line 366) only unwrapped
`tool.function` when `tool.type === "function"` was ALSO true, so a
bare-function tool fell through to `toolData = tool` (the wrapper
itself, with no `.name`), producing an empty `originalName` and
silently dropping the tool from the translated request — worse than
a 400, since the caller has no signal the tool never made it
upstream. Unwrap `tool.function` whenever present, independent of
the parent `type` field. Regression guard:
tests/unit/openai-to-claude-bare-tool.test.ts.

Co-authored-by: Samir Abis <me@samirabis.com>
Inspired-by: https://github.com/decolua/9router/pull/2473

* chore(6704): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: Samir Abis <me@samirabis.com>

* fix(oauth): avoid bare-email dedup of Codex OAuth logins (#6706)

* fix(oauth): avoid bare-email dedup of Codex OAuth logins

When an incoming Codex OAuth connection has no verifiable workspace/account
id, do not merge it into an existing row on email match alone — that
silently overwrote the other account's token pair. Require a matching
chatgptUserId (a stable per-account JWT id) before merging; otherwise
insert a distinct connection row.

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2477

* chore(6706): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>

* fix(sse): skip thinkingConfig for gemma models in openai→gemini translation (#6708)

open-sse/translator/request/claude-to-gemini.ts already guards against
sending thinkingConfig for gemma-4-* models (Gemma doesn't support it —
Vertex returns 400: "Thinking budget is not supported for this model"),
but the OpenAI-shape path (openai-to-gemini.ts) lacked the same guard, so
OpenAI-shape clients hitting a vertex gemma-4-* model still got a 400.

Mirrors the existing claude-to-gemini.ts guard: wrap the reasoning_effort
and Claude-shape thinking.budget_tokens branches with a model.startsWith
("gemma-4") check. Branch 3 (default includeThoughts for modern Gemini
models) already excludes non-"gemini" model ids and needed no change.


Inspired-by: https://github.com/decolua/9router/pull/2480

Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com>

* fix(codex): surface capacity errors embedded in 200-OK SSE streams (#6710)

* fix(codex): surface capacity errors embedded in 200-OK SSE streams

Codex sometimes answers with HTTP 200 and a text/event-stream body whose
payload carries a transient error mid-stream (e.g. "Selected model is at
capacity...", server_is_overloaded, service_unavailable_error). Because the
outer HTTP status was 200, this looked like a successful response to every
caller — no retry, no circuit breaker, and no combo/account fallback ever
engaged, so a healthy account sat idle while the request silently failed or
truncated.

Add peekCodexSseTransientError() to open-sse/executors/codex.ts: it peeks the
first bytes of a text/event-stream Codex response, pattern-matches the known
transient-error signatures, and converts a match into a real 503 Response via
errorResponse() (Hard Rule #12 — sanitized, never raw upstream text). A 503 is
already a recognized provider-failure status in accountFallback.ts, so combo
routing and connection cooldown pick it up automatically. When no error
signature is found, the peeked prefix is prepended back onto the remaining
upstream body so the passthrough stays byte-identical to the unmodified
response.

Regression guard: tests/unit/codex-sse-capacity-fallback.test.ts — a
model-at-capacity payload and a server_is_overloaded/service_unavailable_error
payload both convert to 503; a normal single-chunk SSE stream and one split
across multiple network chunks both reassemble byte-for-byte unchanged.

Inspired-by: https://github.com/decolua/9router/pull/2452 (sub-bug #3 only —
OmniRoute already covers PR #2452's other two sub-bugs: service_tier "fast"
normalization and reasoning_effort "max" normalization).

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* chore(6710): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap (#6712)

* fix(volcengine): clamp Kimi max_tokens to Ark endpoint cap

VolcEngine Ark's Kimi coding-plan endpoint (ark.cn-beijing.volces.com)
enforces max_tokens <= 32768 server-side and returns 400 "integer above
maximum value, expected a value <= 32768" for anything over that ceiling.
OmniRoute's StripRule only supported dropping params outright, with no
numeric clamp mechanism, so a client sending a larger max_tokens (common
default, e.g. 65536) 400s outright against volcengine's kimi-k2-5-260127.

The 32768 cap is independently confirmed against two live-endpoint bug
reports hitting this exact Ark endpoint for both kimi-k2.5 and
kimi-k2.7-code (NousResearch/hermes-agent#51773, MoonshotAI/kimi-cli#1124),
not just upstream's own value — same cap upstream 9router#2460 uses.

StripRule gains two optional fields: `clampToModelMaxOutput` (clamp to the
model's own catalog maxOutputTokens ceiling, when set) and `maxOutputCap`
(a fixed endpoint-imposed ceiling); when both apply, the lower wins. The
new rule is scoped to the literal id `kimi-k2-5-260127` (OmniRoute's real
volcengine Kimi model, not upstream's `Kimi-K2.7-Code`), not a broad
/kimi/i regex, so it can never clamp an unrelated future Kimi listing
whose Ark cap may differ. glm-4-7-251222 (the other volcengine model) is
unaffected.

Inspired-by: https://github.com/decolua/9router/pull/2460
Co-authored-by: whale9820 <whale9820@users.noreply.github.com>

* chore(6712): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

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

* fix(antigravity): surface aborted Gemini tool calls off end_turn (#6713)

* fix(antigravity): surface aborted Gemini tool calls off end_turn

Gemini/Antigravity aborts a turn with finishReason MALFORMED_FUNCTION_CALL
(or a sibling like UNEXPECTED_TOOL_CALL) instead of completing cleanly. Both
Claude-facing translators collapsed these to a clean end_turn, hiding the
aborted tool call as a successful completion:
- the OpenAI hub path (openai-to-claude.ts convertFinishReason default), and
- the DIRECT Gemini->Claude path (gemini-to-claude.ts), which is the one
  Claude Code actually hits through an antigravity/Gemini-routed model.
Add isAbortFinishReason() to finishReason.ts and map these reasons to
tool_use on both paths; genuinely unknown reasons still fall back to end_turn.

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/2462

* chore(6713): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>

* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (#6729)

* fix(translator): strip empty cloud_base_branch from Cursor Subagent tool call (port from 9router#2446)

The Responses->Chat tool-arg cleanup (stripEmptyOptionalToolArgs) only stripped
empty-string/empty-array optional args for Claude Code's Read tool. Cursor's local
Subagent tool call therefore passed through with the cloud-only field
cloud_base_branch: "", which Cursor rejects ("cloud_base_branch may only be specified
when environment equals cloud") before starting the subagent. Extend the cleanup to an
allowlist of Read + Subagent; arbitrary tools stay untouched.

Reported-by: like3213934360-lab (https://github.com/decolua/9router/issues/2446)

* chore(6729): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* fix(translator): defer content_block_start until GLM streams the tool name (#6730)

* fix(translator): defer content_block_start until GLM streams the tool name (port from 9router#2077)

GLM 5.2 (and similar OpenAI-compatible upstreams) stream a tool call's id and
function.name across separate SSE delta chunks. The openai-to-claude streaming
translator emitted content_block_start immediately on the id-only chunk with an empty
name; the Claude SSE protocol cannot patch a block after emission, so the later
name-only chunk was dropped and Claude Code rejected the tool_use with an empty tool
name / "No such tool available:". Defer content_block_start until the name arrives
(start on args if they arrive first), and emit a start for any orphaned id-only tool
call at finish so content_block_stop is never orphaned.

Reported-by: itiwant (https://github.com/decolua/9router/issues/2077)

* chore(6730): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat(dashboard): add search to Playground model picker dropdown (#4086) (#6811)

* feat(dashboard): add search to Playground model picker dropdown (#4086)

The shared ModelSelectModal (combo builder + CLI-code cards) already had
search, but the Playground's raw model <select> in StudioConfigPane stayed
a flat unsearchable list - unusable once a provider like OpenRouter
contributed 50+ models.

Adds a search input above the dropdown that filters options via
filterModelsByQuery() (Turkish-safe accent/case-insensitive match, reusing
matchesSearch()). The currently selected model always stays pinned in the
list even when it doesn't match the query, so typing never silently swaps
the active selection. Reuses the existing common.search i18n key already
translated in all 42 locales - no new key needed.

* chore(6811): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat: request count log per provider, per date (#4009) (#6812)

* feat(dashboard): request count log per provider, per date (#4009)

Some providers bill by request rather than by token, so operators need
a plain per-provider, per-date request count breakdown, not just token
aggregates. Adds a new getProviderDailyUsageRows() aggregation query
(src/lib/db/usageAnalytics.ts), a dedicated GET
/api/usage/requests-by-provider-date route (kept separate from the
frozen /api/usage/analytics route to respect the file-size baseline),
and a sortable, single-date-filterable table on Dashboard -> Analytics.

Closes #4009

* chore(6812): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

* feat(xai): route xAI clients to Grok native /v1/responses endpoint (#6709)

* feat(xai): route xAI clients to Grok native /v1/responses endpoint

xAI ships a native /v1/responses endpoint (https://api.x.ai/v1/responses)
alongside /v1/chat/completions, but XaiExecutor extended BaseExecutor
without overriding buildUrl(), so every request always resolved to the
static chat-completions baseUrl regardless of target format — the last
genuinely-missing slice of decolua/9router#2439 (grok-build-0.1, the
reasoning-effort suffix routing, and bare grok-* routing were already
ported in prior cycles).

Add responsesBaseUrl to the xai registry entry and tag
grok-4.20-multi-agent-0309 (upstream's own Responses-only id) with
targetFormat: "openai-responses", mirroring the existing model-tag-driven
routing pattern already used by the gh executor (9router#102) and the
"openai" -pro heuristic in open-sse/executors/default.ts — the per-model
registry tag is the single source of truth that also drives chatCore's
body translation, so URL and body stay in lockstep. XaiExecutor.buildUrl
now checks getModelTargetFormat("xai", model) and resolves to the native
Responses endpoint only for tagged models, leaving every other grok-*
model on the existing chat-completions bridge.

TDD: tests/unit/executor-xai.test.ts adds a RED-then-GREEN case asserting
grok-4.20-multi-agent-0309 resolves to https://api.x.ai/v1/responses and
a control case asserting grok-4.3 still resolves to
https://api.x.ai/v1/chat/completions.

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2439

* chore(6709): re-sync onto release tip; CHANGELOG → changelog.d fragment (fragments-first)

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* fix(resilience): route remaining credential-selection call sites through quota preflight (#6686) (#6742)

* fix(resilience): route remaining credential-selection call sites through quota preflight (#6686)

* chore(6742): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638) (#6731)

* fix(resilience): apikey-provider 429s honor explicit quota-exhausted text (#6638)

Ollama Cloud (and any other apikey-category provider) 429s skipped body-text
quota classification entirely; a genuine multi-day quota exhaustion was
misclassified as a plain rate_limit_exceeded with a few seconds of cooldown,
so combo routing retried the account immediately. shouldPreserveQuotaSignals()
now lets an explicit quota-exhausted signal (looksLikeQuotaExhausted) override
the apikey-category default, and parseDayGranularityResetMs() adds day-
granularity reset-hint parsing ("...reset in 3 days.") alongside the existing
Xh/Ym/Zs parsing.

Regression guard: tests/unit/issue-6638-ollama-quota.test.ts (RED before the
fix, GREEN after). Aligned two tests/unit/account-fallback-service.test.ts
cases that had codified the old buggy behavior for apikey-provider quota
text.

* chore(6731): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709) (#6817)

* feat(resilience): weekly-429 cooldown for fetcher-less providers (#3709)

Ollama Cloud free-tier accounts have a hard WEEKLY request cap. On cap the
upstream returns 429 "you (<account>) have reached your weekly usage
limit", but ollama-cloud is an apikey-category provider, so the existing
oauth-only shouldUseQuotaSignal gate in checkFallbackError skips the
subscription-quota-text classifier (Issue #2321) for its 429s -- the
account fell through to the generic exponential backoff (~1s, capped at
2min) and got retried every few minutes for the rest of the week (one
account took 285x429 in 48h).

Adds a new, ungated weekly-usage-limit text classifier that applies a 24h
QUOTA_EXHAUSTED cooldown regardless of provider category. Extracted the new
classifier -- together with the existing #2321 subscription-quota logic --
into a new open-sse/services/quotaTextCooldowns.ts module so the frozen
accountFallback.ts (file-size-baseline cap) didn't have to grow; net effect
shrinks accountFallback.ts by 20 lines.

This is Phase A of the plan (open-sse/services/accountFallback.ts:1038-1045
"weekly-429 cooldown"); Phase B (generic local request-counter preflight for
manual provider_plans dimensions) is a separate, larger follow-up per the
plan's own phasing.

* chore(6817): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576) (#6726)

* fix(providers): Kiro adaptive-thinking allowlist excludes sonnet-4.5/haiku-4.5 (#6576)

* chore(6726): re-sync onto release tip; CHANGELOG entry → changelog.d fragment (fragments-first)

* test(kiro): migrate selector-strip test to claude-sonnet-5 (only Kiro adaptive-thinking model, #6576)

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

* chore(quality): rebaseline complexity 2053->2054 (merge-burst drift, v3.8.47)

Inherited drift from today's /implement-prs merge burst (~36 PRs). check:complexity
does not run on the PR->release fast-path, so the branch accrued +1 unmeasured. No
orphan/feature PR introduces a NEW violation (complexity-net-zero); the only flagged
function is the pre-existing getResolvedModelCapabilities. Owner-approved rebaseline
to unblock the FQG of ~7 green-except-complexity orphans.

* chore(stryker): register ollama-quota covering tests (merge-burst drift, v3.8.47)

The 3 covering unit tests from #6731/#6817/#6742 (issue-6638-ollama-quota,
ollama-cloud-weekly-quota-cooldown-3709, issue-6686-quota-preflight-coverage)
exist on release but were never added to tap.testFiles when those PRs merged.
Completes the registration so mutant kills count; unblocks every PR touching a
mutated module. Part of the owner-approved merge-burst drift cleanup.

* fix: auto-start WS server in-process and change default port to 20132 (#6072)

* feat: change default LIVE_WS_PORT from 20129 to 20132

Update the default WebSocket port for the live dashboard server from 20129 to 20132 across all configuration files, documentation, code comments, and tests. Also consolidate OMNIROUTE_DISABLE_LIVE_WS and OMNIROUTE_ENABLE_LIVE_WS into a single OMNIROUTE_ENABLE_LIVE_WS flag. Wire the live WebSocket server to start in-process via instrumentation-node.ts.

* feat: clarify NEXT_PUBLIC_LIVE_WS_PUBLIC_URL path usage and derive upgrade path from URL

Update .env.example and ENVIRONMENT.md to document that the pathname portion of NEXT_PUBLIC_LIVE_WS_PUBLIC_URL (e.g. /live-ws) is used as the WebSocket upgrade path by the dev proxy, handshake response, and client connection logic.

Extract deriveLiveWsPath() into shared/utils/wsPath.ts and wire it through:
- src/app/api/v1/ws/route.ts — handshake response path field
- src/hooks/useLiveDashboard.ts — build

* fix: use the standard URL API to safely parse and update the effectiveWsUrl

* build(docker): expose live WebSocket server port and configure CORS origins

Add LIVE_WS_PORT (20132), LIVE_WS_HOST (0.0.0.0), and LIVE_WS_ALLOWED_ORIGINS environment variables to all Docker Compose profiles and expose the WebSocket port mapping. Prevent infinite self-loop in standalone-server-ws.mjs by skipping proxy when the server itself is running on the LiveWS port.

* docs(env): fix comment formatting for HOST and HOSTNAME variables

---------

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

* fix(logs): prevent stale detail refresh reopening modal (#6323)

* fix(logs): prevent stale detail refresh reopening modal

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* \ feat: operator-configurable account rotation\ (#6763)

* feat(resilience): operator-configurable account rotation

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR); the author's accountFallback/.env deltas were
re-applied cleanly onto the release tip.

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

* docs(env): document configurable account-rotation env vars in ENVIRONMENT.md

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

* refactor(rotation): extract rotation gate/context helpers to keep accountFallback.ts under frozen cap

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

* chore(changelog): re-sync CHANGELOG.md to release tip (restore lost base bullet)

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

* test(stryker): register rotation-config test in tap.testFiles for mutation coverage

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

* test(stryker): register ollama-quota covering tests (drift from #6731/#6817/#6742) + re-sync

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

---------

Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>

* fix(lmarena): modernize Arena web provider + static Direct-chat catalog (#6280)

* fix(lmarena): modernize Arena web provider + static Direct-chat catalog

Update the lmarena provider for arena.ai (product rebranded from LMArena):

- Route chat via arena.ai create-evaluation with Chrome TLS impersonation
  (tls-client-node) and optional browser-minted recaptchaV3Token.
- Seed Text+Search (48) into the chat registry; seed Image (27) only into
  IMAGE_PROVIDERS. Disable live HTML model discovery; resolve public names to
  Arena UUIDs from the static TypeScript allowlist (no scrape JSON in-repo).
- Soft-exclude 404/502 model ids; slow/stop bulk test-all probes for this provider.
- Do not fold IMAGE_PROVIDERS/video specialty into the chat provider catalog when
  a chat registry already exists (lmarena/openai/xai).
- Display name Arena (Free); keep wire id `lmarena` / alias `lma` for back-compat.
- Theme-aware provider icons: arena-light.svg / arena-dark.svg.
- Preserve split Supabase SSR cookie reconstruction for arena-auth-prod-v1.*.

* fix(providers): align provider-models-route test fixture + regen provider reference

Fold the topaz image-only catalog entry's apiFormat/supportedEndpoints
into the local-catalog test fixture (route now tags media-only
providers per the lmarena PR's staticModels.ts change), regenerate
PROVIDER_REFERENCE.md against the merged release providers.ts, and add
the changelog fragment for #6280.

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

* test: align web-cookie fallback suite — lmarena now has a registry entry (probe path)

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

---------

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

* docs(changelog): reconcile 3-day merge burst — 16 fragments, 4 promised credits, contributors hall 32→63

- changelog.d fragments for the 20 merged PRs that landed without a bullet
  (#6072 #6308 #6323 #6538 #6556 #6586 #6611 #6647 #6675 #6698 #6757 #6759
  #6804 #6821 + ci rollup #6781/#6691/#6693 + docs rollup #6643/#6644/#6646/#6663;
  omniglyph bump #6661 folded into the #6556 bullet)
- deliver the 4 credits promised in close comments but never written:
  @alltomatos (#6819 dup of #6721), @samimozcan (#6762/#6753 subsumed by #6790),
  @chirag127 (#6756 dup of #6757), @Squawk7777 (#6565 dup of #6564 — appended to
  the existing #6564 bullet; changelog-integrity flags that edit as a removal,
  intentional: ALLOW_CHANGELOG_REMOVALS justification)
- rebuild the v3.8.47 Contributors hall from merged-PR authors + thanks credits
  + prior hall: 32 → 63 contributors

* Clamp reasoning token buffer to model output cap (#6714)

* fix(combo): clamp reasoning buffer to model output cap

* fix(routing): preserve near-cap reasoning max tokens

* fix(routing): getExplicitModelOutputCap falls through to registry cap on non-numeric synced limit_output

getExplicitModelOutputCap short-circuited to null whenever a synced
capability row existed, even if that row's limit_output was not a number
(models.dev commonly omits it). That silently disabled the reasoning-token
buffer clamp for any model with a synced row lacking an output limit.

Now only return the synced value when it IS a number; otherwise fall
through to registryModel.maxOutputTokens / spec.maxOutputTokens, matching
the ??-chain precedence already used by getResolvedModelCapabilities().

Adds a standalone regression test (proves the fallthrough returns the real
registry cap, not null) and hardens the #6274 fixture id so its no-output-cap
case does not prefix-match the real glm-5.2 static spec.

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

* chore(stryker): register ollama-quota covering tests (release drift from merge burst)

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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI (#6320)

* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI

- Add src/i18n/messages/zh-TW.json translating frontend web UI
- Add bin/cli/locales/zh-TW.json translating CLI commands and descriptors
- Register zh-TW in config/i18n.json and docs/guides/I18N.md
- Update scripts/i18n/generate-multilang.mjs matching the new locale setup

* fix: update i18n locale count from 42 to 43 after adding zh-TW

The docs strict checker (check-docs-counts-sync.mjs) validates that
README.md and I18N.md reflect the real locale count. Adding zh-TW
bumped the count from 42 → 43.

* fix(i18n): translate providers free-filter labels in zh-TW (#6694 guard)

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: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>

* feat(proxy): implement latency-optimized proxy rotation strategy (#6798)

* feat(proxy): implement latency-optimized proxy rotation strategy

Reconstructed onto release/v3.8.47 to drop unrelated main-drift (deps/electron/proxy
files belong to #6620, not this PR) and the direct CHANGELOG.md edit (fragments-first);
the author's env/docs/i18n deltas were re-applied cleanly onto the release tip.

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

* fix(proxy): add latency-rotation env var to .env.example

PROXY_LATENCY_WINDOW_HOURS was referenced in src/lib/db/proxies.ts and
documented in docs/reference/ENVIRONMENT.md, but missing from
.env.example, tripping the env/docs sync gate.

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

* chore(changelog): re-sync CHANGELOG.md to release tip

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

* refactor(proxy): extract latency-strategy helpers to keep frozen files under cap

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

* test(db-rules): expect 35 audited modules (proxyLatency joins INTENTIONALLY_INTERNAL)

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

---------

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

* docs(readme): fix stale strategy/tool/scoring counts (#6853)

README still claimed 17 routing strategies (the table was missing
pipeline), 95 MCP tools, and 9-factor Auto-Combo scoring. Align with
the source (ROUTING_STRATEGY_VALUES has 18 entries) and the canonical
docs (MCP-SERVER.md: 94 tools; AUTO-COMBO.md: 12-factor).

* fix(antigravity): sanitize Cloud Code safety settings (#6839)

Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>

* fix(kiro): probe IdC region during profileArn discovery, cross-region (recovers #6099) (#6840)

* fix(kiro): route Amazon Q runtime by profileArn region for cross-region IdC

Enterprise AWS IAM Identity Center accounts whose IdC instance lives outside the two
Amazon Q Developer profile regions (us-east-1 / eu-central-1) - e.g. eu-north-1
(Stockholm), start URL https://d-XXXX.awsapps.com/start - showed no limits and returned
502 on every request.

Root cause: the backend used the IdC/OIDC token region (providerSpecificData.region, e.g.
eu-north-1) for every CodeWhisperer runtime call, hitting q.eu-north-1.amazonaws.com - a
host that does not exist as a Q Developer runtime endpoint. Per AWS docs ("Supported
Regions for the Q Developer console and Q Developer profile"), the Q Developer *profile*
(which produces the profileArn and hosts generateAssistantResponse / GetUsageLimits /
ListAvailableModels / ListAvailableProfiles) is only hosted in us-east-1 and eu-central-1,
regardless of the IdC region; "data is stored in the Region where you create the Amazon Q
Developer profile."

Fix (new open-sse/services/kiroRegion.ts) decouples the two regions:
- providerSpecificData.region stays the IdC/OIDC region, used ONLY for
  oidc.{region}.amazonaws.com token mint/refresh.
- The runtime region is derived from the profileArn (resolveKiroRuntimeRegion):
  profileArn region -> a valid stored profile region -> us-east-1. A stored IdC region
  that is not a Q profile region (eu-north-1) is ignored for runtime.
- Profile discovery (discoverKiroProfileArnAcrossRegions) probes the Q profile regions
  (EU IdC -> eu-central-1 first) with the cross-region SSO token instead of q.{idcRegion}.

Wired into: executors/kiro.ts (generateAssistantResponse targets the profile region),
services/usage/kiro.ts (getKiroUsage multi-region discovery + profileArn runtime region so
Limits resolves), services/kiroModels.ts (ListAvailableModels), and
src/lib/oauth/providers/kiro.ts (login-time postExchange profile discovery).

Adds tests/unit/kiro-idc-cross-region.test.ts (15 cases). All Kiro suites pass (60 tests).

* fix(kiro): probe the IdC region too during profileArn discovery (any IdC region)

Make profile discovery general for an IdC in ANY of the ~30 IdC-supported AWS regions
(us-west-2, ap-southeast-2, me-central-1, af-south-1, ...), not just eu-north-1.

buildKiroProfileDiscoveryRegions now probes the two documented Q Developer profile regions
FIRST (us-east-1 / eu-central-1, EU-first for EMEA IdC regions to cut latency), then appends
the IdC/stored region itself as a forward-compatible fallback: if AWS ever co-locates the
profile with the IdC or expands the profile-region list, a same-region probe still finds it.
Probing a region with no profile simply returns nothing and we fall through. The profileArn's
own region remains authoritative for every runtime call (resolveKiroRuntimeRegion), so a
newly-issued ARN in any region is honored automatically.

Adds ap-southeast-2 (APAC) cross-region coverage and updates the discovery-order tests.

---------

Co-authored-by: artickc <artur1992123@mail.ru>

* feat(providers): manual context-window override for custom models (#4125) (#6822)

Add a manual per-model "Context Window Override" so an operator can correct
a provider's misreported context length (e.g. reports 1M when the real
limit is 128K) instead of the model getting silently dropped from combo
routing once the wrong value lands in the catalog.

Reuses the existing Feature-5004 model_context_overrides table
(source="manual") — already the priority-0 source getModelContextLimit()
(the function combo's context-window filter calls) reads ahead of the
models.dev/registry/static catalog — so no new resolver logic was needed,
only the missing write path:

- PUT /api/provider-models now accepts an optional contextWindowOverride
  (number to set, null to clear), persisted via setModelContextOverride/
  removeModelContextOverride.
- GET /api/provider-models surfaces the current override value + source
  back on each custom-model row.
- CustomModelsSection.tsx: edit form gained a Context Window Override
  input + a badge on the model row when an override is set.

Regression guard: tests/unit/provider-models-context-window-override-4125.test.ts
(manual override wins over a misreported catalog value, GET round-trip,
clearing via null, default-unchanged behavior).

* feat(dashboard): improve Provider Quota page horizontal density (#3520) (#6815)

QuotaCardGrid stacked every provider group vertically in a single
flex flex-col container, and each group's own card grid didn't go
multi-column until the md breakpoint. Provider groups now flow into a
2-column CSS multi-column layout on very wide (2xl) screens instead of
an unconditional vertical stack, and each group's card grid starts at
2 columns immediately, filling horizontal whitespace sooner on
narrower-but-not-mobile viewports.

Regression guard: tests/unit/quota-card-grid-horizontal-layout.test.ts

* refactor(usage): type saveRequestUsage with UsageEntry interface + any-budget ratchet (#3512) (#6809)

Replace saveRequestUsage(entry: any) with a typed UsageEntry interface
mirroring the usage_history columns 1:1. Fields stay optional/nullable
since different writers (chatCore success/failure, rejected-request
accounting, Codex Responses WS) populate the row incrementally; tokens
stays unknown since callers pass either raw provider-shaped usage or
the normalized {input,output,cacheRead,...} shape.

Also cleaned the file's other any usages (getUsageHistory filter,
getUsageDb next-cursor cast, appendRequestLog tokens param,
getRecentLogs catch) so it now sits at zero any and can be added to
the check:any-budget:t11 zero-any allowlist.

Documents the DB-entity <-> TS-interface convention in
docs/architecture/CODEBASE_DOCUMENTATION.md Sec 11.

* feat(combo): strict budget-cap fallback policy for auto/* combos (#3470) (#6816)

Auto-combo transparency + budget controls: the engine's budgetCap enforcement
always degraded to the globally cheapest candidate when every candidate
exceeded the cap - silently overspending instead of respecting the cap.

- engine.ts: budgetFallback "cheapest" (default, legacy) | "strict"
  (BudgetExceededError when no candidate fits budgetCap)
- requestControls.ts: X-OmniRoute-Budget-Fallback header +
  resolveRequestAutoControls() consolidating mode/budget/fallback parsing
- resolveAutoStrategy.ts / autoConfig.ts: thread combo-level
  config.budgetFallback and catch BudgetExceededError into an HTTP 402
- chat.ts: switch to the consolidated resolveRequestAutoControls() helper
  (net line reduction, stays under the frozen file-size baseline)

Regression guard: tests/unit/auto-combo-budget-fallback-3470.test.ts

* fix(usage): honor xAI provider-reported exact cost (#6711)

OmniRoute's calculateCost() always estimated request cost from token
counts x static pricing, discarding xAI's exact provider-reported cost
when present. xAI's chat-completions usage object reports the precise
billed cost via cost_in_usd_ticks (docs.x.ai/developers/cost-tracking
and the API reference's usage schema: "TICKS_IN_USD_CENT: i64 =
100_000_000" => 1e10 ticks/USD, e.g. 37756000 ticks ~= $0.0038).

calculateCost()/computeCostFromPricing() now short-circuit to this
exact figure when present -- before any pricing DB lookup, so it also
works for models without a local pricing row -- and still fall back to
the token-based estimate when it is absent. The field is threaded
through both the streaming (extractUsage/normalizeUsage) and
non-streaming (extractUsageFromResponse) usage-extraction paths.

Corrected divisor vs upstream: the upstream PR used /1e12 (a 100x
under-report, e.g. reporting $0.00123 as the doc's $0.123 example);
this port uses the doc-verified /1e10 instead, confirmed against both
the cost-tracking guide and the API reference's usage-object schema.

Inspired-by: https://github.com/decolua/9router/pull/2453

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>

* docs: rename /implement-prs → /merge-prs in Hard Rule #21 (skill renamed 2026-07-11) (#6847)

* docs: refresh stale llm.txt facts + relocate design.md to docs/architecture/DESIGN_SYSTEM.md (#6849)

* docs: refresh stale llm.txt facts + move design.md to docs/architecture/DESIGN_SYSTEM.md

llm.txt was frozen at the v3.8.8 era (177 providers, 37 MCP tools, 14
strategies, 9-factor scoring, 75% coverage gate). Update every factual
claim to the current state (248 providers, 94 tools / 30 scopes, 18
strategies, 12-factor scoring, ratchet + 60% floor, TS 6, current docs/
layout) and re-sync the 42 exact-copy i18n mirrors.

design.md at the root was a standardization plan whose phases 1-6 all
shipped; rewrite its header as a permanent reference and relocate it to
docs/architecture/DESIGN_SYSTEM.md per the root-hygiene policy (root =
configs + canonical docs only).

* docs: add MDX frontmatter to DESIGN_SYSTEM.md (in-app docs pipeline requires it)

* feat: per-model web-search interception rule (#3384) (#6814)

* feat(routing): per-model web-search interception rule (#3384)

Adds a per-provider/per-model interceptSearch rule (src/lib/db/interceptionRules.ts,
key_value namespace interception_rules) that overrides the existing native
web-search bypass defaults (Codex/Gemini/Claude->Claude passthrough) in
webSearchFallback.ts. Wired at the existing prepareWebSearchFallbackBody() call
site in chatCore.ts. Resolution precedence: per-model rule > provider-level rule
> existing native-bypass defaults.

This lands Phase 1-2 of the plan (rule store + search interception). Web-fetch
interception and the dashboard UI toggle are tracked as follow-up phases.

* fix(db): register interceptionRules in localDb re-export layer (db-rules gate)

* fix(db): renumber interception_rules migration 119→120 (collision with model_capability_overrides)

* feat: sidebar search/filter input (#4013) (#6810)

* feat(dashboard): add search/filter input to the dashboard sidebar (#4013)

Adds a search box at the top of the expanded sidebar that filters nav
sections/groups/items client-side by label, so users don't have to
hunt through the growing nav tree. Reuses the existing common.search /
common.noResults i18n keys (no new locale edits needed) and the shared
Input icon="search" pattern. Matching sections auto-expand while
searching and the accordion/pin state is restored once the query is
cleared.

Filtering logic is extracted into a pure filterSidebarSectionsByQuery()
helper (src/shared/utils/sidebarSearch.ts) so it is trivially unit
testable independent of React/next-intl/next-navigation.

* fix(test): move Sidebar.search test to a runner-collected path (test-discovery gate)

* fix(i18n): backfill 194 missing pt-BR keys (#6695) (#6723)

* fix(i18n): backfill 194 missing pt-BR keys and add key-parity regression test (#6695)

* Merge branch 'release/v3.8.47' into fix/6695-i18n-drift

Resolve i18n key-parity and CHANGELOG-fragment conflicts:
- Convert the #6695 CHANGELOG.md bullet to a changelog.d/ fragment
  (the fragment convention landed on release/v3.8.47 after this PR
  branched, per changelog.d/README.md).
- Backfill 61 additional pt-BR keys that entered en.json on
  release/v3.8.47 after this PR's original 194-key backfill, so the
  PR's own key-parity regression test (tests/unit/i18n-pt-br.test.ts)
  stays green against the moving release baseline.

* Discover live Codex models (#6776)

* Add live model discovery for provider catalog

* Fix model discovery request headers

* fix(codex): sync live model limits with local catalog

* test(codex): split live model discovery coverage into dedicated route tests

* fix(codex): use chatgpt account id for live model sync

* Add GitHub-backed Codex model discovery fallback

* fix(providers): tighten oauth config tests and provider model display comments

* test: align client version expectations with release default

* fix(codex): keep discovery complexity within baseline

* fix: rebase live Codex model discovery onto release/v3.8.47, preserving kimi-web buildHeaders (#6308)

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: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>

* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697) (#6820)

* feat(codex): echo requested effort-suffixed model id in Responses payloads (#3697)

Codex CLI compatibility shim: the Responses API response.created/
response.in_progress/response.completed payloads now carry a `model`
field (previously absent), and for Codex-CLI-originated requests it
echoes the client-requested effort-suffixed model id (e.g.
gpt-5.5-xhigh) instead of the bare upstream id (gpt-5.5), so the Codex
CLI status line/model button shows the active reasoning effort.

- openai-responses.ts translator threads the upstream model into the
  Responses event objects (additive, omitted when unknown).
- New isCodexOriginatedHeaders() (codexIdentity.ts) reuses PR #3481's
  originator/User-Agent detection, header-based so it still fires when
  a combo routes codex/gpt-5.5-xhigh to a non-codex upstream.
- chatCore's existing opt-in #1311 echoModel pipeline now also fires
  automatically for Codex clients on the Responses API, regardless of
  the echoRequestedModelName setting.
- responseModelEcho.ts now also rewrites the nested response.model
  field the Responses API uses (previously only top-level model).
- /v1/models keeps returning models: [] for Codex (unchanged, #3481).

Regression guard: tests/unit/codex-effort-model-echo-3697.test.ts.

Closes #3697

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017) (#6818)

* feat(usage): surface Antigravity weekly quota alongside the 5-hour window (#4017)

Antigravity enforces both a 5-hour and a weekly usage limit, but the agy/antigravity
quota widget only exposed the 5-hour window. The weekly limit isn't in the per-model
retrieveUserQuota response already fetched — it lives in a separate, undocumented
retrieveUserQuotaSummary RPC that groups models into families (Gemini Models, Claude
and GPT models) with one weekly bucket per family.

Adds a self-contained usage/antigravityWeeklyQuota.ts leaf: a cached, best-effort
fetch of that RPC + a pure parser that extracts the weekly-labeled bucket per group
(window inferred from bucketId/displayName text, matching the reverse-engineered
shape documented by third-party Antigravity clients) into gemini_weekly/
claude_gpt_weekly quota entries, merged into the existing quotas map the widget
already renders generically. A failed/unavailable RPC never affects the existing
per-model quotas.

Live VPS validation attempt (192.168.0.15, real antigravity account): both
retrieveUserQuota and retrieveUserQuotaSummary currently return 429
RESOURCE_EXHAUSTED for that account, so the live response shape could not be
captured directly. The parser was instead validated via TDD against the bucket
shape documented by CodexBar (steipete/CodexBar), a third-party Antigravity client
that reverse-engineered the same RPC, and is defensive against both response
envelopes it has observed (top-level groups[] and nested quotaSummary.groups[]).

* chore(merge): re-sync with release/v3.8.47 (restore CHANGELOG, keep own bullet)

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* feat: add Z.ai Web free web-cookie provider (#4056) (#6823)

* feat(providers): add Z.ai Web free web-cookie provider (#4056)

New zai-web web-session provider drives the free chat.z.ai consumer
chat UI via a pasted browser cookie, distinct from the existing
API-key zai/glm/glm-cn/glmt providers (api.z.ai). ZaiWebExecutor posts
to chat.z.ai/api/chat/completions with the cookie forwarded both as
Cookie and Authorization: Bearer <token>, and normalizes both z.ai's
internal delta_content/phase SSE envelope and a pass-through
OpenAI-shaped choices[].delta frame into standard chat-completion
chunks.

Registered in WEB_COOKIE_PROVIDERS, WEB_SESSION_CREDENTIAL_REQUIREMENTS,
the provider registry (GLM-4.6/4.5/4.5V models), the executor factory,
and tokenExtractionConfig.ts for in-app cookie capture.

* fix(providers): regenerate translate-path golden for zai-web + reduce cognitive complexity

* fix(providers): rename ZaiWebExecutor.buildHeaders to avoid incompatible BaseExecutor override

* chore(merge): re-sync with release/v3.8.47; move changelog bullet to changelog.d fragment (merge-storm proof)

* fix(codex): bump default client version to 0.144.0 (#6780)

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

* refactor(usage): extract per-group parsing in antigravityWeeklyQuota (cognitive-complexity gate 886→885, release-level drift from #6818 merge)

* ci(quality): cut PR gate wall time without dropping protection (#6716)

Collapse duplicate CI spend while keeping each gate's existence reason:

- quality.yml: TIA __RUN_ALL__ defers full unit to fast-unit 4-shard (#6781);
  path filters via classify-pr-changes; docs-gates split; draft skip
- ci.yml: wire docs/i18n/code path filters; ESLint JSON artifact for quality-gate;
  drop advisory typecheck:noimplicit; float actions/cache@v6
- TIA parity: memory/usage/combo/serial; **/*.test.mjs any depth; electron/bin
  no longer force unit __RUN_ALL__
- check:complexity-ratchets: one ESLint walk, ruleId-isolated baselines + cache
- check:api-docs-refs + lib/apiRoutes: shared API route inventory
- husky pre-push: intentionally light (gates live in pre-commit); CLAUDE.md +
  QUALITY_GATES.md docs synced
- collect-metrics / lint:json: path.resolve cache path; Windows-safe eslint bin
- env-doc allowlist for ESLINT_RESULTS_JSON / COMPLEXITY_ESLINT_REPORT
- release-green --full-ci expects check:api-docs-refs (not docs-symbols alone)

Tests: select-impacted, classify-pr-changes, api-routes lib, complexity-rule-count,
validate-release-green.

Reconciled after #6781 (fast-unit 2→4 shards) per maintainer request on #6716.

Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>

* fix(docs): document Turbopack build memory tradeoff for RAM-constrained machines (#6409) (#6885)

* fix(routing): recognize Kimi token-limit 400 as context overflow for combo fallback (#6637) (#6893)

combo.ts's isContextOverflow400() guard required the literal word
'context' in the 400 error body before letting a combo fall through to
the next target. Kimi's exact wording ('Your request exceeded model
token limit: 262144 (requested: 308458)') never says 'context', so the
guard misclassified it as a body-specific error and halted the whole
combo instead of trying the next (larger-context) target.

accountFallback.ts's CONTEXT_OVERFLOW_PATTERNS already recognized this
wording one layer below (via checkFallbackError -> shouldFallback), so
the two independently-maintained classifiers disagreed and the
stricter one won. Export CONTEXT_OVERFLOW_PATTERNS from
accountFallback.ts and reuse it inside combo.ts's
isContextOverflow400() so both layers share a single source of truth.

Regression test: tests/unit/repro-6637-kimi-token-limit.test.ts
(RED on unfixed code -> GREEN after the fix). Existing #4519 guard
tests (tests/unit/combo-param-validation-fallback-4519.test.ts) still
pass, including the negative case that a genuinely body-specific 400
is NOT misclassified as overflow.

* fix(providers): honor a provider-level proxy assigned to no-auth providers (#6272) (#6895)

No-auth providers (mimocode, opencode, ...) are always dispatched with a single
hardcoded connectionId ("noauth" — SYNTHETIC_NOAUTH_CONNECTION_ID in
src/sse/services/auth.ts). No provider_connections row ever has id="noauth", so
resolveProxyForConnection() in src/lib/db/settings.ts could never populate
connectionRecord for them, and its provider-level proxy lookup (Steps 6/8) only
runs when connectionRecord is present. A proxy assigned via Settings -> Providers
-> mimocode was therefore silently ignored, reproducing the reporter's "same
thing happen when i set the proxy directly in the provider menu" symptom.

Adds a best-effort fallback (src/lib/db/settings/noAuthProxyFallback.ts): when
connectionRecord could not be resolved, scan the known no-auth provider ids for a
configured provider-level proxy (registry first, then legacy) before falling
through to the global/direct steps.

Regression test: tests/unit/proxy-noauth-provider-6272.test.ts (RED on unfixed
code — resolved to level=direct/proxy=null; GREEN after the fix).

* fix(dashboard): surface Claude extraUsage credits in quota card (#6806) (#6896)

Enterprise-tier Claude accounts (default_raven_enterprise) don't get
five_hour/seven_day utilization windows from Anthropic's OAuth usage
endpoint — only an extra_usage credit-billing block. parseClaude()
only read data.quotas, so quotas stayed {} and the dashboard showed
"No quota data" even when extraUsage showed the account 100%
exhausted. parseClaude() now folds an enabled extraUsage block into a
credits-style quota row (mirroring parseCodex's bankedResetCredits
pattern), both when quotas is empty and when it's already populated.

* fix(db): share sql.js preinit across callers, fix named-param bind (#6628, #6802) (#6899)

- preInitSqlJs() now memoizes an in-flight Promise (not just the resolved
  adapter) per filePath, so concurrent BATCH/STARTUP/HealthCheck/
  ProviderLimitsSync callers at boot share one full-file read+WASM decode
  instead of each independently reloading the whole database — the
  thundering-herd amplifier of the OOM condition #6632 already partly
  fixed, left un-implemented by the reporter's own proposed fix (#6628).

- sqljsAdapter's run/get/all now unwrap a lone named-parameter object
  (e.g. .all({ isActive: 1 }) for "WHERE is_active = @isActive", the same
  call shape getProviderConnections() already uses against better-sqlite3)
  before calling sql.js's stmt.bind(), expanding it to the @/:/$ sigil
  variants sql.js's own named-bind path requires. Previously the object
  was wrapped into an array and sql.js took the positional-bind path,
  throwing "Wrong API use : tried to bind a value of an unknown type
  ([object Object])." whenever the sql.js WASM fallback driver was active
  — exactly the error #6802 reported (misattributed to better-sqlite3).

Regression tests added to tests/unit/db-adapters/driverFactory.test.ts and
tests/unit/db-adapters/sqljsAdapter.test.ts, both proven RED against the
prior code and GREEN after the fix.

* fix(plugin): split OC-gate provider id from OmniRoute-facing routing id (#6859) (#6900)

resolveOmniRoutePluginOptions() auto-prefixes providerId with "opencode-"
(commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate accepts it as a
registered provider id. That prefixed value was being reused for the
OmniRoute-server-facing identifiers too: mapRawModelToModelV2's id/providerID,
mapComboToModelV2's providerID, and the dynamic provider hook's combo catalog
keys. OmniRoute's server has no "opencode-<x>" provider alias, so every
dispatched model failed credential lookup with "No credentials for
opencode-omniroute" / "No active credentials for provider:
opencode-omniroute".

Add a …

* test(ci): static body in codex e2e mock route bridge (CodeQL #737) (#7559)

CodeQL js/stack-trace-exposure flags ANY error-derived value returned in
the mock route bridge's 500 path, not just error.stack — swapping .stack
for error.message (in #7354, alert #736) left sibling alert #737 open on
the same line. Replace the body with a static string; the test only
asserts status===200, so the 500 body is never inspected. Clears the last
open CodeQL alert repo-wide, unblocking the Quality Ratchet on every PR.

Companion to the release/v3.8.49 PR (merge-gates §8 — gate/CI-touching fix
lands on main in the same session).

* fix(security): bump adm-zip >=0.6.0 + exact host matching in mitm DNS test (#7733)

* fix(cli): register ESM alias resolver for @/ paths under global install (#7791)

OmniRoute's tsconfig.json maps '@/*' to './src/*' so bare specifiers like
'@/shared/network/outboundUrlGuard' resolve under tsx in a dev checkout.
When the package is installed globally (or via npx without the tsconfig
context), tsx does not apply the path mapping and every '@/...' import
fails with ERR_MODULE_NOT_FOUND — reproducing as 'Cannot find package
'@/shared'' during setup-opencode.

Add bin/aliasResolver.mjs exporting registerAliasResolver(root), which
installs an ESM resolve hook that maps '@/<path>' to
file:///<root>/src/<path>.[ts|tsx|d.ts]. bin/omniroute.mjs calls it
right after ROOT is computed, before any '@/...' import is evaluated.

Design notes:
- Pure resolveAlias(specifier, root) is exported separately so it can be
  unit-tested without spawning a child process.
- registerAliasResolver validates root (non-empty string) BEFORE the
  _registered short-circuit, so a second call with invalid input still
  rejects instead of silently returning true.
- Returns false (no-op) when <root>/src does not exist, so the hook is
  not installed in environments where it would rewrite to nowhere.
- The hook only rewrites '@/...' specifiers; bare (node:*) and relative
  ('./...') specifiers fall through to the default resolver.

Tests: tests/unit/cli/alias-resolver-7791.test.ts covers the pure
resolver, the register guard (input validation, missing src/), and an
end-to-end regression that imports the real outboundUrlGuard.ts from a
child process (the exact chain that setup-opencode triggers).

* fix(security): silence CodeQL js/incomplete-url-substring-sanitization in aliasResolver (#7808)

CodeQL flagged bin/aliasResolver.mjs for building a
`data:text/javascript,...` URL dynamically via `new URL()` to register the
ESM loader hook — flagged as js/incomplete-url-substring-sanitization.

Refactor: extract the hook source into a real file
`bin/aliasResolverHook.mjs` and load it via `pathToFileURL()` from
`node:url` instead of the inline data-URL approach. The hook behaviour is
unchanged (still resolves `@/<path>` specifiers relative to the repo root
passed via the register `data` option).

Supporting changes so CI stays green:
- pack-artifact-policy.ts: add bin/aliasResolver.mjs and
  bin/aliasResolverHook.mjs to both ALLOWED_EXACT_PATHS (tarball ship) and
  REQUIRED_PATHS (regression guard — absence now fails loudly).
- tests/unit/pack-artifact-policy.test.ts: update the
  findMissingArtifactPaths snapshot expectation.
- config/quality/quality-baseline.json: rebaseline bundleSize 6534 -> 6762
  (+228). The hook file is now a 5th bin/*.mjs entrypoint counted by
  size-limit; the bytes were previously hidden inside aliasResolver.mjs
  because the template literal was compressed away.

* fix(antigravity): attempt onboarding when projectId is empty (#5193 regression of #2541)

PR #5193 changed onboarding from inline await to fire-and-forget gated by
if (projectId), which never fires when projectId is empty — the exact case
that needs onboarding. This re-introduced the #2541 catch-22.

Add an else-if branch that attempts onboarding inline (bounded by
AbortSignal.timeout) when projectId is empty, then retries loadCodeAssist
to discover the newly created project.

- Existing accounts with projectId: unchanged (fire-and-forget)
- New accounts without projectId: now onboarded within login flow
- Timeout bounded: +8s worst case (onboardUser + retry loadCodeAssist)
- Graceful degradation: if onboarding fails, lazy retry handles it

Tests:
- 3 new tests covering empty-projectId onboarding path (RED→GREEN)
- Existing 2 tests preserved and passing
- Adjusted timeout assertion for the stall test (now includes onboardUser stall)
- 5/5 passing on Node 24

Fixes #7814
Related: #5193, #2569, #2541, #2219

* fix(quality): rebaseline zizmorFindings 175 -> 176 for #7808 CI

Quality Gates (Extended) failed on PR #7808 with:
  REGRESSION — 176 zizmor finding(s) > baseline 175.

This +1 is NOT caused by the PR's code changes (bin/aliasResolver*.mjs are
not workflow files). It is pre-existing drift that surfaced because the
ratchet gate runs on this PR's CI: the zizmor version on the GitHub runner
gained (or extended) a rule since the v3.8.49 baseline was seeded on
2026-07-17. The new finding is an unpinned-uses @vN class item — the same
deliberate convention documented in _scanner_harden_workflows_2026_06_16
(SHA-pinning only this one would violate the convention).

No new template-injection / artipacked / cache-poisoning / dangerous-triggers
classes introduced. Measured by Quality Gates (Extended) on run 29713001401.

* fix(cli): reject path-traversal specifiers in @/ alias resolver

resolveAlias() (bin/aliasResolver.mjs) and tryResolveAliasFsPath()
(bin/aliasResolverHook.mjs) only rejected specifiers starting with an
absolute-ish `/` or `\`. A specifier like `@/../../../etc/hostname`
slips past that guard (it starts with `.`), gets `join()`-ed against
`<root>/src`, and node's `join()` happily normalizes the `..` segments
outside of `src/` — so a crafted specifier could resolve (and,
combined with existsSync, expose) files outside the intended root.

Close the gap in both files: reject any specifier whose remainder
contains a literal `..` path segment, then double-check with
`path.relative(srcRoot, candidate)` that the joined path cannot land
outside `<root>/src` even after normalization (defense in depth).

Add a regression test asserting `resolveAlias("@/../../../etc/hostname", root)`
returns `null`, plus a few related traversal shapes.

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

* Revert "fix(antigravity): attempt onboarding when projectId is empty (#5193 regression of #2541)"

This reverts commit b36d3b6b70.

That commit is a byte-identical duplicate of the antigravity onboarding
fix shipped separately in PR #7815 (same author, same diff to
src/lib/oauth/providers/antigravity.ts and its test file). Keeping it
here would ship and credit the same fix twice across two PRs. #7808 is
scoped to the #7791 global-install alias-resolver fix only; the
antigravity fix stays in #7815.

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

* feat(alias-resolver): extend alias coverage to @omniroute/open-sse/*

- Refactored resolveAlias() from hardcoded @/ prefix to a configurable
  ALIAS_MAP table matching tsconfig.json paths entries:
    @/                        → ./src/*
    @omniroute/open-sse/       → ./open-sse/*
    @omniroute/open-sse        → ./open-sse/index.ts (exact match)
- Updated aliasResolverHook.mjs with identical ALIAS_TABLE logic
- Fixed probeFile() to check extensions before bare path to avoid
  false-positive directory matches (e.g. usage/ vs usage.ts)
- Extracted probeFile() and probeIndex() helpers for clarity
- Renamed isWithinSrcRoot → isWithinRoot (generic)
- Added 7 new unit tests for @omniroute/open-sse aliases:
  ALIAS_MAP shape validation, bare package resolution,
  subpath resolution, non-existent paths, unmatched scopes,
  path-traversal guards
- All 23 tests passing

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: growab <nekron@icloud.com>
Co-authored-by: KooshaPari <42529354+KooshaPari@users.noreply.github.com>
Co-authored-by: Chirag Singhal <76880977+chirag127@users.noreply.github.com>
Co-authored-by: Chirag Singhal <chirag127@users.noreply.github.com>
Co-authored-by: Ronaldo Davi <ronaldodavi@gmail.com>
Co-authored-by: Andrew Munsell <andrew@wizardapps.net>
Co-authored-by: WITALO ROCHA <witalo_rocha@hotmail.com>
Co-authored-by: Wital <witalorocha216@gmail.com>
Co-authored-by: Aoxiong Yin <i@yinaoxiong.cn>
Co-authored-by: Andrew B. <37745667+AndrianBalanescu@users.noreply.github.com>
Co-authored-by: Andrian B. <andrewbalanesq@gmail.com>
Co-authored-by: Xiangzhe <32761048+xz-dev@users.noreply.github.com>
Co-authored-by: Jon Bailey <297513015+Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: Pitchfork-and-Torch <Pitchfork-and-Torch@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: Samir Abis <me@samirabis.com>
Co-authored-by: lucasjustinudin <34107354+lucasjustinudin@users.noreply.github.com>
Co-authored-by: chy1211 <31048289+chy1211@users.noreply.github.com>
Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
Co-authored-by: whale9820 <whale9820@users.noreply.github.com>
Co-authored-by: anhdiepmmk <n08ni.dieppn@gmail.com>
Co-authored-by: Septianata Rizky Pratama <19322988+ianriizky@users.noreply.github.com>
Co-authored-by: NOXX - Commiter <artur1992123@mail.ru>
Co-authored-by: backryun <bakryun0718@proton.me>
Co-authored-by: lunkerchen <labanchen@gmail.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>
Co-authored-by: Ray Doan <raydoan.contact@gmail.com>
Co-authored-by: kfiramar <83420275+kfiramar@users.noreply.github.com>
Co-authored-by: Jan Leon <Jan.gaschler@gmail.com>
Co-authored-by: Someres <168349709+quanturbo@users.noreply.github.com>
Co-authored-by: MikeTuev <ra9ftm@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Imam Wahyu Widodo <120608486+hajilok@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
Co-authored-by: AgentKiller45 <jamalzzj45@gmail.com>
Co-authored-by: judy459 <JUDYZHU459@outlook.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <souzamiriamrodrigues790@gmail.com>
Co-authored-by: Markus Hartung <mail@hartmark.se>
Co-authored-by: KooshaPari <koosha@phenotype.io>
Co-authored-by: Jade Guo <jade.gly@gmail.com>
Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com>
Co-authored-by: oyi77 <oyi77@users.noreply.github.com>
Co-authored-by: Dayna Blackwell <dayna@blackwell-systems.com>
Co-authored-by: backryun <backryun@daonlab.local>
Co-authored-by: brick30llc-ctrl <brick30llc@gmail.com>
Co-authored-by: brick30llc-ctrl <admin@brick30.com>
Co-authored-by: Saren <saren@dumstruck.com>
Co-authored-by: Xiangzhe <xiangzhedev@gmail.com>
Co-authored-by: KooshaPari <62650152+KooshaPari@users.noreply.github.com>
Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com>
Co-authored-by: huohua-dev <celentanohertor@gmail.com>
Co-authored-by: huohua-dev <258873123+huohua-dev@users.noreply.github.com>
Co-authored-by: CitrusIce <31264099+CitrusIce@users.noreply.github.com>
Co-authored-by: minisforum <no@mail.com>
2026-07-20 10:10:33 -03:00
Diego Rodrigues de Sa e Souza
7951b60bc3 feat(cli): add auth export command for decrypted provider credentials (#6683) (#7724) 2026-07-19 14:37:12 -03:00
Diego Rodrigues de Sa e Souza
491f9472b8 fix(cli): load DATA_DIR/server.env as fallback for .env on Electron migration (#7302) (#7759)
Electron persists secrets (JWT_SECRET, API_KEY_SECRET, STORAGE_ENCRYPTION_KEY) to
<DATA_DIR>/server.env, but the CLI bootstrap only ever loaded <DATA_DIR>/.env. Copying
storage.sqlite + server.env from an Electron install to the CLI (exactly as the app's own
UI text instructs) silently lost STORAGE_ENCRYPTION_KEY, permanently corrupting every
encrypted provider credential.

bin/omniroute.mjs now does a one-time, one-directory migration: if <DATA_DIR>/.env is
absent but <DATA_DIR>/server.env is present, copy it to .env before the normal env-file
loading loop runs. An existing .env is never overwritten -- it always wins over a legacy
server.env.
2026-07-19 09:55:44 -03:00
Diego Rodrigues de Sa e Souza
4387ee86e0 fix(cli): omniroute dashboard respects PORT env when --port is omitted (#7049) (#7252) 2026-07-17 10:41:02 -03:00
Diego Rodrigues de Sa e Souza
205361a850 fix(cli): fast-path --version to skip full CLI bootstrap (#7208)
* fix(cli): fast-path --version to skip full CLI bootstrap

`omniroute --version` ran the entire CLI bootstrap before printing the
version: the tsx/esm + polyfill imports, env-file loading, and
Commander's ~70-command registration (importing DB, providers, OAuth,
and other heavy modules). That took ~1.5s just to print a version
string.

Add isVersionFastPath() (bin/cli/utils/versionFastPath.mjs) and check it
at the very top of bin/omniroute.mjs, before any of that work runs. It
only trips for an unambiguous bare `--version`/`-V` invocation (no other
args), so it never changes behavior for real commands or for `--help`
(whose output is generated dynamically from every registered
subcommand, so it still needs full registration and is deliberately not
fast-pathed).

`--version` now returns in ~0.3s instead of ~1.5s locally.

Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/2414

* chore(changelog): fragment for #7208

* fix(build): enforce bin/cli/utils/versionFastPath.mjs in the pack-artifact gate

bin/omniroute.mjs now imports ./cli/utils/versionFastPath.mjs on its boot path
(the --version fast-path). bin/cli/ is only an allowlist PREFIX, so the file
vanishing from the npm tarball would never fail the unexpected-paths check --
only PACK_ARTIFACT_REQUIRED_PATHS makes its absence loud (#7065 class).

Adds the required path and updates the hardcoded expectation in
pack-artifact-policy.test.ts, matching the existing data-dir.mjs /
storageKeyProvision.mjs entries. Fixes the red in
tests/unit/pack-artifact-entrypoint-closures.test.ts, which derives the
requirement from the entrypoint's own imports.

---------

Co-authored-by: Sutarto Jordan Chrisfivo <Jordannst@users.noreply.github.com>
2026-07-17 10:39:39 -03:00
Diego Rodrigues de Sa e Souza
a62141210b fix(cli): verify better-sqlite3 native binary is actually loadable (#7105)
* fix(cli): verify better-sqlite3 native binary is actually loadable (port from 9router#2493)

isBetterSqliteBinaryValid() only checked the .node file's magic bytes (ELF/Mach-O/PE header), never whether the binary was built for the ABI (NODE_MODULE_VERSION) of the Node runtime that loads it. A stale or foreign-ABI binary passed the check and then segfaulted the process on the first database call instead of triggering a rebuild via npmInstallRuntime(). The fix adds a real load probe (require() in a throwaway subprocess) after the magic-byte check, so an incompatible binary is now correctly reported as invalid and the runtime self-heal reinstalls it.

Reported-by: Manikandan (@mrprohack) (https://github.com/decolua/9router/issues/2493)

* chore(changelog): move #2493 entry to changelog.d fragment

Consistency with the repo's canonical changelog.d/fixes/ workflow (avoids
merge-storm re-conflicts from editing CHANGELOG.md directly).
2026-07-16 14:13:33 -03:00
Diego Rodrigues de Sa e Souza
c0682c1ac2 fix(cli): waitForServer must not report ready on bare TCP accept (#6800) (#6892)
waitForServer() polled /api/monitoring/health but fell back to declaring
the server ready once the port had merely accepted TCP connections for
>= 3s, even if no HTTP response was ever received. On CPU-bound warmup
(e.g. small VPS running Next.js standalone), the OS-level listener
accepts TCP almost immediately while the request pipeline is still
compiling, so the fallback fired within ~3-7s and the CLI printed
'OmniRoute is running!' 30-60s before any route actually answered.

Classify each health poll into ready / fast-reject / hanging /
not-listening: only a fast HTTP rejection (fetch error that is not a
timeout, e.g. ECONNRESET before the route mounts) grants the original
#2460 Windows-cold-start grace window. A request that times out with
zero response (the reported #6800 symptom) resets the grace window
instead of accumulating toward it.

Regression tests: tests/unit/waitForServer-tcp-fallback-6800.test.mjs
(new RED-then-GREEN probe from the bug analysis) and
tests/unit/cli-waitForServer.test.mjs (existing suite realigned to the
corrected contract, plus a new case for the hanging-socket scenario).
2026-07-12 01:59:49 -03:00
lunkerchen
b6ab8ba1ec feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI (#6320)
* feat(i18n): add Traditional Chinese (zh-TW) localization for frontend and CLI

- Add src/i18n/messages/zh-TW.json translating frontend web UI
- Add bin/cli/locales/zh-TW.json translating CLI commands and descriptors
- Register zh-TW in config/i18n.json and docs/guides/I18N.md
- Update scripts/i18n/generate-multilang.mjs matching the new locale setup

* fix: update i18n locale count from 42 to 43 after adding zh-TW

The docs strict checker (check-docs-counts-sync.mjs) validates that
README.md and I18N.md reflect the real locale count. Adding zh-TW
bumped the count from 42 → 43.

* fix(i18n): translate providers free-filter labels in zh-TW (#6694 guard)

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: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: lunkerchen <lunkerchen@users.noreply.github.com>
2026-07-11 02:15:44 -03:00
Diego Rodrigues de Sa e Souza
ec0381c453 fix(api): point CLI health command at /api/monitoring/health (#6677) (#6717)
* fix(api): point CLI health command at /api/monitoring/health (#6677)

bin/cli/commands/health.mjs called GET /api/health, a route that was
moved to /api/monitoring/health without updating the CLI; the top-level
/api/health handler never existed on disk (only degradation/ and ping/
sub-routes). Point runHealthCommand()/runHealthComponentsCommand() at
/api/monitoring/health and read its real payload shape
(activeConnections, circuitBreakers: {open,halfOpen,closed}, memoryUsage)
instead of the old nonexistent requests/breakers/cache/memory fields.

* chore(6717): re-sync onto release tip; move CHANGELOG entry to changelog.d fragment (fragments-first)
2026-07-10 18:06:00 -03:00
Diego Rodrigues de Sa e Souza
a6ea6074cb fix(cli): compression REST fallback uses canonical defaultMode + JSON object cells (#6571) (#6682)
* fix(cli): compression REST fallback uses canonical defaultMode + JSON object cells (#6571)

* test(cli): align existing compression-command tests to the #6571 canonical field contract (engine→strategy/defaultMode)
2026-07-08 16:50:11 -03:00
KooshaPari
fc01f53f94 chore(cli): harden empty catches in completion.mjs with env-gated error logging (#6257)
chore(cli): harden empty catches in completion.mjs with env-gated error logging (#6257). Reconstructed cleanly onto release/v3.8.47; env var documented. Integrated into release/v3.8.47.
2026-07-07 23:57:44 -03:00
Diego Rodrigues de Sa e Souza
318b0b01d2 fix(cli): surface a diagnostic instead of a silent hang on serve readiness timeout (#6321) (#6504)
fix(cli): surface a diagnostic instead of a silent hang on serve readiness timeout (#6321). Integrated into release/v3.8.47.
2026-07-07 17:46:07 -03:00
Swing Tempo
69d2b31930 Swingtempo/fixwindowscodex (#6312)
launch-codex spawns codex.cmd via shell on Windows (#6263 pattern). Added resolveCodexSpawn() helper + regression test (2/2), satisfying Hard Rule #18. Integrated into release/v3.8.46.
2026-07-06 21:20:52 -03:00
Diego Rodrigues de Sa e Souza
e3d29d1419 fix(cli): register reset-password subcommand + non-TTY stdin path (#6261, #6258) (#6387) 2026-07-06 18:13:06 -03:00
Aris
74546ed854 fix(doctor): resolve two false-positive WARNs (#6162) (#6163)
* fix(doctor): resolve two false-positive WARNs (#6162)

The `omniroute doctor` command reported two warnings on healthy installs
even though the underlying checks actually passed. Both came from the
doctor probing state that already worked; they looked like bugs but users
couldn't tell without manual digging.

Issue 1 — Server liveness HTTP 401
  /api/health and /api/health/degradation both require the management
  token. Doctor called them without auth → 401 → WARN, even when the
  Next.js server was clearly alive and listening.

  Fix: probe the configured health endpoint first; on 401/403, fall
  back to a publicly served static asset (/favicon.ico) to confirm the
  server is alive. WARN now only fires when both probes fail.

Issue 2 — CLI Tools '@/shared' import
  tool-detector.ts (and 3 other cli-helper files) import @/shared/...
  aliases that resolve via tsconfig.json paths. The CLI ships raw TS
  source (no compile step) and runs through tsx, but tsx does not honor
  tsconfig paths at runtime, and tsconfig-paths only hooks CJS
  Module._resolveFilename while doctor uses ESM `import()`.

  Fix: replace @/shared/... with relative imports in the 4 cli-helper
  files. This is the same pattern these files already use for ./config-
  generator/* imports. No new dependency, no architectural change, and
  the fix doesn't regress Next.js itself which keeps using @/shared.

Verified on v3.8.43 (Node v24.17, Windows 11):
  Before: 7 ok, 2 warning(s), 0 failure(s)
  After:  8 ok, N warning(s), 0 failure(s)
    where N accurately reflects which CLI tools are installed and
    configured for OmniRoute (e.g. Hermes Agent installed but not
    pointed at 20128 → 2 real warnings, not 1 false-positive).

Refs #6162

* fix(doctor): derive fallback URL from primary URL via new URL()

Per Gemini code-assist review feedback: the previous fallback constructed
the /favicon.ico URL from defaults (127.0.0.1:PORT) which ignored custom
host/port/protocol configurations supplied via:
  - OMNIROUTE_DOCTOR_LIVENESS_URL
  - OMNIROUTE_DOCTOR_HOST
  - --liveness-url / --host CLI flags

Parse the primary URL with new URL() to preserve protocol, host, port, and
subpaths. The previous default-based fallback remains as a catch-all for
invalid primary URLs.

* test(doctor): add regression tests for #6162 fixes

Two new test files lock the fix and satisfy the PR Test Policy gate
("production code change without tests"):

- tests/unit/cli-helper-tool-detector-paths-6162.test.ts
    Locks the @/shared → relative imports fix across all 4 cli-helper
    files. Asserts (a) no @/shared alias remains in the cli-helper
    sources, and (b) each file is importable at runtime via tsx/ESM,
    which would have thrown "Cannot find package '@/shared'" before
    the fix.

- tests/unit/cli-doctor-liveness-fallback-6162.test.ts
    Locks the /favicon.ico fallback in doctor.mjs. Asserts the
    fallback probe exists, derives its URL from the primary URL via
    new URL() (per Gemini review feedback), and that the buggy
    'Server responded with HTTP 401' WARN path is gone.

Both tests use only node:test + node:assert/strict so they slot into
the existing 'test' and 'test:unit' scripts with no extra config.

* test(doctor): fix primary.ok regex in fallback test

The earlier regex /primary\.ok\s*\?/ required a '?' immediately after,
but the actual doctor.mjs code uses a multi-line if-block:

  if (primary.ok) {
    return ok(...);
  }

Use /\bprimary\.ok\b/ instead so the assertion matches the existing
branching.

---------

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
2026-07-05 02:41:07 -03:00
R. Beltran
b074c6d75e fix(cli): detect POSIX auto-set HOSTNAME via os.hostname() to fix bind address (#6194) (#6195)
POSIX shells (bash/zsh) always set HOSTNAME to the machine name. The
.env loader uses first-wins semantics, so HOSTNAME=0.0.0.0 in .env is
silently ignored. This causes the server to bind to the LAN hostname
instead of 0.0.0.0, breaking localhost access and all internal
self-requests (ModelSync, HealthCheck, cloud sync).

The fix compares process.env.HOSTNAME against os.hostname(): when they
match, it's the POSIX auto-set signature and HOSTNAME is ignored.
OMNIROUTE_SERVER_HOST takes precedence as the dedicated escape hatch.

Backward compatibility is preserved: users who set HOSTNAME to a value
that doesn't match the machine name (e.g. Windows CMD/PowerShell users
with HOSTNAME in .env) will still have their value honoured.

Closes #6194
2026-07-05 02:30:16 -03:00