Compare commits

..

162 Commits

Author SHA1 Message Date
Diego Rodrigues de Sa e Souza
6c6ab8ab37 test(release): collect the #6082 fingerprint-expansion ghost test
check:test-discovery (Lint job, layered behind the round-1 t06 fix) flagged
tests/e2e/fingerprint-expansion.test.ts as a NEW orphan — it is a node:test
server-boot test that no runner collected, so it had never run. Moved to
tests/integration/ (the collector for this shape), fixed the helper import,
and verified it actually passes (3/3 on first-ever run). CHANGELOG ref updated.
2026-07-04 11:53:06 -03:00
Diego Rodrigues de Sa e Souza
103236db7c fix(release): round-2 CI findings — LocaleAutoDetect refresh gating + ratchet tighten
- fix(i18n): LocaleAutoDetect (#5979) refreshed the router on EVERY cookie-less
  first visit, even when the detected locale matched the server-rendered
  <html lang> — re-navigating mid-interaction (flaky e2e 'execution context
  destroyed' + visible flash for new visitors). Refresh now only fires when
  the locale actually differs; regression test added.
- quality: tighten openapiCoverage.pct 36.9->39.3 (require-tighten gate on the
  release PR; value measured by the CI Quality Ratchet on 00c55afcb)
- quality(file-size): shrink the ProxyRegistryManager TDZ note to fit the
  1117-line freeze (prettier reflow added a line at commit time)
- changelog bullet + 42 i18n mirrors re-synced
2026-07-04 11:29:15 -03:00
Diego Rodrigues de Sa e Souza
00c55afcb6 fix(release): v3.8.44 one-pass release-PR CI sweep
- fix(dashboard): /dashboard/system/proxy 500'd on EVERY render — #5918 put
  useProxyBatchOperations(load) before the const load declaration (TDZ
  ReferenceError, digest 539380095). Hook block moved after load; SSR
  renderToString regression test added (the exact crash mode).
- fix(server): TRACE/TRACK/CONNECT crashed Next's middleware adapter
  (undici cannot represent them) into a raw 500 on every route — the raw
  HTTP method guard now answers 405 + Allow up-front (dast-smoke
  Schemathesis finding on /api/keys/{id}/devices); guard test added.
- fix(api): restore Zod validation on the provider-scoped chat route via a
  .passthrough() schema preserving #5907's relaxed semantics (t06 gate).
- docs(openapi): /api/keys/{id}/devices 401 now refs the management error
  envelope (Schemathesis schema-conformance).
- quality: rebaseline i18nUiCoverage 77.5->76.8 (+~1352 new en.json UI keys
  from the cycle await the async translation workflow; v3.8.39 precedent).
- CodeQL: dismissed 2 incomplete-url-substring FPs on unit-test asserts
  (v3.8.35 precedent) with Hard Rule #14 justifications.
- changelog: bullets for the above + 42 i18n mirrors re-synced
2026-07-04 10:40:48 -03:00
Diego Rodrigues de Sa e Souza
babb759441 chore(release): v3.8.44 — 2026-07-04
Release reconciliation + close (generate-release Phases 0a/1):
- CHANGELOG [3.8.44]: 21 PR refs added to existing bullets, 62 new bullets
  (incl. restoration of ~10 bullets erased by the stale-branch merge in
  1f6ec5bc8), 3 Maintenance rollups, #6061/#6130 credit fixes, 🙌 Contributors
  table (35 external contributors) — coverage 144/153 cycle commits by #ref
- 42 docs/i18n CHANGELOG mirrors synced (EN content; i18n workflow translates)
- README: What's New refreshed for v3.8.44 highlights
- build scope: exclude electron/node_modules + electron/dist-electron + .build
  from tsconfig (local build-output leak poisoned next build with 8GB OOM —
  same class as the 2026-06-25 incident; scope 14765→5207, gate green)
- quality: cyclomatic baseline 2026→2028 (+2 inherited end-of-cycle drift;
  verified the release-captain code fixes add 0 new violations)
2026-07-04 09:05:01 -03:00
Diego Rodrigues de Sa e Souza
979a7c63e9 fix(release): v3.8.44 Phase-0 pre-flight — base-red sweep + ratchet absorption
- fix(models): stop resolveProviderAlias at registered provider ids so oc/
  reaches the no-auth opencode provider again (#2901 contract, regressed by
  #5918's transitive chain; transitivity kept across alias-only hops)
- fix(auggie): handle async EPIPE 'error' events on child stdin so a
  fast-exiting CLI surfaces a sanitized error instead of crashing (both
  spawn sites); deflakes auggie-executor tests
- test: align provider family count 166->167 (Kenari #6104), regenerate
  translate-path golden on Linux (+kenari), opencode quota scope
  provider->connection (#6061)
- quality(test-masking): add _deletedWithReplacement allowlist support to
  check-test-masking.mjs (deletion exempt ONLY when the declared replacement
  test exists in HEAD; 5 new gate unit tests) + reduction allowlist entries
  for the verified #5958/#6088/#5816 migrations + targetExhaustion->
  combo-target-exhaustion replacement (#5976, 21 cases/52 asserts vs 13/37)
- quality(file-size): absorb v3.8.44 cycle drift (oauth route 960,
  providerLimits 998, chat 1662, auth 2426) with justification; #6158 will
  restore the oauth-route freeze
- changelog: bullets for the above + the #6155 cooling-panel build fix
2026-07-04 06:39:21 -03:00
Diego Rodrigues de Sa e Souza
5ca28f2ff7 fix(dashboard): resolve broken Card import breaking next build (base-red from #6061) (#6155)
* fix(dashboard): resolve broken Card import breaking next build (base-red from #6061)

CoolingConnectionsPanel imported `Card` from `@/components/ui/card`, a path
that does not exist in this repo (there is no shadcn-style `src/components/ui/`).
The PR->release fast-gates do not run `next build`, so the broken import slipped
in and `next build` failed with:

  Module not found: Can't resolve '@/components/ui/card'

Fix: the <Card> here was only a styled container, so replace it with a <div>
carrying the equivalent Tailwind classes (border/bg/padding + rounded-card
shadow-sm). Also normalize the file from CRLF to LF (it shipped with CRLF).

Adds a vitest/jsdom regression test (tests/unit/ui/CoolingConnectionsPanel.test.tsx)
that fails-without-fix (Vite: 'Failed to resolve import @/components/ui/card')
and passes with it, plus renders/empty-state coverage. Rule #18.

* fix(dashboard): stop client CoolingConnectionsPanel dragging server DB barrel into browser bundle

Second base-red from #6061, surfaced once the broken Card import was fixed:

  ./node_modules/ioredis/built/connectors/StandaloneConnector.js
  Module not found: Can't resolve 'net'
  Import trace: ioredis <- rateLimiter.ts <- apiKeys.ts <- @/lib/localDb
                <- CoolingConnectionsPanel.tsx (a "use client" component)

The client panel imported `formatResetCountdown` from `@/lib/localDb` — the
server-side DB re-export barrel — which transitively pulls better-sqlite3/ioredis
(node:net) into the browser bundle. That violates the CLAUDE.md rule 'never
barrel-import from localDb'.

`formatResetCountdown` is a pure date-formatting function, so move its
implementation to the client-safe `@/shared/utils/formatting` (alongside
formatTime/formatDuration) and re-export it from db/providers/rateLimit.ts for the
existing server callers + barrel. The panel now imports it directly from the
shared util — no server code in the client bundle.

Tests (Rule #18):
- tests/unit/format-reset-countdown.test.ts (node:test, blocking test:unit) —
  pure-function coverage: null/past/invalid, s, m+s, h+m, ISO string.
- tests/unit/ui/CoolingConnectionsPanel.test.tsx mock updated to the new module.
2026-07-04 06:35:07 -03:00
Diego Rodrigues de Sa e Souza
415d159c80 fix(oauth): graceful 400 for keychain-import-only providers (zed) (#6041) (#6054)
Graceful 400 for keychain-import-only providers on OAuth route (zed, #6041). Integrated into release/v3.8.44.
2026-07-04 01:00:58 -03:00
Diego Rodrigues de Sa e Souza
dc2efa5e63 feat(resilience): throttle concurrent upstream quota fetches — closes #6009 (#6058)
Throttle concurrent upstream quota fetches (#6009). Integrated into release/v3.8.44.
2026-07-04 00:58:55 -03:00
Diego Rodrigues de Sa e Souza
2fa47b7b2c feat(sse): per-request Auto-Combo controls (X-OmniRoute-Mode / X-OmniRoute-Budget) — closes #6023 #6024 #6025 (#6057)
Per-request Auto-Combo controls (X-OmniRoute-Mode / X-OmniRoute-Budget). Integrated into release/v3.8.44.
2026-07-04 00:57:33 -03:00
Diego Rodrigues de Sa e Souza
016fec3c82 feat(providers): add Kenari OpenAI-compatible gateway (#6104)
Add Kenari OpenAI-compatible gateway (BYOK). Integrated into release/v3.8.44.
2026-07-04 00:56:05 -03:00
Diego Rodrigues de Sa e Souza
c900bbb67a fix(providers): strip thinking param for minimax-m2.7 on NVIDIA NIM (#6102)
Strip unsupported thinking param for minimax-m2.7 on NVIDIA NIM. Integrated into release/v3.8.44.
2026-07-04 00:37:10 -03:00
Diego Rodrigues de Sa e Souza
402d9dea81 feat(models): add claude-sonnet-5 to Antigravity catalog (#6103)
Add claude-sonnet-5 to Antigravity catalog. Integrated into release/v3.8.44.
2026-07-04 00:35:34 -03:00
Diego Rodrigues de Sa e Souza
0cc32565aa fix(mitm): guard against concurrent MITM server starts (#6107)
Guard against concurrent MITM server starts (#2316). Integrated into release/v3.8.44.
2026-07-04 00:32:40 -03:00
Diego Rodrigues de Sa e Souza
d927f2d31f fix(translator): strip Responses-only truncation field before Chat Completions forwarding (#6109)
Strip Responses-only truncation field before Chat Completions forwarding (#2311). Integrated into release/v3.8.44.
2026-07-04 00:30:04 -03:00
Chirag Singhal
d8e4eeef5d fix(onboarding): route provider-details link by node id, not provider slug (#6145) (#6145)
Route onboarding provider-details link by node id (#6145). Integrated into release/v3.8.44.
2026-07-04 00:26:15 -03:00
derhornspieler
81f5bce4be fix(cli): give setup-claude a fallback profile generator like setup-codex (#6138)
Give setup-claude a fallback profile generator like setup-codex. Integrated into release/v3.8.44.
2026-07-04 00:23:09 -03:00
Raxxoor
65bec99c3c fix(glm): suppress </think> close marker leak in GLM Anthropic transport (#6133)
Suppress </think> close-marker leak in GLM Anthropic transport. Integrated into release/v3.8.44.
2026-07-04 00:22:15 -03:00
janeza2
c51693b1a3 fix(provider-limits): close TOCTOU race in quota recovery clear (I2) (#6139)
Close TOCTOU race in quota recovery clear via CAS primitive (I2 from #6128). Integrated into release/v3.8.44.
2026-07-04 00:16:12 -03:00
backryun
d3edbb5535 fix(repo): remove Windows case-conflicting DESIGN duplicate (#6140)
Remove stale root DESIGN.md (Windows case-conflict with design.md). Integrated into release/v3.8.44.
2026-07-04 00:11:47 -03:00
Diego Rodrigues de Sa e Souza
4a7602f96d chore(quality): rebaseline residual eslint + file-size drift (v3.8.44)
Residual drift on release tip 716041223 (moving target): eslintWarnings 4270->4279
(+9 as the branch advanced past the prior rebaseline) and testFrozen/frozen file-size
caps for providerLimits.ts (955->982), accountFallback.ts (1790->1864) and
sse-auth.test.ts (1553->1600). All inherited from parallel-session merges (e.g. #6128);
the two production god-files ideally warrant decomposition rather than a bump (tracked
as debt). No production code touched.
2026-07-03 23:57:56 -03:00
Devin
e42e55426e i18n(zh-CN): translate CHANGELOG entries and section headings (#6043)
Adopt zh-CN as a translated locale: translate CHANGELOG + supporting docs. Integrated into release/v3.8.44.
2026-07-03 23:57:35 -03:00
Diego Rodrigues de Sa e Souza
926b08aa80 chore docs 2026-07-03 23:54:44 -03:00
Milan Soni
cfd1f7219a docs: Normalize mixed-language documentation content (#6105)
Normalize mixed-language documentation to English. Integrated into release/v3.8.44.
2026-07-03 23:53:48 -03:00
janeza2
716041223e fix(provider-limits): clear transient rate-limit state when quota recovers (#6128)
Clear transient rate-limit state when quota recovers. Integrated into release/v3.8.44.
2026-07-03 20:29:04 -03:00
NOXX - Commiter
8cded07233 feat(build): backend-only fast build (skip the dashboard frontend) (#6119)
Backend-only fast build (skip dashboard frontend). Integrated into release/v3.8.44.
2026-07-03 20:21:11 -03:00
KooshaPari
e9d83e7c3f fix(accountFallback): persist per-account 429 cascade + classify 'Monthly usage limit. Resets in N days.' (#6061)
Persist per-account 429 cascade + classify 'Monthly usage limit. Resets in N days'. Integrated into release/v3.8.44.
2026-07-03 19:58:09 -03:00
Diego Rodrigues de Sa e Souza
e2672466c5 chore(quality): rebaseline v3.8.44 release-green drift (eslint/cognitive/cyclomatic/file-size)
Measured on release tip 32e4c906e during the #6131/#5975 release-green pass:
eslintWarnings 4256->4270 (+14), cognitiveComplexity 861->867 (+6), cyclomatic
count 2015->2026 (+11), and testFrozen caps for models-catalog-route (1507->1600),
perplexity-web (959->999), route-edge-coverage (1234->1241, my #5975 comment +7).
Inherited cycle drift (the Quality Ratchet does not run on PR->release fast-gates);
compression 'bun not found' is a local-env false and codeql is within baseline, so
neither is rebaselined. No production code touched.
2026-07-03 19:46:22 -03:00
Ankit
d072db21e6 fix(combos): expand OpenCode/MiMo fingerprint accounts in combo builder (#6087) (#6092)
Expand OpenCode/MiMo fingerprint accounts in combo builder (#6087). Integrated into release/v3.8.44.
2026-07-03 19:44:12 -03:00
Arthur Bodera
8c3bcf06b8 fix(auth): persist quota preflight account lockouts (#6090)
Persist quota preflight account lockouts until reset window. Integrated into release/v3.8.44.
2026-07-03 19:41:16 -03:00
PizzaV
421a1ec5bc fix(combo): expand fingerprint-based providers into per-fingerprint combo targets (#6082)
Expand fingerprint-based providers into per-fingerprint combo targets. Integrated into release/v3.8.44.
2026-07-03 19:39:04 -03:00
Semianchuk Vitalii
32e4c906ef fix: reqId ReferenceError in safety-net redirect, dead code, filename typo (#6097)
Fix reqId ReferenceError in safety-net combo redirect + dead-code + DESING→DESIGN rename. Integrated into release/v3.8.44.
2026-07-03 18:56:42 -03:00
Ankit
631c6c4dc3 fix(compression): send patch-only to PUT /api/settings/compression in CompressionHub (#6039) (#6077)
Send patch-only to PUT /api/settings/compression in CompressionHub (#6039). Integrated into release/v3.8.44.
2026-07-03 18:54:13 -03:00
Ankit
00d97cab74 fix(codex): use access_token.exp instead of id_token.exp for import expiresAt (#6075) (#6084)
Prefer access_token.exp over id_token.exp for Codex auth import (#6075). Integrated into release/v3.8.44.
2026-07-03 18:52:32 -03:00
Diego Rodrigues de Sa e Souza
f9b2fb4138 fix(security): persist IP filter config + enforce it in the authz pipeline (#6131) (#6132)
Integrated into release/v3.8.44 — IP filter persistence + authz-pipeline enforcement (closes #6131). HARD-neutro: validate-release-green on the merge shows the same 3 pre-existing base-reds as the release baseline (test-masking cycle-wide, unit red-herring, integration batch-E2E env); #6131's own tests + ip-filter/pipeline suites all green.
2026-07-03 18:50:29 -03:00
KooshaPari
c02f8d5c2c fix: keep Claude tool results adjacent (#6035)
Reattach OpenAI tool_result adjacent to tool_use before Claude send (#6026). Integrated into release/v3.8.44.
2026-07-03 18:42:16 -03:00
Arthur Bodera
f884c407bd fix(combo): prefer known context capacity over unknown (#6088)
When a combo filters a target for exceeding a known context limit, prefer remaining known-compatible targets over unknown-metadata ones. Regression guard: combo-context-window-filter.test.ts.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 17:10:38 -03:00
Arthur Bodera
9c2fbf577c fix(models): normalize GLM-5.2 provider context (#6091)
Hosted GLM-5.2 provider aliases now respect their declared context caps instead of inheriting the native 1M; native/bare + verified OpenCode/ZenMux routes stay at 1M. Regression guards added.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 17:08:45 -03:00
backryun
91cda11c83 fix(providers): update Gemini Web cookies and models (#6095)
Refresh Gemini Web cookie handling + model catalog. Regression guard: gemini-web.test.ts.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 16:55:12 -03:00
backryun
caeee8970d fix(providers): update Perplexity Web models (#6106)
Refresh the Perplexity Web model catalog + mode/model_preference mappings to the current live set. Regression guard: perplexity-web.test.ts.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 16:50:58 -03:00
Diego Rodrigues de Sa e Souza
1f1fda7cf5 fix(config): externalize ws for copilot-m365-web executor (#6130, closes #6062)
Re-lands the #6098 ws-externalization fix onto release/v3.8.44 (it had merged to main by mistake and was reverted). Externalize ws/bufferutil/utf-8-validate so the copilot-m365-web WebSocket masking path works at runtime.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 16:42:58 -03:00
Diego Rodrigues de Sa e Souza
dbc0cdbe1b test(embeddings): pin seeded connection to direct egress in route-edge-coverage (#5975 collateral)
#5975 made the embeddings service honor the connection-level proxy. The pre-existing
route-edge-coverage embeddings edge-case tests seed an openai connection while the
settings-proxy suite has left a provider-level proxy (provider.local:8080) in the shared
DATA_DIR that resetStorage() does not clear — inert before #5975, but now the leaked
proxy fast-fails the embedding upstream with PROXY_UNREACHABLE.

These tests do not exercise proxying, so seedOpenAIConnection now pins the connection to
proxyEnabled:false, making resolveProxyForConnection return a direct egress regardless of
leaked global proxyConfig. No assertions weakened; 16/16 in the file pass. Regression
surfaced by the concurrency=1 full-suite run; passes on #5975's parent, red after it.
2026-07-03 16:13:39 -03:00
KooshaPari
439f28d456 fix(ci): harden provider translate-path golden across CI runners (#6076)
Normalize OS/arch-derived request headers (X-Stainless-Os/Arch, (OS;arch) UAs, and Antigravity's os.platform()-derived platform substring) in the golden so the test is runner-independent. Fixes the Mac-literal Antigravity UA that would have failed on Linux CI. Supersedes stale #6002.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 16:05:15 -03:00
KooshaPari
1f6ec5bc86 feat(minimax): extract M3 reasoning_content on OpenAI-format tiers (#6073)
MiniMax M3 leaks raw <think>...</think> into content on 8 OpenAI-format provider tiers; extract it into reasoning_content, leaving the direct minimax/minimax-cn (Claude-format) tiers untouched. Replacement for the stale #5804 branch.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 16:01:31 -03:00
Paijo
ad58bc15d6 feat(proxy): batch delete, auto-test, health scheduler + transitive alias fix (#5918)
Proxy-registry batch management (batch-delete, auto-test, background health scheduler) + fix resolveProviderAlias to follow the alias chain transitively (oc -> opencode -> opencode-zen). Probe target now operator-configurable via PROXY_HEALTH_TEST_URL. Scope-creep files from the original branch dropped.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 13:31:44 -03:00
Chewji
9c6a3640fd fix(registry): update grok-cli model context lengths (#5913)
grok-build 128k→256k, grok-composer-2.5-fast 128k→200k to match actual Grok CLI /context capacities so context-aware routing stops filtering these models out. Registry-only.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 12:53:26 -03:00
Diego Rodrigues de Sa e Souza
29e04b7089 fix(providers): strip orphan tool_result on Antigravity MITM path (#6026) (#6115) 2026-07-03 12:50:26 -03:00
Diego Rodrigues de Sa e Souza
2a20e5ca47 fix(providers): emulate OpenAI tool_calls in GitLab Duo executor (#6051) (#6111)
Co-authored-by: felssxs <felssxs@users.noreply.github.com>
2026-07-03 12:49:56 -03:00
Diego Rodrigues de Sa e Souza
513e3b0047 Merge pull request #6110 from diegosouzapw/fix/release-envdoc-bifrost-port
fix(ci): document BIFROST_PORT — clear env-doc-sync base-red blocking the PR queue
2026-07-03 12:11:10 -03:00
Diego Rodrigues de Sa e Souza
8d7e3e28f2 fix(ci): document BIFROST_PORT to clear env-doc-sync base-red
The Bifrost embedded-service merge referenced process.env.BIFROST_PORT
(src/lib/services/bootstrap.ts, default 8080) without adding it to
.env.example / ENVIRONMENT.md, so check:env-doc-sync failed on the release
tip and reddened Fast Quality Gates for every open PR->release. Docs-only.
2026-07-03 12:10:07 -03:00
Diego Rodrigues de Sa e Souza
d0be015e38 Merge pull request #5912 from chirag127/fix/pnpm11-allowbuilds-workspace
fix(install): pnpm 11 ERR_PNPM_IGNORED_BUILDS — add allowBuilds to pnpm-workspace.yaml
2026-07-03 10:29:08 -03:00
Diego Rodrigues de Sa e Souza
556f191d30 feat(services): promote Bifrost to embedded/supervised service (#5670) (#5817)
Promotes Bifrost (@maximhq/bifrost — Go AI-gateway) from an env-only relay
sidecar to a first-class embedded/supervised service, matching the existing
cliproxy/9router model. Implements item #2 of #5670; the broader RouterBackend
contract (items #1, #3-#5) stays out of scope.

- Installer (npm-style, ninerouter model): install/update/getInstalledVersion/
  getLatestVersion (1h cache)/resolveSpawnArgs (Go single-dash flags, pinned
  BIFROST_TRANSPORT_VERSION), needsApiKey=false
- Bootstrap SERVICES entry (healthPath /v1/models) + spawn-args factory branch
- Migration 113 seeds the version_manager row (not_installed, port 8080,
  auto_update=1, provider_expose=1)
- 7 lifecycle API routes under /api/services/bifrost/ (verbatim from cliproxy,
  errors sanitized) — loopback-only via existing LOCAL_ONLY_API_PREFIXES
- Shared [name]/logs branch for bifrost
- Dashboard tab + registration in the services page shell
- Relay auto-wiring: getBifrostRoutingConfig defaults BIFROST_BASE_URL to the
  supervised port when the instance is running; explicit env still wins; the
  env-only relay path (/v1/relay/.../bifrost) stays unchanged (compat layer)
- Docs (EMBEDDED-SERVICES, openapi) + unit tests (installer/route-guard/routing,
  19 tests) + RUN_SERVICES_INT-gated integration lifecycle

Note: the actual Go-binary install/start/health path requires a documented VPS
live-test before merge (Hard Rule #18 / spec section 7); the gated integration
harness is the vehicle for that run.
2026-07-03 09:05:41 -03:00
Diego Rodrigues de Sa e Souza
dc7892c4b0 feat(services): add Mux managed embedded service (#6034)
Adds Mux (coder/mux — local agent-orchestration daemon) as a fourth-tier
embedded service built on the existing ServiceSupervisor framework, the
same shape as 9Router and CLIProxyAPI:

- Installer (src/lib/services/installers/mux.ts): npm install/update via
  runNpm (array args + env-based prefix, no shell interpolation), modeled
  on ninerouter.ts. Mux ships an npm package (`mux`) with a documented
  headless `mux server --host <host> --port <port>` mode, so no
  git-clone+build path was needed.
- Registered in bootstrap.ts (SERVICES[] + buildSpawnArgsFactory).
- DB seed migration 113 (version_manager row, not_installed/auto_start=0).
- 7 API endpoints under /api/services/mux/ (install/start/stop/restart/
  update/status/auto-start) plus the shared [name]/logs SSE endpoint,
  mirroring the cliproxy route shape and delegating errors through
  createErrorResponse().
- Dashboard tab (MuxServiceTab) reusing ServiceStatusCard,
  ServiceLifecycleButtons, AutoStartToggle, ServiceLogsPanel.
- Docs: EMBEDDED-SERVICES.md (service table, architecture diagram, API
  reference, key-injection section), openapi.yaml, ENVIRONMENT.md,
  .env.example.

Security:
- Every /api/services/mux/* route is covered by the existing
  LOCAL_ONLY_API_PREFIXES "/api/services/" prefix (Hard Rule #17);
  added an explicit isLocalOnlyPath regression test for all 8 routes.
- Mux binds to 127.0.0.1 explicitly (never 0.0.0.0) as defense-in-depth,
  since it orchestrates AI agents that can execute host commands.
- The bearer token is generated the same way as 9Router's key
  (getOrCreateApiKey) and injected via MUX_SERVER_AUTH_TOKEN (mux's
  documented env form) rather than a CLI flag, so it never appears in
  `ps`/process listings.
- No shell interpolation anywhere in the installer (Hard Rule #13): all
  npm/spawn args are static arrays; the install prefix and auth token
  travel via the env option.


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

Co-authored-by: Ansh7473 <Ansh7473@users.noreply.github.com>
2026-07-03 08:57:02 -03:00
Diego Rodrigues de Sa e Souza
b7160e9fc5 feat(xai): surface Grok usage on quota dashboard via local usageHistory aggregation (#5806)
xAI has no public per-account quota API (the billing console requires a
session cookie, not an API key). Add getXaiUsage(connectionId), mirroring
the existing Xiaomi MiMo self-track pattern: sum tokens routed to the
connection from usage_history via getMonthlyProviderTokensForConnection
and surface them as a cumulative, uncapped quota (unlimited: true,
remaining: 100 — xAI has no fixed monthly cap). Register 'xai' in
USAGE_FETCHER_PROVIDERS and wire a switch case in getUsageForProvider.


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

Co-authored-by: ron <devestacion@gmail.com>
2026-07-03 08:51:01 -03:00
Markus Hartung
5fe225850e fix(combo): fallback to sibling model on 500 for per-model-quota providers (#5976)
* fix(combo): fallback to sibling model on 500 for per-model-quota providers

Two issues prevented combo fallback when gemini/gemma-4-31b-it returned 500:

1. targetExhaustion: connection-level exhaustion marked the shared gemini
   connection as exhausted, skipping the sibling model (gemma-4-26b-a4b-it).
   Skip markConnectionLevelExhaustion for per-model-quota providers (gemini,
   github, passthrough, compatible) since a model-level 500 does not mean
   the connection is bad.

2. combo retry loop: the auth layer records a model lockout on 500, but the
   retry loop did not check isModelLocked before retrying — it retried the
   same locked model instead of falling back. Add isModelLocked guard before
   the transient-retry decision.

* fix tests timeout

* fix: clear quota fallback CI gates

* quality-gate: extract test SSE stream helpers

* drop scope creep

* fix(combo): retry sibling models only on 500 errors

* fix(combo): reconcile onto release/v3.8.44 — keep targetExhaustion 500 fix, drop slow integration test

Reconciled by maintainer onto the current release tip:
- kept the core fix (targetExhaustion.ts model-500 guard for per-model-quota
  providers + the isModelLocked retry early-return in combo.ts) and its unit test
- dropped tests/integration/combo-concurrent-failure-recovery.test.ts +
  _sseTestHelpers.ts: they use Math.random()-based delays and 30s timeouts, run
  >3min and are flake-prone in the test:integration CI job; the unit test
  (tests/unit/combo/combo-target-exhaustion.test.ts, 21 cases) fully covers the fix
- CHANGELOG entry added

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: Koosha Pari <kooshapari@gmail.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: hartmark <hartmark@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 08:48:52 -03:00
Diego Rodrigues de Sa e Souza
4573684eee refactor(sse): extract applyStrategyOrdering leaf from handleComboChat (Block J Task 3) (#6063)
* refactor(sse): extract applyStrategyOrdering leaf from handleComboChat

Block J Task 3: the ~177-line else-if chain covering every non-auto combo
strategy (lkgp / strict-random / random / fill-first / p2c / least-used /
cost-optimized / reset-aware / reset-window / context-optimized / headroom /
quota-share) is extracted into
open-sse/services/combo/applyStrategyOrdering.ts::applyStrategyOrdering.

Each branch only reorders orderedTargets (no early returns, no other mutable
state), so the extraction is a clean verbatim move returning the reordered list;
the host replaces the chain with `else { orderedTargets = await
applyStrategyOrdering(strategy, orderedTargets, deps); }`. Semantic diff vs the
original chain = only the leading `if` (was `} else if`), the trailing return
and the deeper getLKGP import path — no logic line changed. None of the 13 strategy
helpers live in combo.ts, so no DI/cycle (unlike the auto branch).

combo.ts 3065->2883 LOC (3309->2883 across Task 2+3). typecheck:core + check:cycles
clean; 9 dead host imports removed (targetSorters block emptied). 47/47 consumer
tests (router-strategies / combo-strategy-fallbacks / rr-session-stickiness /
tag-routing) cover the DB-backed branches end-to-end; new
tests/unit/combo-apply-strategy-ordering-split.test.ts pins random / fill-first /
unknown exits.

* test(sse): point #2359 modelStr-guard scans at applyStrategyOrdering leaf

The LKGP fallback + non-auto strategy ordering (the two target.modelStr string-
method call sites) were extracted verbatim from combo.ts into the
applyStrategyOrdering leaf (Block J Task 3). The #2359 source scans now read the
leaf that owns those usages; the guard and the no-unguarded-usage assertions are
unchanged in intent.

* chore(ci): scan combo strategy leaves in check:known-symbols

Block J decomposed the combo dispatch: the `strategy === "..."` branches for
the 12 non-auto strategies moved to combo/applyStrategyOrdering.ts and the auto
branch to combo/resolveAutoStrategy.ts. The known-symbols gate previously scanned
only combo.ts, so it would report those strategies as canonicalNotHandled. Scan
all three dispatch files. Verified: 18/18 canonical strategies via dispatch.
2026-07-03 08:44:52 -03:00
KooshaPari
772fea3f4d fix: unwrap Cline response envelope (#6046)
Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
2026-07-03 08:40:43 -03:00
KooshaPari
5d902e5b66 feat(minimax): extract M3 <think> to reasoning_content on OpenAI-format tiers (#6050)
MiniMax M3 is registered with format:"openai" on 8 provider tiers (trae,
huggingchat, bazaarlink, ollama-cloud, opencode, cline, opencode-zen,
codebuddy-cn), where its raw <think>...</think> tags leaked directly into
`content` instead of surfacing as a separate `reasoning_content` field.

OmniRoute already has the extraction primitive
(extractThinkingFromContent in responseSanitizer/reasoning.ts); it was just
gated to deepseek-r1/r1-distill/qwq. Extend the allowlist
(isTextualReasoningTagNativeRoute) with a minimax-m3-only pattern, excluding
the two direct minimax/minimax-cn tiers, which stay on Anthropic's Messages
format (targetFormat: "claude") and already surface reasoning natively.


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

Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: zmf963 <19422469+zmf963@users.noreply.github.com>
2026-07-03 08:36:31 -03:00
janeza2
8d0ed4f936 fix(kimi-web, qwen-web): align model catalog with live /models + map scenario per model (#5915)
* fix(kimi-web): align catalog with live models

Update the kimi-web catalog and request scenario selection to match
www.kimi.com's live GetAvailableModels response.

* fix(qwen-web): stop aliasing qwen3-coder-plus

Keep qwen3-coder-plus as its own model because it is present in the
live Qwen web models catalog.
2026-07-03 08:35:56 -03:00
Diego Rodrigues de Sa e Souza
f496738d7f fix(translator): strip multipleOf from antigravity/gemini tool schemas (port from 9router#2309) (#6052)
`multipleOf` is not part of the Gemini/antigravity OpenAPI 3.0 schema subset, so
leaving it in function_declaration parameters triggered a hard upstream 400
("Unknown name multipleOf"). Add it to GEMINI_UNSUPPORTED_SCHEMA_KEYS so it is
stripped at every schema level; minimum/maximum stay (Gemini accepts them).

Reported-by: abil0321 (https://github.com/decolua/9router/issues/2309)
2026-07-03 08:35:02 -03:00
Diego Rodrigues de Sa e Souza
90aca8b85e fix(translator): wrap Kiro system prompt in <system-reminder> (port from 9router#2306) (#6053)
Kiro/CodeWhisperer has no system role, so system messages were normalized to a
user turn with no wrapper — the full Claude Code system prompt then appeared as
raw user text, polluting the model context. Wrap system-origin content in
<system-reminder> tags before merging it into the Kiro user message. Real user
turns are unaffected. Existing history-merge tests aligned to the wrapped value.

Reported-by: VitzS7 (https://github.com/decolua/9router/issues/2306)
2026-07-03 08:31:12 -03:00
Diego Rodrigues de Sa e Souza
65602b5477 fix(ci): release-green base-reds — #5695 test regex + file-size rebaseline (#6093)
- tests/unit/ui/quick-start-api-keys-link-5695.test.ts: tolerate Prettier
  splitting <Link href=...> across lines (\s+) so the step1Desc regex matches
  the multi-line /dashboard/api-manager Link instead of skipping to step2's
  single-line /dashboard/providers Link. Code is correct; the test was brittle.
- config/quality/file-size-baseline.json: rebaseline 5 files that grew via
  already-merged PRs on the release tip (ApiManagerPageClient 3017->3058,
  OAuthModal 969->989, cliRuntime 1090->1100, webProvidersA 805->809,
  deepseek-web.test 1081->1092). Dated note added; shrink tracked in #3501.
2026-07-03 08:30:07 -03:00
Diego Rodrigues de Sa e Souza
d6dc869c9c refactor(sse): decompose handleComboChat auto-strategy region (Block J Task 2 — parseAutoConfig + resolveAutoStrategyOrder) (#6049)
* refactor(sse): extract pure parseAutoConfig leaf from handleComboChat

Block J Task 2 (safe slice): the auto-strategy config-resolution block in
handleComboChat is a pure function of (combo, eligibleTargets) with no side
effects, no early returns and no mutation. Extract it verbatim into
open-sse/services/combo/autoConfig.ts::parseAutoConfig so the god-function
shrinks and the derivation is independently unit-testable.

Behavior is byte-identical (verbatim-audited); combo.ts 3309->3280 LOC.
Adds tests/unit/combo-auto-config-split.test.ts (5 cases) pinning the
strategy-precedence, candidate-pool, weights and fallback derivations.

* refactor(sse): extract resolveAutoStrategyOrder leaf from handleComboChat

Block J Task 2 (coupled slice): the ~215-line `if (strategy === "auto")`
branch of handleComboChat is extracted into
open-sse/services/combo/resolveAutoStrategy.ts::resolveAutoStrategyOrder.

The branch is a control-flow region (mutates orderedTargets +
autoUsedExplicitRouter, early-returns 429, side-effect _registerExecutionCandidates),
so it is not a pure byte-identical move: the two `return unavailableResponse(...)`
exits become `{ earlyResponse }` and the mutated locals are returned instead of
closed over. Every other logic line is verbatim (semantic diff = only those
wrappers + the deeper getLKGP import path). `buildAutoCandidates` lives in
combo.ts, so it is injected via deps to keep the leaf acyclic (same DI pattern as
buildTargetTimeoutRunner) — which also makes the branch independently testable.

combo.ts 3280->3065 LOC. typecheck:core + check:cycles clean; dead host imports
removed. 60/60 consumer tests (router-strategies / auto-combo-engine /
combo-strategy-fallbacks / scoring-clamp / candidate-expansion / hidden-models)
cover the routable path end-to-end; new tests/unit/combo-resolve-auto-strategy-split.test.ts
pins the DI contract + the early-429 and default-ordering exits.

* test(sse): point quota-bypass source scan at resolveAutoStrategy leaf

The 'auto combo disables hard provider quota cutoffs when relay requests bypass'
source scan asserted combo.ts contains the bypass logic
(relayOptions?.bypassProviderQuotaPolicy === true + quotaPreflight enabled:false).
That block was extracted verbatim into combo/resolveAutoStrategy.ts (Block J
Task 2), so the scan now reads the leaf. Behavior unchanged.
2026-07-03 04:00:03 -03:00
Diego Rodrigues de Sa e Souza
8fb020676e docs(openapi): document 9 newly-added routes to restore coverage ratchet (v3.8.44)
Documents the routes added this cycle that dropped openapiCoverage 36.9%->36.2%
below the ratchet baseline: 2 public v1 endpoints (/v1/ocr Mistral-OCR-compatible,
/v1/audio/translations Whisper-compatible) with full request/response specs, plus 7
dashboard/CLI-local routes marked x-internal:true (suggested-models, provider-plugin-
manifest, keys/{id}/devices, settings/purge-usage-history, oauth/codex/import-token,
cli-tools crush-settings + codewhale-settings). Coverage 36.2%->37.8% (207/547),
above baseline 36.9. check:openapi-routes/security-tiers/fabricated-docs all pass.
2026-07-03 02:48:21 -03:00
Diego Rodrigues de Sa e Souza
e12bbd33ad feat(providers): client-identity header profiles for compatible nodes (re-cut) + forbid cookie in custom headers (#5812) 2026-07-03 02:23:23 -03:00
Diego Rodrigues de Sa e Souza
6a04114f0e feat(claude-code): opt-in auto-permission classifier compat mode (re-cut onto release tip) (#5810) 2026-07-03 02:15:59 -03:00
Diego Rodrigues de Sa e Souza
204bd7f465 chore(quality): rebaseline eslintWarnings 4199->4256 + cognitiveComplexity 860->861 (v3.8.44 cycle drift)
Inherited v3.8.44 cycle drift measured on release tip 72ee80649 by the release-green
pre-flight during the /review-prs fix-batch round. The Quality Ratchet does NOT run on
PR->release fast-gates, so eslint warnings + cognitive complexity accrue unmeasured
across the cycle. Cyclomatic complexity is already green (2012 < baseline 2015) and
needs no bump. Each value carries a dated justification note; no production code touched.
2026-07-03 02:15:05 -03:00
Diego Rodrigues de Sa e Souza
0e9853460f feat(usage): on-demand period-scoped usage-data reset (re-cut onto release tip) (#5831) 2026-07-03 02:12:31 -03:00
Diego Rodrigues de Sa e Souza
83f67c8e0e docs(changelog): restore #5181/#5199/#5462 feature bullets eaten by merge 2026-07-03 02:03:18 -03:00
Diego Rodrigues de Sa e Souza
29fc4a7395 Merge PR #6045: CN sign-up geo-restriction notices for SenseNova & StepFun (#5462) 2026-07-03 02:01:30 -03:00
Diego Rodrigues de Sa e Souza
4f1803ac84 Merge PR #6038: surface Codex banked reset credits per account (#5199) 2026-07-03 02:01:17 -03:00
Diego Rodrigues de Sa e Souza
95218647c9 Merge PR #6037: refresh The Old LLM (Free) model catalog (#5181) 2026-07-03 02:01:08 -03:00
KooshaPari
72ee80649c feat(autoCombo): latency/speed-optimized routing mode + omniroute_pick_fastest_model MCP tool (#6011)
* feat(autoCombo): latency/speed-optimized routing mode + omniroute_pick_fastest_model MCP tool

* test(translator): split responses chat request coverage

* refactor(mcp): extract fastest-model tool modules

* fix(i18n): cover provider icon and cors labels

* test(mutation): register latency coverage files

* test(ci): collect executor unit tests

* refactor(ci): reduce latency path complexity

* fix(mcp): include models catalog module

* feat(autoCombo): latency/speed-optimized routing + omniroute_pick_fastest_model MCP tool

Re-cut onto release tip: keep speed-routing + MCP tool + supporting catalog split;
drop out-of-scope translator split, en.json/ci.yml/package.json orphans, and unrelated
proxyFetch/responsesStreamHelpers/tokenLimitCounter refactors.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: kooshapari <kooshapari@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 01:38:51 -03:00
KooshaPari
0c2b0571ee feat(sidecar): advertise provider manifest url (#6007)
* feat(sidecar): advertise provider manifest url via X-OmniRoute-Provider-Manifest-Url header

Re-cut onto release tip: manifest-url feature only (dropped stale-base noise).

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* docs(changelog): add sidecar manifest-url entry

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* chore(complexity): rebaseline 2009->2015 (inherited release-tip drift; feature adds 0)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 01:32:45 -03:00
Diego Rodrigues de Sa e Souza
96c04f75bf feat(providers): add CN sign-up geo-restriction notices for SenseNova & StepFun (#5462) 2026-07-03 01:26:46 -03:00
KooshaPari
6bce788aed feat(api): expose provider plugin manifest (#6001)
* feat(api): expose provider plugin manifest

* test(translator): split responses chat request coverage

* test(mutation): register provider coverage tests

* feat(api): expose provider plugin manifest

* fix(ci): fail closed for prerelease latest promotion

* chore(ci): reconcile provider manifest complexity gate

* feat(api): expose provider plugin manifest

* test(translator): split responses chat request coverage

* test(mutation): register provider coverage tests

* fix(ci): fail closed for prerelease latest promotion

* chore: rebase onto release tip; drop out-of-scope translator test split + promote-script tweak

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* docs(changelog): add provider plugin manifest entry

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

* chore(stryker): register account-fallback-retry-after-json test (base-red)

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>

---------

Co-authored-by: kooshapari <kooshapari@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@outlook.com>
Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-03 01:24:08 -03:00
Diego Rodrigues de Sa e Souza
e34d1a4b57 fix(dashboard): render Update-now API errors as text, not the raw envelope object (#5991) (#6028)
Integrated into release/v3.8.44 — fix(dashboard) render Update-now API errors as text, not the raw envelope object (#5991).

Merged with --admin: the fix is a one-line frontend change funneling the error body through the already-tested extractApiErrorMessage() helper, guarded by tests/unit/ui/home-update-error-render-5991.test.ts (3/3 pass, 3/3 fail on pre-fix source). The release branch is under a heavy parallel-merge storm (tip advanced ~6× mid-CI), so the branch is synced to the latest tip and landed atomically to avoid perpetual CONFLICTING; unit-shard reds seen earlier were pre-existing base-reds/flakes unrelated to this source-scan-only change.
2026-07-03 01:01:25 -03:00
Diego Rodrigues de Sa e Souza
c9032e478b feat(i18n): auto-detect browser language on first visit (#5979)
* feat(i18n): auto-detect browser language on first visit

Adds a pure detectBrowserLocale() matcher (exact match, zh-HK/zh-MO
folded to zh-TW, language-prefix match, else null) plus a client-only
LocaleAutoDetect component mounted once in the root layout. On first
visit (no locale cookie set), it reads navigator.languages, computes a
match against the supported locales, and persists it via the same
cookie/localStorage writer LanguageSelector already used for manual
selection (now extracted to shared/lib/persistLocale.ts) before
refreshing the router.

Co-authored-by: anmingwei <anmingwei@dobest.com>
Inspired-by: https://github.com/decolua/9router/pull/1324

* chore(changelog): restore release entries + add browser-lang-detect bullet

---------

Co-authored-by: anmingwei <anmingwei@dobest.com>
2026-07-03 00:57:58 -03:00
Diego Rodrigues de Sa e Souza
1bd4b02110 feat(cli-tools): add CodeWhale CLI tool (#5996)
CodeWhale (https://github.com/Hmbown/CodeWhale) is the actively-maintained
successor to DeepSeek TUI — same author, renamed project. Added as a dual
entry alongside the existing "deepseek-tui" catalog entry (rather than a
hard rename) so users who still run the old DeepSeek TUI binary keep a
working dashboard card, while new users are steered to "codewhale".

New /api/cli-tools/codewhale-settings route writes the primary
~/.codewhale/config.toml and keeps an existing legacy
~/.deepseek/config.toml in sync (read fallback + best-effort write sync),
mirroring deepseek-tui-settings/route.ts. CLI_TOOLS and cliRuntime catalogs
updated; catalog cardinality tests/constants bumped accordingly (18→19
visible code tools, 28→29 total).


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

Co-authored-by: aristorinjuang <aristorinjuang@gmail.com>
2026-07-03 00:57:06 -03:00
Diego Rodrigues de Sa e Souza
373dd17ccb refactor(combo): extract buildTargetTimeoutRunner from handleComboChat (#6036)
Bloco J (hot-path decomposition), Task 1. Extract the per-target-timeout dispatch wrapper
(handleComboChat's handleSingleModelWithTimeout closure) verbatim into the leaf
combo/targetTimeoutRunner.ts as a factory buildTargetTimeoutRunner({handleSingleModel,
comboTargetTimeoutMs, log}). The per-model abort still comes from target.modelAbortSignal,
so the outer request signal is intentionally not a dependency. Host call-sites unchanged.

combo.ts shrinks ~60 LOC; leaf is 91 LOC (<800). Body byte-identical (verbatim), no cycle.
This is the first slice toward extracting the shared attempt-loop/success/error handlers
(Tasks 3-4) that de-duplicate handleComboChat and handleRoundRobinCombo. Adds a dedicated
test (5) so the failover path can be mutated independently. Consumer tests stay green
(combo-strategy-fallbacks 24, combo-499-abort 5, empty-content-failover 3, body-400-stop 1,
priority-quota-exhaustion 2, rr-streaming-lock 1, rr-session-stickiness 2).

Plan: _tasks/superpowers/plans/2026-07-03-blocoJ-combo-hotpath-decomposition.md
2026-07-03 00:56:45 -03:00
Diego Rodrigues de Sa e Souza
4e16a491a1 feat(server): support reverse-proxy basePath deployment (#5992)
* feat(server): support reverse-proxy basePath deployment

Adds OMNIROUTE_BASE_PATH (opt-in, empty by default) to next.config.mjs
using Next.js's native basePath support so a deployment behind a
reverse-proxy subpath (e.g. https://host/omniroute/) works without
manual header stripping. Next.js strips the configured prefix from
nextUrl.pathname before route classification, so classifyRoute() and
isLocalOnlyPath() keep matching un-prefixed paths.

The two hardcoded auth redirect targets in
src/server/authz/pipeline.ts (root "/" -> "/dashboard" and
unauthenticated dashboard -> "/login") now prefix with
request.nextUrl.basePath so they stay inside the deployed subpath.
Default empty basePath is a no-op for existing root-path deployments.

Co-authored-by: zocomputer <help@zocomputer.com>
Inspired-by: https://github.com/decolua/9router/pull/1810

* docs(env): document OMNIROUTE_BASE_PATH in .env.example + ENVIRONMENT.md; restore changelog

* docs(env): document AUGGIE_BIN + CLI_AUGGIE_BIN (base-red from #5972 auggie)

---------

Co-authored-by: zocomputer <help@zocomputer.com>
2026-07-03 00:53:12 -03:00
Diego Rodrigues de Sa e Souza
f984bef4ec feat(providers): add SumoPod and X5Lab OpenAI-compatible providers (#5963)
* feat(providers): add SumoPod and X5Lab OpenAI-compatible providers

Both are OpenAI-compatible BYOK aggregator gateways, wired via the
default executor with bearer API-key auth. Neither ships a hardcoded
model list — both use passthroughModels with an empty seed list and a
live /v1/models fetcher, so the catalog always reflects what each
gateway actually serves instead of speculative model IDs.

- SumoPod: https://ai.sumopod.com/v1/chat/completions (sk- keys)
- X5Lab: https://api.x5lab.dev/v1/chat/completions (x5- keys)

Regression guard: tests/unit/sumopod-x5lab-provider.test.ts.

Co-authored-by: Rigel Ramadhani Waloni <rigel8911@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1288

* chore(changelog): restore release entries + add sumopod/x5lab bullet

* test(golden): regenerate translate-path for sumopod + x5lab providers

* test(providers): bump APIKEY count 164→166 for sumopod + x5lab

---------

Co-authored-by: Rigel Ramadhani Waloni <rigel8911@gmail.com>
2026-07-03 00:46:30 -03:00
Diego Rodrigues de Sa e Souza
7658a7da1e feat(providers): add Charm Hyper OpenAI-compatible provider (#5961)
* feat(providers): add Charm Hyper OpenAI-compatible provider

Registers Charm Hyper (hyper.charm.land) as a new API-key gateway
provider: OpenAI-compatible chat completions format, bearer auth,
free tier (100 monthly Hypercredits). Models are resolved via
passthrough (modelsUrl + live /v1/models import) instead of a
hardcoded upstream model list, since the specific model catalog is
not publicly documented.

Co-authored-by: whale <admin@dyntech.cc>
Inspired-by: https://github.com/decolua/9router/pull/2006

* test(golden): regenerate translate-path for charm-hyper provider

* test(providers): bump APIKEY count 163→164 for charm-hyper

---------

Co-authored-by: whale <admin@dyntech.cc>
2026-07-03 00:45:06 -03:00
Diego Rodrigues de Sa e Souza
50f770583d feat(providers): add Nube.sh OpenAI-compatible provider (#5936)
* feat(providers): add Nube.sh OpenAI-compatible provider

Nube.sh is a live BYOK OpenAI-compatible gateway (LiteLLM proxy) at
https://ai.nube.sh/api/v1, Bearer/API-key auth. Registered as an apikey
inference-host with an OpenAI-format, default-executor registry entry.

Its live model catalog is only reachable with a valid key
(/api/v1/models returns 401 unauthenticated), so no model IDs are
hardcoded — the entry uses passthroughModels + modelsUrl for live
enumeration instead of shipping unverifiable IDs.

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

* test(golden): regenerate translate-path for nube provider

* test(providers): bump APIKEY count 162→163 for nube

---------

Co-authored-by: whale9820 <87256750+whale9820@users.noreply.github.com>
2026-07-03 00:43:41 -03:00
Diego Rodrigues de Sa e Souza
70dd5df445 feat(providers): add b.ai OpenAI-compatible provider (#5969)
* feat(providers): add b.ai OpenAI-compatible provider

Adds bai as a new OpenAI-compatible BYOK provider, distinct from the
existing thebai/theb.ai provider, using passthrough model discovery
(no hardcoded model list, live catalog served from api.b.ai/v1/models).

Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
Inspired-by: https://github.com/decolua/9router/pull/963

* test(golden): regenerate translate-path for b.ai provider

* test(providers): bump APIKEY count 161→162 for b.ai

---------

Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
2026-07-03 00:42:04 -03:00
Diego Rodrigues de Sa e Souza
a518ce8f8a feat(providers): add Qiniu OpenAI-compatible provider (#5966)
* feat(providers): add Qiniu OpenAI-compatible provider

Wires Qiniu (七牛云) AI inference gateway as a BYOK API-key provider.
Qiniu proxies many upstream models (DeepSeek V3/V4, Claude, Kimi and
more) behind a single key, so it ships with an empty static seed and
relies on passthroughModels + the live /v1/models catalog instead of a
single stale hardcoded model id.

- metadata: src/shared/constants/providers/apikey/gateways.ts
- registry entry: open-sse/config/providers/registry/qiniu/index.ts
  (format openai, executor default, bearer auth, baseUrl
  https://api.qnaigc.com/v1/chat/completions, modelsUrl
  https://api.qnaigc.com/v1/models)
- added to NAMED_OPENAI_STYLE_PROVIDERS so model import serves the live
  catalog and falls back to the (empty) local catalog on error, same
  pattern as the existing dgrid/zenmux/orcarouter gateways
- tests: tests/unit/qiniu-provider.test.ts (metadata, registry
  resolution, passthrough validation, live /v1/models fetch + fallback)

Co-authored-by: JiangZhuo <jiangzhuo@qiniu.com>
Inspired-by: https://github.com/decolua/9router/pull/911

* chore(changelog): restore release entries + add qiniu bullet

* test(golden): regenerate translate-path for qiniu provider

* test(providers): bump APIKEY count 160→161 for qiniu

---------

Co-authored-by: JiangZhuo <jiangzhuo@qiniu.com>
2026-07-03 00:40:07 -03:00
Diego Rodrigues de Sa e Souza
7aaf109c0c feat(providers): add ModelScope OpenAI-compatible provider (#5965)
* feat(providers): add ModelScope OpenAI-compatible provider

Ports ModelScope (Alibaba 魔搭) as a new API-key, OpenAI-compatible
provider — upstream 9router PR #1764. The upstream PR hardcoded
`https://api-inference.modelscope.ai/...` (`.ai` TLD); verified against
ModelScope's own API-Inference docs and third-party integration guides
that the real production domain is `api-inference.modelscope.cn`
(`.cn` TLD) and shipped that instead. Also drops the PR's static
5-model snapshot in favor of `passthroughModels: true` with an empty
seed list + `modelsUrl`, since ModelScope's open-model catalog moves
fast.

Updates the providers-constants-split characterization test's hardcoded
APIKEY_PROVIDERS count (159 -> 160) to match the new entry.

Co-authored-by: Umar Javed <114807145+tn5052@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/1764

* chore(changelog): restore release entries + add modelscope bullet

* test(golden): regenerate translate-path for modelscope provider

---------

Co-authored-by: Umar Javed <114807145+tn5052@users.noreply.github.com>
2026-07-03 00:38:06 -03:00
Diego Rodrigues de Sa e Souza
196375b8a9 feat(providers): add Augment (Auggie CLI) local provider (#5972)
* feat(providers): add Augment (Auggie CLI) local provider

Adds a new local, no-auth provider that spawns the user's local `auggie`
CLI (`auggie --print --quiet --model <m> --`) and pipes a flattened prompt
via stdin, wrapping stdout as an OpenAI-compatible SSE stream or a single
chat.completion JSON body depending on the request's `stream` flag.

Auth is delegated entirely to `auggie login` outside OmniRoute — the
connection is registered `noAuth: true` and `refreshCredentials()` is a
no-op, matching the existing `NOAUTH_PROVIDERS` credential-less flow
(synthetic connection, no DB row required). An optional connection row is
still admitted via `FREE_APIKEY_PROVIDER_IDS` for display/priority
tracking, consistent with `opencode`. The dashboard "Test Connection"
flow spawns `auggie --version` to confirm the CLI is installed and
runnable, since there is no API key to validate upstream.

Security hardening (spawn is an untrusted-input sink):
- Command injection: spawn no longer passes `shell: true` on Windows. The
  binary is resolved to a concrete path/name and argv is handed straight to
  the OS loader, so no cmd.exe metacharacter interpretation is possible.
- Argument injection (flag smuggling): `model` is validated against the
  registry allowlist (`auggieProvider.models`) before any spawn — a model
  that is unknown or starts with "-" is rejected with a sanitized error and
  the subprocess is never started. A trailing `--` marks end-of-options in
  the argv as belt-and-suspenders.

Co-authored-by: chamdanilukman <16629923+chamdanilukman@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/1200

* test(golden): regenerate translate-path for auggie provider

---------

Co-authored-by: chamdanilukman <16629923+chamdanilukman@users.noreply.github.com>
2026-07-03 00:35:43 -03:00
Diego Rodrigues de Sa e Souza
ef7b4febee feat(providers): add NVIDIA NIM image generation (#5971)
* feat(providers): add NVIDIA NIM image generation

NVIDIA already exists as a chat provider (integrate.api.nvidia.com,
OpenAI-compatible) but image generation is served on a different host
(ai.api.nvidia.com/v1/genai/<model>) with a native NIM body shape, so it
gets a dedicated `nvidia-nim` image format and handler rather than reusing
the OpenAI image path.

Adds the 4 FLUX models (flux.1-dev, flux.1-schnell, flux.1-kontext-dev,
flux.2-klein-4b) to IMAGE_PROVIDERS, plus handleNvidiaNimImageGeneration()
which shapes the per-model NIM request body (flux.1-dev's mode/cfg_scale
and 768-1344px/64px-increment dimension validation, flux.1-kontext-dev's
required input image + aspect_ratio, schnell/klein's optional array-form
edit image) and normalizes the NIM response (artifacts[]/images[]/data[]/
single-value shapes) into the OpenAI `{created, data}` shape.

Co-authored-by: eng2007 <aleksey.semenov@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1195

* chore(changelog): restore release entries + add nvidia-nim image bullet

---------

Co-authored-by: eng2007 <aleksey.semenov@gmail.com>
2026-07-03 00:33:05 -03:00
Diego Rodrigues de Sa e Souza
ff64c9e0bb feat(dashboard): surface Codex banked reset credits per account (#5199) 2026-07-03 00:20:58 -03:00
Diego Rodrigues de Sa e Souza
08acf86fe3 fix(api): guard shared API client against non-JSON error responses (#5973)
Integrated into release/v3.8.44 — guard shared API client against non-JSON error responses.
2026-07-03 00:19:06 -03:00
Diego Rodrigues de Sa e Souza
e6e6d36907 fix(embeddings): forward connection-level proxy to embedding requests (#5975)
Integrated into release/v3.8.44 — forward connection-level proxy to embedding requests.
2026-07-03 00:18:20 -03:00
Diego Rodrigues de Sa e Souza
85a7be992c fix(resilience): parse Retry-After from 429 JSON body for cooldown (#5974)
Integrated into release/v3.8.44 — parse Retry-After from 429 JSON body for cooldown (incl. #6013 retry-after-json extraction by @KooshaPari).
2026-07-03 00:17:25 -03:00
Diego Rodrigues de Sa e Souza
cd10a6f5f4 feat(oauth): import Codex connection from a raw ChatGPT access token (#5995)
* feat(oauth): import Codex connection from a raw ChatGPT access token

OmniRoute's only Codex import path (/api/oauth/codex/import) required both
access_token and refresh_token, leaving no import path for a user who only
has a bare ChatGPT website access token (no refresh token).

- src/lib/db/providers.ts: createProviderConnection gains an explicit
  authType "access_token" branch — intentionally never deduped (no stable
  long-lived identity to match on) — and derives the connection name from
  email/name the same way "oauth" does.
- src/lib/oauth/services/codexImport.ts: export extractCodexAccountInfo so
  the new import path reuses the existing JWT decode instead of duplicating
  one.
- New route POST /api/oauth/codex/import-token (Zod-validated body
  { accessToken, name? }); errors routed through buildErrorBody /
  sanitizeErrorMessage. The executor's refreshCredentials() already
  degrades safely to null when there is no refresh token, forcing re-auth
  on expiry instead of a refresh exchange.
- OAuthModal.tsx: the callback-URL manual-paste path for codex now detects
  an eyJ-prefixed pasted token and posts it to the new endpoint, mirroring
  the existing grok-cli raw-token paste pattern.

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

* chore(changelog): restore release entries + add codex token-import bullet

---------

Co-authored-by: ryanngit <74137224+ryanngit@users.noreply.github.com>
2026-07-03 00:15:34 -03:00
Diego Rodrigues de Sa e Souza
cbe2ec1244 feat(dashboard): add tool-source diagnostics settings toggle (#5978)
* feat(dashboard): add tool-source diagnostics settings toggle

Adds a Settings > Advanced card (cloned from DebugModeCard) that lets
operators flip the existing `logToolSources` flag from the UI instead
of editing the DB row directly. The backend gate (chatCore.ts) and DB
default were already present but had no toggle. Also adds
`logToolSources` to the /api/settings Zod PATCH schema (it is `.strict()`,
so the key was previously rejected) and en-only i18n strings.

Co-authored-by: DuyPrX <93126969+DuyPrX@users.noreply.github.com>
Inspired-by: https://github.com/decolua/9router/pull/1825

* chore(changelog): restore release entries + add tool-source toggle bullet

---------

Co-authored-by: DuyPrX <93126969+DuyPrX@users.noreply.github.com>
2026-07-03 00:14:32 -03:00
Diego Rodrigues de Sa e Souza
a4872ce1de feat(providers): refresh The Old LLM (Free) model catalog (#5181) 2026-07-03 00:14:28 -03:00
Diego Rodrigues de Sa e Souza
aac5ebcde5 feat(dashboard): collapse and sort provider quota rows by remaining (#5977)
* feat(dashboard): collapse and sort provider quota rows by remaining

Sort the expanded quota list by remaining percentage (highest first)
and collapse it to the first 3 rows by default, with a "Show N more" /
"Show less" toggle when a connection reports more than 3 quotas. This
keeps the most at-risk quotas out of view below a long list of
healthy ones.

Extracts the sort/slice logic into pure helpers
(sortQuotasByRemaining, getVisibleQuotas) exported from
QuotaCardExpanded.tsx and unit-tests them directly.

Co-authored-by: CườngNH <j2.cuong@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1919

* chore(changelog): restore release entries + add quota collapse/sort bullet

---------

Co-authored-by: CườngNH <j2.cuong@gmail.com>
2026-07-03 00:13:46 -03:00
Diego Rodrigues de Sa e Souza
9838acc741 feat(dashboard): suggest HuggingFace Hub media models (#5990)
* feat(dashboard): suggest HuggingFace Hub media models

MVP scope:
- imageRegistry.ts: add an image kind entry for the huggingface provider
  (HF Inference API text-to-image), with a dedicated "huggingface-image"
  format since the endpoint returns raw image bytes rather than JSON.
- New handler open-sse/handlers/imageGeneration/providers/huggingface.ts,
  wired into imageGeneration.ts's format dispatch.
- New pure helper module open-sse/services/hfModelSuggestions.ts: maps a
  dashboard media kind to an HF Hub pipeline_tag and sorts/limits raw HF
  Hub search results (unit-tested directly).
- New route GET /api/v1/providers/suggested-models proxies the public HF
  Hub models search API server-side (Zod-validated query, buildErrorBody
  on every error path, no HF token exposed client-side — this project has
  no server-side HF search token config, so it calls unauthenticated).
- UI: ImageExampleCard now fetches suggested HF Hub models for the
  huggingface provider and merges them into the model picker as a
  selectable chip row, alongside the existing static provider models list.
- i18n: adds media.suggestedModels to en.json only.

Co-authored-by: yicone <yicone@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1633

* chore(changelog): restore release entries + add hf-hub media suggest bullet

---------

Co-authored-by: yicone <yicone@gmail.com>
2026-07-03 00:12:52 -03:00
Diego Rodrigues de Sa e Souza
c9ac3057f0 feat(cli-tools): add Crush CLI tool to the dashboard (#5970)
* feat(cli-tools): add Crush CLI tool to the dashboard

Add a `crush` entry to the dashboard CLI-Tools catalog and a new
`/api/cli-tools/crush-settings` route (GET/POST/DELETE), cloned from the
`pi` tool's route as a template. OmniRoute already ships a `crush` CLI
command path (bin/cli/commands/setup-crush.mjs) but the dashboard catalog
had no matching entry.

The new route writes the real Crush config shape (providers.omniroute as
an openai-compat provider block) to the same canonical config path
(~/.config/crush/crush.json) that setup-crush.mjs's resolveCrushTarget()
already writes to, so the dashboard and the CLI command agree on one
location. Adds CLI_TOOL_RUNTIME_CONFIG.crush for detection/status, and
bumps EXPECTED_CODE_COUNT (18 -> 19) plus the catalog-count/schema tests
that enumerate the full tool list.

Co-authored-by: dopaemon <polarisdp@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1233

* chore(changelog): restore release entries + add crush cli bullet

---------

Co-authored-by: dopaemon <polarisdp@gmail.com>
2026-07-03 00:11:29 -03:00
Diego Rodrigues de Sa e Souza
b253d8a74a feat(providers): support Vercel AI Gateway embeddings and images (#5968)
* feat(providers): support Vercel AI Gateway embeddings and images

Extends the existing vercel-ai-gateway (alias vag) provider — currently
chat-only — with embeddings and image generation support, since the
gateway's OpenAI-compatible /v1 API also exposes /embeddings and
/images/generations. Adds entries to EMBEDDING_PROVIDERS
(embeddingRegistry.ts) and IMAGE_PROVIDERS (imageRegistry.ts) modeled
on the existing openai entries.

Out of scope for this PR (tracked as follow-ups): the /v1/credits
usage reader, retry:{429:2} tuning, and claude->reasoning_effort
mapping.

Co-authored-by: Ngô Tấn Tài <tantai@newnol.io.vn>
Inspired-by: https://github.com/decolua/9router/pull/1704

* chore(changelog): restore release entries + add vercel-gateway media bullet

---------

Co-authored-by: Ngô Tấn Tài <tantai@newnol.io.vn>
2026-07-03 00:09:52 -03:00
backryun
1264d1f882 fix: deepseek-web reliability — auto-refresh on 401/403, refresh v2.0.0 client headers, fix token-kind bulk import (#5988)
Integrated into release/v3.8.44 — deepseek-web auto-refresh + v2.0.0 headers + token-kind bulk import (thanks @backryun).
2026-07-03 00:09:32 -03:00
Randi
1640b8cf83 fix(cc-compatible): send SSE accept for streamed requests (#5958)
Integrated into release/v3.8.44 — SSE Accept header for streamed cc-compatible requests (thanks @rdself).
2026-07-03 00:09:25 -03:00
Diego Rodrigues de Sa e Souza
5d3f85fc71 fix(providers): only apply openai-family model inference fallback when no cataloged provider serves the id (#5852) (#5938)
resolveModelByProviderInference() in open-sse/services/model.ts had an
unconditional /^gpt-/i heuristic that hijacked any model id starting with
gpt-/o1/o3 into provider openai, even when the id is cataloged under other
providers. This broke bare (non-combo) requests for open-weight models like
gpt-oss-120b (served by fireworks/cerebras/scaleway/byteplus/sambanova/
heroku), which don't exist on openai's catalog, producing a 404 with no
fallback.

Gate the heuristic on providers.length === 0 so it only fires for genuinely
uncataloged openai-family ids, letting cataloged ids fall through to the
existing single-candidate / ambiguous-candidate resolution paths.

Regression guard: tests/unit/gptoss-provider-inference-5852.test.ts
2026-07-03 00:08:19 -03:00
Diego Rodrigues de Sa e Souza
b6a11f04df feat(api-keys): add per-key device/connection tracking (#5998)
* feat(api-keys): add per-key device/connection tracking

Tracks distinct client devices (SHA-256 fingerprint of IP + User-Agent)
seen with each API key, with a 30-minute TTL and per-key/global caps. The
tracker is in-memory only (module-scoped Map, same pattern as
sessionManager.ts — no global.* singleton) and never stores the raw IP:
it is masked before being written.

Hooked into open-sse/handlers/chatCore.ts (the real chat entry) rather
than the legacy src/sse/handlers path. New GET /api/keys/[id]/devices
management route exposes masked device details for a key, and the
API Keys dashboard tab gets a "Devices" count badge alongside the
existing Sessions badge.

This is a new granularity distinct from the existing maxSessions cap
(src/lib/db/apiKeys.ts), which limits concurrent sticky-routing sessions
rather than tracking device identity.

Co-authored-by: Muhammad Mugni Hadi <mugni@rukita.co>
Inspired-by: https://github.com/decolua/9router/pull/931

* chore(changelog): restore release entries + add api-keys device-tracking bullet

---------

Co-authored-by: Muhammad Mugni Hadi <mugni@rukita.co>
2026-07-03 00:08:17 -03:00
Diego Rodrigues de Sa e Souza
bff75cbd3c feat(proxy): add Webshare proxy pool import and sync (#5993)
* feat(proxy): add Webshare proxy pool import and sync

Adds Webshare (https://proxy.webshare.io) as a fourth source in the
free-proxy provider framework alongside 1proxy, Proxifly, and IPLocate.
WebshareProvider paginates the account's `/api/v2/proxy/list/` endpoint
(Authorization: Token <key>), upserts proxies into the shared
`free_proxies` table via the existing db/freeProxies.ts helpers, and
tombstones proxies the account no longer lists (recycled/retired IDs)
while never touching rows already promoted into the live proxy pool.

Unlike the other sources, Webshare is a paid per-account list, so it is
gated on FREE_PROXY_WEBSHARE_API_KEY rather than a plain on/off flag.
No DB migration needed — reuses the existing free_proxies table and
proxy_registry-on-promote path.

Co-authored-by: ricatix <d.enistraju155@gmail.com>
Inspired-by: https://github.com/decolua/9router/pull/1176

* chore(changelog): restore release entries + add webshare bullet

---------

Co-authored-by: ricatix <d.enistraju155@gmail.com>
2026-07-03 00:06:21 -03:00
nickwizard
4b8056b464 feat(agy): support Google Cloud project ID settings (#5905)
* feat(agy): support Antigravity project ID settings

* refactor(agy): collapse Antigravity family project gate

---------

Co-authored-by: Nikolay Alafuzov <alafuzov_nn@rusklimat.ru>
2026-07-03 00:03:55 -03:00
Diego Rodrigues de Sa e Souza
31e80a9328 test(cli): deflake cli-setup-opencode.test.ts — silence console (#5959-class landmine) (#6033)
The command under test prints CLI progress with multi-byte glyphs
(printSuccess "✔" in the happy paths, printError "✖" in the dist-missing
path that test 4 exercises) via console.log. Under the node:test runner
those child-stdout writes interleave with the V8-serialized report frames
and can corrupt the stream — the exact #5959 mechanism proven for
setup-claude.test.ts; this file's ✖ line was already visible entangled in
red CI runs. No test here asserts on stdout, so silence console.log/info/
warn for the file (same pattern as #6019/#6021, restored in after()).

Validation: pre-fix the ✖/✔ lines reach stdout every run (grep-able);
post-fix stdout is clean, 4/4 tests green, 0/20 failures across 20 runs.
2026-07-03 00:00:23 -03:00
Diego Rodrigues de Sa e Souza
3b512f4b47 test(ci): move orphaned executor tests to top-level so a runner collects them (#6027)
Integrated into release/v3.8.44 — collect orphaned executor tests (check:test-discovery base-red).
2026-07-02 23:57:07 -03:00
Diego Rodrigues de Sa e Souza
25783d1ca7 refactor(executors): extract reasoning-effort (base) + tool-normalization (codex) leaves (#6030)
Two pure-leaf follow-ups closing the Block H tail:

- base/reasoningEffort.ts: provider-aware reasoning_effort sanitation
  (MISTRAL/GITHUB reject patterns, supportsMaxEffortForProvider,
  sanitizeReasoningEffortForProvider). Deps are config/services only
  (PROVIDER_CLAUDE, isClaudeCodeCompatible, supportsClaudeMaxEffort/supportsXHighEffort)
  so the leaf never imports the host — no cycle. base.ts re-exports
  sanitizeReasoningEffortForProvider for its external importers (mimoThinking + tests).
  base.ts 1466 -> 1312 LOC.

- codex/tools.ts: Responses-API tool normalization (CODEX_HOSTED_TOOL_TYPES hosted-tool
  passthrough, isCodexFreePlan gating, normalizeCodexTools). Self-contained
  (console.debug only). codex.ts re-exports isCodexFreePlan + normalizeCodexTools for
  external importers (tests + provider services). codex.ts 1430 -> 1268 LOC.

Byte-identical bodies (verbatim: base 100/100, codex 126/126); both leaves have zero host
imports. Adds two split-guards asserting the leaf owns the symbol and both import paths
resolve to the same function. Consumer tests stay green (base-executor-sanitize-effort 34,
executor-codex 40, mimoThinking 9, codex-free-plan-image-generation 3, issue-fixes 6).
2026-07-02 23:51:52 -03:00
Diego Rodrigues de Sa e Souza
08b5e082b7 fix(cli): stabilize setup-claude.test.ts flake — inject dry-run log sink (#6021)
* fix(cli): stabilize setup-claude.test.ts flake — inject dry-run log sink (#5959)

Root cause (isolated empirically, 5/10 fail on the pristine base): the
dry-run path of syncClaudeProfilesFromModels console.log's a multi-byte
box-drawing heading ("── [dry-run] … ──"). Under the node:test runner
that write lands on the test child's stdout and corrupts the runner's
V8-serialized event stream ~50% of the time ("Unable to deserialize
cloned data due to invalid or unsupported version"), killing the file at
the first logging test. ASCII-only logging never reproduced it (0/20);
the unicode heading alone reproduced it (10/20).

Fix: syncClaudeProfilesFromModels accepts an injectable log sink
(opts.log, CLI default unchanged: console.log). The dry-run test injects
a collector — keeping unicode off the child's stdout — and gains
assertions on the dry-run report (path + parsed settings content), which
FAIL on the old code (log ignored) and PASS on the new one.

Validation: 0/30 failures post-fix vs 5/10 pre-fix on the same tree.

Baselines: complexity 2003->2006 and cognitive 859->860 are inherited
post-3a3d618fe release drift — measured identical on the pristine base
with and without this change (notes added in both files).

* test(ci): collect the orphaned tests/unit/executors/ directory (base-red unblock)

#5800 created tests/unit/executors/ outside every unit-runner brace glob,
so its 2 test files (firecrawl-fetch, xai-executor) never ran anywhere and
check:test-discovery flags them as NEW orphans on the pristine base,
red-flagging every PR into release/v3.8.44. Added 'executors' to the
runner globs in package.json (7 scripts), ci.yml unit shards, quality.yml
TIA glob, build-test-impact-map.mjs, and the test-discovery gate's
COLLECTORS (the gate enforces those stay in sync). Both files pass when
actually collected (10/10); cli+executors under suite flags: 99/99.

* chore(quality): complexity baseline 2006 -> 2007 (CI-observed value)

The GitHub fast-gates runner measures 2007 where local measures 2006 —
the same local-vs-CI off-by-one documented in the 2026-06-26 note. Pin
the CI-observed value so the gate is deterministic where it runs.

* fix(i18n): add the 6 missing en.json keys flagged by settings-i18n-keys (base-red unblock)

providers.iconUrlLabel/iconUrlHint (referenced by AddCompatibleProviderModal
and EditCompatibleNodeModal) and settings.authz.cors.wildcard.title/desc
(the #5602 CORS_ALLOW_ALL banner in AuthzSection) shipped without their
en.json messages — 'direct translation calls have English messages' fails
on the pristine release tip, red-flagging every PR. git log -S proves the
keys never existed (not a merge-eat). Scanner test: 10/10 green.
2026-07-02 23:35:35 -03:00
Diego Rodrigues de Sa e Souza
cfa3686f9f test(cli): deflake setup-claude.test.ts — silence console to stop stdout/report interleaving (#5959) (#6019)
Integrated into release/v3.8.44. Deflakes tests/unit/cli/setup-claude.test.ts (#5959) — verified in CI: setup-claude now passes in Unit Tests fast-path (2/2).

Merged with --admin over two PRE-EXISTING base-reds proven independent of this test-only change (this PR only touches setup-claude.test.ts + CHANGELOG):
- Fast Quality Gates → check:test-discovery: tests/unit/executors/{firecrawl-fetch,xai-executor}.test.ts are orphaned on release/v3.8.44 (added by #5793/#5800); the shard glob 'tests/unit/{api,...,ui}/**' omits 'executors'. Both blobs exist on the pristine base.
- Unit Tests fast-path (2/2): tests/unit/settings-i18n-keys.test.ts → 'direct translation calls have English messages' fails on the pristine base too (unrelated i18n base-red).
2026-07-02 22:45:37 -03:00
Diego Rodrigues de Sa e Souza
6756fa8e54 refactor(executors): extract challenge solver from duckduckgo-web (#6020)
Extract the DuckDuckGo anti-abuse challenge solver + FE signals (CHALLENGE_STUBS,
countHtmlElements, buildHtmlLookup, sha256Base64, solveDuckDuckGoChallenge,
makeDuckDuckGoFeSignals) verbatim into the leaf duckduckgo-web/challenge.ts. The vm
sandbox + 5s timeout (SECURITY note) are preserved. Host imports back the two it uses.

Host 924 -> 788 LOC. Byte-identical bodies (verbatim 132/132), leaf does not import the
host (no cycle). The now-dead createHash/parse5 host imports are removed; vm stays (still
used in host). Auth/cookie/warm/seed/executor untouched. Adds a split-guard; consumer
tests stay green (duckduckgo-web-executor 15, duckduckgo-domain-4037 8).
2026-07-02 22:44:47 -03:00
Diego Rodrigues de Sa e Souza
2ce64e8f38 refactor(executors): extract pure EventStream framing from kiro (#6018)
Extract the pure AWS EventStream binary framing (ByteQueue, CRC32 table + crc32,
TEXT_ENCODER/TEXT_DECODER, KIRO_VERIFY_FULL_CRC, parseEventFrame, EventFrame type)
verbatim into the self-contained leaf kiro/eventstream.ts (local JsonRecord alias to avoid
a cycle). Host imports back the 3 it uses (ByteQueue, TEXT_ENCODER, parseEventFrame).

Host 943 -> 758 LOC. Byte-identical bodies (verbatim 145/145), leaf has zero host imports
(no cycle). Auth/token-refresh/streaming-state/executor class untouched; the test-imported
flushBufferedToolArgs/resolveKiroRegion/kiroRuntimeHost stay exported on the host. Adds a
split-guard; consumer tests stay green (executor-kiro 9, kiro-tool-args-streaming 7,
kiro-iam-region 10).
2026-07-02 22:44:43 -03:00
Diego Rodrigues de Sa e Souza
0c3498e724 refactor(executors): extract pure Meta AI response parser from muse-spark-web (#6017)
Extract the pure Meta AI SSE/JSON response parsing + content/reasoning/error extraction
(parseMetaSseFrames, readMetaJsonPayloads, collect*/extract*/classify* helpers,
parseMetaAiResponseText, isRecord, the reasoning/renderer key arrays, MetaSseFrame/
ParsedMetaAiResponse types) verbatim into the leaf muse-spark-web/response-parser.ts.
Host imports back the 3 it uses; all module-private (no re-export).

Host 1301 -> 925 LOC. Byte-identical bodies (verbatim), leaf has zero imports (no cycle).
Conversation cache, cookie/auth, fetch, executor class untouched. Adds a split-guard;
consumer tests stay green (muse-spark-cookie-copy-5449 2, muse-spark-web-continuation 6).
2026-07-02 22:29:37 -03:00
Diego Rodrigues de Sa e Souza
20a67d4456 refactor(executors): extract pure JSONL stream translation from huggingchat (#6016)
Extract the pure JSONL->OpenAI-SSE translation (sseChunk, parseJsonlLine,
streamJsonlToOpenAi, readJsonlResponse) verbatim into the leaf huggingchat/jsonlStream.ts.
They consume a passed-in ReadableStream (no fetch/network/state). Host imports back the
two it uses; all module-private (no re-export).

Host 812 -> 594 LOC. Byte-identical bodies (verbatim), leaf has zero imports (no cycle).
Cookie/auth/multipart/execute untouched. Adds a split-guard; consumer tests stay green
(executor-huggingchat 6, huggingchat-model-catalog 3).
2026-07-02 22:18:12 -03:00
Diego Rodrigues de Sa e Souza
f7dea02853 feat(dashboard): wildcard-CORS runtime warning + CORS security doc (#5602) (#5759)
Integrated into release/v3.8.44 — wildcard-CORS runtime warning banner + docs/security/CORS.md security guide (#5602). Re-cut clean onto the release tip (branch was fossilized). Validated: 20+9 backend + 2 banner(vitest) tests green, typecheck:core 0, docs-sync/symbols/fabricated/doc-links pass. UNSTABLE red is the inherited environmental setup-claude base-red.
2026-07-02 22:15:02 -03:00
Diego Rodrigues de Sa e Souza
cbd08ef780 feat(api): add /v1/audio/translations endpoint (#5809)
Integrated into release/v3.8.44 — /v1/audio/translations endpoint (Whisper-style audio translation) + audioTranslation handler + translation providers in audioRegistry. Re-cut clean onto the release tip (branch was fossilized). Validated: 8 route tests (incl. no-stack-leak), typecheck:core 0, route-guard-membership OK, docs gates pass. UNSTABLE red is the inherited environmental setup-claude base-red.
2026-07-02 22:11:26 -03:00
Diego Rodrigues de Sa e Souza
a49d34bf49 feat(providers): custom icon URL for compatible provider nodes (#5815)
Integrated into release/v3.8.44 — custom icon URL for compatible provider nodes (DB migration 113 + nodes.ts + Zod schema + API routes + catalog + ProviderIcon UI). Re-cut onto the release tip (branch was fossilized ~13 real files); reconciled icon_url into the release's evolved nodes.ts/routes via 3-way. Validated: 14 backend + 5 frontend(vitest) + 24 page-utils tests green, typecheck:core 0, provider-consistency OK, file-size/env-doc-sync pass. UNSTABLE red is the inherited environmental setup-claude base-red.
2026-07-02 22:08:44 -03:00
Diego Rodrigues de Sa e Souza
0965c54245 feat(discovery): Phase 2 — reporter, /api/discovery/* routes (strict loopback-only) + dashboard UI (#5939)
* feat(discovery): Phase 2 reporter — discoveryResults DB module + service wiring

Adds src/lib/db/discoveryResults.ts (CRUD over the discovery_results table
from migration 074) and wires the opt-in discovery service to persist and read
findings through it: persistDiscoveryResult / getDiscoveryResults /
getDiscoveryResultById / markVerified / deleteDiscoveryResult, with
(provider, method, endpoint) upsert de-duplication. Re-exported from localDb.

The service stays opt-in / default-off. The /api/discovery/* routes and the
dashboard UI tab are intentionally deferred to Phase 2b — they need the
local-only enforcement model (Hard Rules #15/#17 territory) decided first.

TDD: tests/unit/db/discovery-results.test.ts (8 cases, DB + service delegation),
isolated DATA_DIR with resetDbInstance cleanup.

* feat(discovery): Phase 2b — /api/discovery/* routes (strict loopback-only)

Adds the discovery HTTP surface on top of the reporter DB module:
  GET    /api/discovery/results            list findings (optional ?providerId)
  GET    /api/discovery/results/:id        one finding (404 if absent)
  DELETE /api/discovery/results/:id        delete a finding
  POST   /api/discovery/scan               scan a provider + persist findings
  POST   /api/discovery/verify/:id         mark a finding verified

Authorization: strict loopback-only. "/api/discovery/" is added to
LOCAL_ONLY_API_PREFIXES so the central authz pipeline (proxy.ts →
runAuthzPipeline → managementPolicy) rejects non-loopback callers with a 403
LOCAL_ONLY before any handler runs. It is deliberately NOT in
LOCAL_ONLY_MANAGE_SCOPE_BYPASS_PREFIXES — no remote manage-scope bypass —
because POST /scan issues outbound probes to provider endpoints (SSRF-adjacent)
and must never be tunnel-reachable. Handlers also call requireManagementAuth
(defense in depth) and return sanitized errors via createErrorResponse.

Tests:
- tests/unit/authz/discovery-routes-local-only.test.ts (8) — security guard:
  isLocalOnlyPath true + not manage-scope-bypassable for all four paths.
- tests/unit/api/discovery-routes.test.ts (6) — handler integration over an
  isolated DATA_DIR: list/filter, by-id 200/404/400, scan persist + 400 on
  empty/malformed body, verify 200/404, delete 200/404, no stack-trace leak.

* feat(discovery): Phase 2c — dashboard UI tab (Tools → Discovery)

Adds the /dashboard/discovery page (DiscoveryPageClient) that consumes the
Phase 2b /api/discovery/* routes: scan a provider, list findings, verify or
delete them. Registered in the sidebar under the Tools group (icon
travel_explore) and given a "discovery" i18n namespace + sidebar keys in
en.json (other locales fall back to en via next-intl until synced — the
locale files are in a pre-existing coverage deficit unrelated to this change).

Registers the UI test path in vitest.config.ts (advisory ui suite).

Tests: src/app/(dashboard)/dashboard/discovery/__tests__/DiscoveryPageClient.test.tsx
(3 cases: loads+renders results, empty state, fetches /api/discovery/results on
mount; stable useTranslations mock to avoid the fetch-loop). NOTE: the ui vitest
suite cannot run in this workspace — @testing-library/dom (a @testing-library/
react peer dep) is absent from node_modules, which fails ALL existing ui tests
equally; the test runs in CI. Component verified locally via typecheck + lint.

* test(discovery): register discovery-routes-local-only in stryker tap.testFiles

The mutation-test-coverage gate (--strict) flags any unit test covering a
mutated module that isn't listed in stryker.conf.json tap.testFiles. This PR's
tests/unit/authz/discovery-routes-local-only.test.ts covers src/server/authz/
routeGuard.ts (a mutated module, which this PR edits by adding the
/api/discovery/ local-only prefix), so it must be registered for its mutant
kills to count. No behavior change.

* refactor(discovery): split DiscoveryPageClient to satisfy max-lines-per-function

The complexity ratchet (max-lines-per-function: 80) flagged the single
184-line DiscoveryPageClient function (+1 over baseline). Extract the data
layer into two hooks (useDiscoveryResults for list/loading/feedback,
useDiscoveryActions for scan/verify/delete), a shared callApi helper, and two
presentational sub-components (DiscoveryScanForm, DiscoveryResultCard). Every
function is now under the 80-line ceiling; complexity gate back to baseline
1995. No behavior change — same exported component, same endpoints, same props.

* test(sidebar): include discovery in omni-proxy item-order snapshot

Adding the Discovery item to the Tools group (this PR's sidebar entry) extends
the ordered omni-proxy section list. Update the exact-match deepEqual snapshot
in sidebar-visibility.test.ts to include "discovery" in its position (after
traffic-inspector). The assertion stays exact — this reflects the intentional
new item, it does not weaken the check.

* docs(changelog): restore release bullets eaten by merge auto-resolve; re-add discovery bullet additively

* chore(quality): bump testFrozen for translator-openai-responses-req.test.ts (1097 -> 1172)

Base-red inherited from #5933, which grew the test file to 1171 lines
(Hard Rule #18 regression tests) without adjusting the frozen cap. The
release tip itself fails check:file-size; this unblocks every PR into
release/v3.8.44. File untouched by this PR.

* chore(quality): restore stryker tap.testFiles entries eaten by merge auto-resolve

The merge of origin/release/v3.8.44 silently dropped the 3 entries added
on the release side (#5903, clinepass, #5923). Took the release version
verbatim and re-added only this PR's entry (discovery-routes-local-only)
in alphabetical order. check:mutation-test-coverage green locally.

* chore(quality): reconcile inherited v3.8.44 merge-burst drift + include discovery in tools-group order test

- complexity 1995->2003 and cognitive 856->859: both measure IDENTICAL on
  the pristine release tip (3a3d618fe) and this PR's merged HEAD — the PR
  is complexity-net-zero; drift is from the 2026-07-02 merge burst
  (notes added to both baselines, same family as prior reconciliations).
- sidebar-tools-group.test.ts: append 'discovery' to the expected
  TOOLS_GROUP order — the intentional new sidebar item this PR adds
  (same expected-value update already made in sidebar-visibility.test.ts).
2026-07-02 22:02:29 -03:00
Diego Rodrigues de Sa e Souza
9e5d2d1480 feat(xai): register XaiExecutor with reasoning-effort suffix parsing (#5800)
Integrated into release/v3.8.44 — XaiExecutor with reasoning-effort suffix parsing. Re-cut clean onto the release tip (branch was fossilized). Validated: 6 xai-executor tests green, provider-consistency OK, typecheck:core 0 errors, env-doc-sync in sync. UNSTABLE red is the inherited environmental setup-claude base-red.
2026-07-02 22:01:50 -03:00
Diego Rodrigues de Sa e Souza
babfa7b8fe feat(webfetch): support self-hosted FireCrawl instances (#5793)
Integrated into release/v3.8.44 — self-hosted FireCrawl support (FIRECRAWL_BASE_URL/FIRECRAWL_TIMEOUT_MS). Re-cut clean onto the release tip (branch was fossilized from a pre-v3.8.40 snapshot). Validated: 4 firecrawl tests green, env-doc-sync + docs-sync pass. UNSTABLE red is the inherited environmental setup-claude base-red.
2026-07-02 21:59:50 -03:00
Diego Rodrigues de Sa e Souza
fa51354e35 refactor(executors): extract pure URL normalizers from default (#6015)
Extract the pure per-provider chat-URL normalizers (normalizeBailianMessagesUrl,
normalizeDataRobotChatUrl, normalizeAzureAiChatUrl, normalizeWatsonxChatUrl,
normalizeOciChatUrl, normalizeSapChatUrl, normalizeXiaomiMimoChatUrl,
normalizeOpenAIChatUrl, getOpenRouterConnectionPreset) verbatim into the leaf
default/urlNormalizers.ts. Host imports them back into buildUrl/transformRequest; the
now-dead build*ChatUrl/normalizeBaseUrl imports move to the leaf. All module-private
(no re-export).

Host 864 -> 815 LOC (shrunk below its frozen baseline). Byte-identical bodies (verbatim
45/45), leaf does not import the host (no cycle). buildHeaders/execute/auth untouched.
Adds a split-guard; consumer tests stay green (executor-default-base 49,
anthropic-compatible-bearer 3, strip-client-metadata 3).
2026-07-02 21:57:52 -03:00
Diego Rodrigues de Sa e Souza
7bcd750d72 refactor(executors): extract pure wire protocol from perplexity-web (#6014)
Extract the pure Perplexity wire protocol (consts, SSE stream types, SSE parsing,
OpenAI<->Perplexity message translation, request/query builders, content extraction,
sseChunk) verbatim into the leaf perplexity-web/protocol.ts. Host imports back the 10
symbols it uses; everything module-private (no re-export). Session cache, TLS fetch,
auth, and the executor class stay in the host.

Host 1028 -> 534 LOC. Byte-identical bodies (verbatim), leaf imports only randomUUID
(no host import, no cycle). Adds a split-guard; consumer tests stay green
(perplexity-web 26, streaming-tools-5927 2, tls-client 6, key-validation-models 2).
2026-07-02 21:53:19 -03:00
Diego Rodrigues de Sa e Souza
3a3d618fe5 refactor(executors): extract pure upstream-header helpers from base (#6008)
Extract the pure upstream-header helpers (mergeUpstreamExtraHeaders, getCustomUserAgent,
setUserAgentHeader, applyConfiguredUserAgent, isOpenAICompatibleEndpoint,
stripStainlessHeadersForOpenAICompat) verbatim into the leaf base/headers.ts. base.ts is
imported by ~18 executors, so the host re-exports all 6 to keep those import paths intact;
it also imports the 4 it uses internally in the BaseExecutor class. The trivial JsonRecord
type alias is redefined locally in the leaf to avoid a base<->leaf cycle.

Host 1539 -> 1451 LOC. Byte-identical bodies (verbatim 78/78), leaf does not import the
host (no cycle). typecheck:core validates all base importers still resolve via the
re-export. Adds a split-guard; consumer tests stay green (executor-base-utils 22,
executor-default-base 49, executor-strip-stainless-openai-compat 6, plus executor sanity
via typecheck).
2026-07-02 20:59:36 -03:00
Diego Rodrigues de Sa e Souza
1a73dd2936 refactor(executors): extract pure payload construction from claude-web (#6006)
Extract the pure Claude-web payload types + transforms + default tools/style
(ClaudeWebRequestPayload, ClaudeWebStreamChunk, DEFAULT_CLAUDE_MODEL,
generateMessageUUIDs, getDefaultTools, getDefaultPersonalizedStyle, transformToClaude,
transformFromClaude) verbatim into the leaf claude-web/payload.ts. Host imports the 3
it uses back (ClaudeWebRequestPayload type + the two transforms).

Host 1056 -> 835 LOC. Byte-identical bodies (verbatim 149/149), leaf imports only
randomUUID (no host import, no cycle), all module-private (no re-export). Cookie/auth/
Turnstile/TLS/HTTP dispatch untouched. Adds a split-guard; consumer tests stay green
(claude-web 13, claude-web-auto-refresh 6).
2026-07-02 20:59:33 -03:00
Diego Rodrigues de Sa e Souza
de3fbd5b6a fix(registry): flag cline-pass/minimax-m3 as multimodal (supportsVision) (#6003)
The cline-pass provider's minimax-m3 entry was missing supportsVision, breaking the
LEDGER-4 registry-consistency test (all minimax-m3 entries must set supportsVision to
match lite.ts — minimax-m3 is multimodal). Every other minimax-m3 registry entry
(trae, bazaarlink, cline, ollama-cloud, ...) already sets it. This was a base-red on
release/v3.8.44 inherited by every open PR.

Validated by the existing failing-then-passing guard tests/unit/review-reviews-v3814-fixes.test.ts
(LEDGER-4).
2026-07-02 20:59:30 -03:00
Diego Rodrigues de Sa e Souza
501d4b272e fix(providers): minimax-m3 supportsVision (LEDGER-4) + stryker tap.testFiles drift (#6012)
Release-green cleanup — clears LEDGER-4 minimax-m3 supportsVision + stryker tap.testFiles drift base-reds. Validated locally.
2026-07-02 20:51:07 -03:00
AgentKiller45
a3e6868c3f feat(qoder): drive PAT auth via qodercli, add dashboard quota, fix connection display (#5816)
Integrated into release/v3.8.44 — Qoder PAT auth via qodercli binary + dashboard quota + dual-auth connection fix. Thanks @AgentKiller45 (co-author @judy459)!

Validated locally (release-green on its own merits): lint 0, typecheck:core 0, 104 qoder/usage/UI tests green, file-size gate OK (owner-approved qoderCli.ts baseline-freeze 666→989), env-doc-sync fixed (documented QODER_CLI_CONFIG_DIR).

The 2 remaining CI reds are INHERITED base-reds, not caused by this PR: (1) LEDGER-4 minimax-m3 supportsVision (minimax-m3 base + cline-pass/minimax-m3 from the already-merged #5942); (2) mutation-test-coverage missing 3 tests in stryker.conf (#5903/#5942/#5923). Both cleaned up separately.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-02 20:46:51 -03:00
Diego Rodrigues de Sa e Souza
4e6918530a refactor(api): add validatedJsonBody helper (salvage #5075) (#5931)
Fuses JSON body parsing + Zod validation into a single call that returns
either type-narrowed data or a ready-to-return 400 NextResponse with the
standard error envelope. Salvaged as the Tier 1 portable helper from the
closed refactor PR #5075; the bulk route migration is intentionally not
ported. Adds a focused 6-case regression test.

Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
2026-07-02 20:27:47 -03:00
Diego Rodrigues de Sa e Souza
17d8cb9f67 refactor(executors): extract pure stream formatters from deepseek-web (#6000)
Extract the pure content/citation formatters (isThinkingModel, isSearchModel,
cleanDeepSeekToken, formatStreamContent, DeepSeekSearchResult, appendSearchCitations)
verbatim into the leaf deepseek-web/stream-format.ts. Host imports the 5 it uses back
into transformSSE/collectSSEContent (cleanDeepSeekToken stays internal to the leaf).

Host 1147 -> 1108 LOC. Byte-identical bodies (verbatim 34/34), leaf has zero imports
(no cycle), all module-private (no re-export). PoW/auth/token-cache/HTTP dispatch
untouched. Adds a split-guard; consumer tests stay green (deepseek-web 35,
deepseek-web-rolling-window-2942 5, deepseek-web-tools-execute 3).
2026-07-02 20:12:23 -03:00
Diego Rodrigues de Sa e Souza
edb01b9cfe refactor(executors): extract pure quota parsing from codex (#5999)
Extract the pure Codex quota-snapshot parsing + reset/cooldown scheduling
(CodexQuotaSnapshot, parseCodexQuotaHeaders, getCodexResetTime,
getCodexDualWindowCooldownMs) verbatim into the leaf codex/quota.ts. Host re-exports
the 4 symbols so handlers/chatCore/codexQuota.ts + tests keep resolving.

Host 1539 -> 1427 LOC. Byte-identical bodies (verbatim 98/98), leaf has zero imports
(only Date, no cycle). WS transport, auth, HTTP dispatch untouched. Adds a split-guard;
consumer tests stay green (executor-codex 40, codex-quota-fetcher 7, chatcore-codex-quota 5).
2026-07-02 20:04:10 -03:00
Diego Rodrigues de Sa e Souza
cc570cbc6b refactor(executors): decompose grok-web into pure tool/markup leaves (#5994)
Extract the pure OpenAI<->Grok tool-translation, native-tool mapping, markup cleanup,
and NDJSON stream types out of the 1872-line grok-web executor into 4 sibling leaves:
- grok-web/types.ts: GrokStreamResponse/GrokStreamEvent (stream types)
- grok-web/tool-bridge.ts: OpenAI<->Grok tool translation + registry + classifiers
- grok-web/native-tools.ts: native-tool selection/scoring + native->OpenAI mapping
- grok-web/text-cleanup.ts: Grok markup stripping + GrokMarkupFilter

Layered, acyclic: types <- tool-bridge <- native-tools; text-cleanup <- types; host
imports the leaves. All symbols module-private (no host re-export). Host 1872 -> 887 LOC.
Byte-identical bodies (verbatim per-leaf), no cycle, all new leaves <= 800 cap
(tool-bridge split at line 753 to stay under). Auth/cookie/TLS/HTTP dispatch untouched.
Adds a split-guard; consumer tests stay green (grok-web 62, grok-cli-oauth 15,
grok-cli-strip-params 2).
2026-07-02 20:04:05 -03:00
Diego Rodrigues de Sa e Souza
273e749e6c refactor(executors): extract pure model maps + resolvers from chatgpt-web (#5967)
Extract the static model maps (MODEL_MAP, MODEL_FORCED_EFFORT, THINKING_CAPABLE_SLUGS)
and the pure thinking-effort resolvers (isThinkingCapableModel, normalizeThinkingEffort,
resolveThinkingEffort, ResolvedChatGptModel, resolveChatGptModel) verbatim into the pure
leaf chatgpt-web/models.ts. Host imports the two resolvers it uses back.

Host 3205 -> 3076 LOC. Byte-identical bodies (verbatim multiset 120/120), leaf has zero
imports (no cycle). Auth/PoW/session/HTTP dispatch and all module caches untouched.
Adds a split-guard; consumer tests stay green (chatgpt-web 86, chatgpt-web-tools-5240 4,
chatgpt-web-sha3-boringssl-5531 5).
2026-07-02 18:41:12 -03:00
Diego Rodrigues de Sa e Souza
81d640dccc refactor(executors): extract pure SSE-collect parsing from antigravity (#5962)
Extract the pure SSE-payload -> collected-stream parser (AntigravityCollectedStream,
stripZeroWidth, parseAntigravityTextualToolCall, addAntigravityTextualToolCall,
processAntigravitySSEPayload/Text, flushAntigravitySSEText) verbatim into the leaf
antigravity/sseCollect.ts. Host imports the helpers it uses and re-exports
processAntigravitySSEPayload for external importers (tests).

Host 1812 -> 1671 LOC. Byte-identical bodies (verbatim multiset 135/135), leaf does
not import the host (no cycle). Credit/quota state, auth, and HTTP dispatch untouched.
Adds a split-guard; consumer tests stay green (executor-agy 8, executor-antigravity 26,
antigravity-sse-collect-socket-release, copilot-agent-antigravity-parity 6).
2026-07-02 18:41:09 -03:00
Diego Rodrigues de Sa e Souza
4eaa2f4c65 refactor(executors): extract pure prompt + composer helpers from cursor (#5960)
Extract two pure clusters from the cursor executor into sibling leaves:
- cursor/prompt.ts: isRecordLike + toolChoiceDirectiveLine + buildCursorOutputConstraints
- cursor/composer.ts: composer thinking-as-content decoding (isComposerModel,
  visibleComposerContentFromThinking, composerReasoningRemainder + markers)

Host imports both back for internal use and re-exports the 3 composer helpers for
external importers (tests). Host 1576 -> 1451 LOC. Byte-identical bodies (verbatim
multiset prompt 65/65, composer 32/32), leaves have zero imports (no cycle). Adds a
split-guard; consumer tests stay green (cursor-composer-thinking, cursor-streaming,
cursor-agent-tool-calls, translator-openai-to-cursor, cursor-agent-system-prompt).
2026-07-02 18:41:06 -03:00
Diego Rodrigues de Sa e Souza
717069af8e refactor(translator): extract pure message helpers from openai-to-kiro (852→751) (#5947)
* refactor(translator): extract pure message helpers from openai-to-kiro

Extract the pure tool/message helpers (parseToolInput, normalizeKiroToolSchema,
serializeToolResultContent) verbatim into the leaf openai-to-kiro/messageHelpers.ts.
The host imports them back for convertMessages. They were module-private, so the
public export set is unchanged (no re-export needed).

Host 852 -> 751 LOC. Byte-identical bodies (multiset 99/99), leaf has zero imports
(no cycle). Adds a split-guard; consumer tests stay green (translator-openai-to-kiro 33,
translator-ai-sdk-image-parts 3).

* chore: re-trigger CI (stuck runner on 2/2 shard)
2026-07-02 18:41:02 -03:00
KooshaPari
b56a94ca3e feat(relay): gate bifrost auto routing by provider manifest (#5870)
Integrated into release/v3.8.44 — gates Bifrost auto-routing by the provider plugin manifest (only manifest-eligible providers reach the sidecar; ineligible/unknown fall back to the TS path with explicit reasons). Superset of #5869 (carries the full manifest + registry + docs). Resolved an integration-test conflict in favor of the release (which already subsumes this PR's readiness/removeDirWithRetry improvements). Validated locally: 4 provider-plugin-manifest + 11 relay-routing-backend tests green. Thanks @KooshaPari!

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-02 17:54:50 -03:00
Giorgos Giakoumettis
4f157e9073 feat(providers): add Claude Sonnet 5 support across the model pipeline (#5833)
Integrated into release/v3.8.44 — wires claude-sonnet-5 end-to-end (registries, modelSpecs, pricing ×3, cost, Sonnet-family fallback, 1M-ctx, static models). Reconciled the add/add overlap with the already-merged #5796 (kept the PR's superset test with the family-fallback assertion). Validated locally: kiro-sonnet-5 + catalog + pricing/modelSpecs/fallback suites all green. Thanks @ggiak!

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-02 17:52:18 -03:00
Fadhil Yusuf
61bba3e3ca fix(codex): convert chat json schema to responses text format (#5933)
Integrated into release/v3.8.44 — converts Chat Completions json_schema response_format → Responses API text.format on the Codex path, and preserves existing text.format through verbosity normalization. Base redirected main→release; the openai-responses.ts split that landed this cycle was reconciled by re-applying the delta onto openai-responses/toResponses.ts. Validated locally: 48 translator-openai-responses-req + 8 codex-verbosity tests green.

Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
2026-07-02 17:49:06 -03:00
Diego Rodrigues de Sa e Souza
01d2e1a841 feat(api): add /v1/ocr endpoint (Mistral OCR) + Mistral moderation (#5950)
Integrated into release/v3.8.44 — /v1/ocr endpoint (Mistral OCR) + Mistral moderation (port upstream 9router#2064, co-authored @waguriagentic). Validated locally: 14 ocr-route tests + moderation/servicekind/endpoint-category suites green (CORS→Zod→handler + no-stack-leak assertion). Reds are inherited DRIFT only: cognitive-complexity ratchet (none from OCR files — pre-existing cycle drift, rebaselined at release) + environmental setup-claude base-red.
2026-07-02 17:42:31 -03:00
Diego Rodrigues de Sa e Souza
26fd7b6a8a feat(providers): add ClinePass API-key provider (#5942)
Integrated into release/v3.8.44 — ClinePass API-key (BYOK) provider (port upstream 9router#2304, co-authored @adentdk). Validated locally: 16 clinepass tests green; fixed the APIKEY count 158→159 + translate-path golden snapshot (clinepass is a genuine new provider). Remaining UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path). Supersedes stub #5541.
2026-07-02 17:36:16 -03:00
Diego Rodrigues de Sa e Souza
2b0da37c19 fix(thinking): only inject redacted_thinking replay block when tool_use present and thinking enabled (#5945) (#5953)
Integrated into release/v3.8.44.
2026-07-02 17:34:17 -03:00
Diego Rodrigues de Sa e Souza
8d2df914f0 fix(resilience): honor active codex session affinity over per-request reset-aware re-scoring (#5903) (#5943)
Integrated into release/v3.8.44.
2026-07-02 17:34:11 -03:00
Diego Rodrigues de Sa e Souza
476968e290 fix(resilience): per-window is_exhausted + honor quota-exhaustion preflight for priority combos (#5923) (#5941)
Integrated into release/v3.8.44.
2026-07-02 17:34:06 -03:00
Ankit
ad9a8599e3 fix(providers): strip /v1 unconditionally to avoid /v1/v1/models fetch error (#5899) (#5920)
Integrated into release/v3.8.44 — unconditional /v1 strip in both models-discovery paths + regression test (thanks @anki1kr).
2026-07-02 17:33:45 -03:00
nickwizard
b2f77f3028 fix(api): relax provider-scoped chat completion validation (#5907)
Integrated into release/v3.8.44 — relaxed provider-scoped chat validation + regression test (thanks @nickwizard).
2026-07-02 17:33:27 -03:00
Vittor Guilherme Borges de Oliveira
11a22bb599 fix(providers): validate v0 Platform API keys via chats endpoint (#5954)
Integrated into release/v3.8.44 — v0-vercel Platform API key validation (thanks @vittoroliveira-dev).
2026-07-02 17:33:08 -03:00
Chewji
70a70e68c8 fix(mcp): auto-recover stale streamable HTTP sessions on initialize (#5957)
Integrated into release/v3.8.44 — MCP stale streamable-HTTP session auto-recovery (thanks @Chewji9875).
2026-07-02 17:32:47 -03:00
Diego Rodrigues de Sa e Souza
18cf641df4 fix(translator): strict Anthropic content-block compliance in antigravity→openai request (#5935)
Integrated into release/v3.8.44 — strict Anthropic content-block compliance in antigravity→openai (port upstream #2296). PR test green (9/9). UNSTABLE red is the pre-existing environmental setup-claude base-red (opencode-plugin dist not built in fast-path), not a regression from this PR.
2026-07-02 17:30:29 -03:00
Diego Rodrigues de Sa e Souza
058cfd4f95 fix(sse): strip ANSI/VT100 codes from gemini-cli stream frames (#5934)
Integrated into release/v3.8.44 — ReDoS-safe ANSI/VT100 strip for gemini-cli stream frames (port of upstream #2273, thanks @anki1kr). PR test green (5/5), file-size gate OK.
2026-07-02 17:29:16 -03:00
Diego Rodrigues de Sa e Souza
26dc500c16 docs(compression): document upstream sync policy for RTK/Caveman engines (#5830) (#5948)
Integrated into release/v3.8.44 — docs-only upstream sync policy for RTK/Caveman engines (closes #5830). All 7 checks green.
2026-07-02 17:27:44 -03:00
Diego Rodrigues de Sa e Souza
b6249dd374 refactor(translator): extract pure helpers from response/openai-responses (#5949)
Extract the 5 stateless helpers (normalizeToolName, stripEmptyOptionalToolArgs,
normalizeOutputIndex, normalizeUpstreamFailure, extractResponsesReasoningSummaryText)
verbatim into the pure leaf openai-responses/pureHelpers.ts (no stream state, no host
import). Host imports them back and re-exports normalizeUpstreamFailure for external
importers (tests).

Host 1091 -> 1001 LOC. The stateful streaming core stays in the host (out of scope).
Byte-identical bodies (multiset 73/73), no cycle. Adds a split-guard; consumer tests
stay green (responses-translation-fixes 37, combo-param-validation-fallback-4519 5).
2026-07-02 16:38:38 -03:00
Diego Rodrigues de Sa e Souza
283a501a1a docs(claude): add Hard Rule #22 — cross-session safety (git stash + in-flight PRs) (#5955)
Integrated into release/v3.8.44 — Hard Rule #22 (cross-session safety).
2026-07-02 16:16:19 -03:00
Diego Rodrigues de Sa e Souza
f199e40ed5 docs(changelog): record #5926 + #5944 (release-pipeline hardening) under v3.8.44 Maintenance (#5952) 2026-07-02 15:43:08 -03:00
Hamsa_M
cd81b2ab98 fix(discovery): resolve duplicate /v1 paths and redirect aborts (#5904)
Integrated into release/v3.8.44. Thanks @hamsa0x7 for diagnosing the doubled /v1 discovery path and the REDIRECT_BLOCKED probe-loop abort (#5899). De-scoped to the discovery fix (the #5903 session-affinity work is handled by #5943) and added Rule #18 regression guards.
2026-07-02 15:30:49 -03:00
Diego Rodrigues de Sa e Souza
9ba79c7a70 fix(providers): Perplexity Web emits real tool_calls in streaming mode (mirror chatgpt-web toolMode) (#5927) (#5937)
Perplexity Web (Pro/Max) only converted <tool>{...}</tool> text into
OpenAI tool_calls for non-streaming requests (hasTools && !stream).
Streaming requests -- the default for agentic coding clients -- got
the raw <tool> text as plain delta.content and never emitted a
tool_calls SSE delta, so clients could not execute tools.

Reuses the provider-agnostic buildToolModeResponse()/
toolCompletionToSseStream() helpers already shipped for chatgpt-web
(#5240): when tools are requested, buffer the full completion and
convert it into either a JSON completion or a terminal SSE replay
carrying delta.tool_calls + finish_reason: tool_calls, regardless of
the caller's stream flag. Extended buildToolModeResponse()'s idSeed
to be caller-supplied (default 'cgpt', perplexity-web passes 'pplx')
so tool_call ids stay provider-specific without duplicating the
helper. Non-tool streaming is unchanged (still lives token-by-token
via buildStreamingResponse).
2026-07-02 15:04:30 -03:00
Diego Rodrigues de Sa e Souza
16ccd5f586 chore(ci): pr-evidence FAIL output tells you to push (body edit does not re-run the gate) (#5944)
ci.yml ignores the 'edited' event, so adding the Evidence block to the PR body after a
push does not re-run check:pr-evidence — you need another commit. The FAIL report now
says so, at the exact place someone sees the red check. + 5 unit tests (classification +
hint-on-fail / no-hint-on-pass). Decided against a separate edited-triggered workflow:
pr-evidence is not a required check (no ruleset gates it; release PRs merge UNSTABLE, not
BLOCKED), so the gap is cosmetic and the generate-release skill already puts Evidence in
the body before the first push.
2026-07-02 14:53:58 -03:00
Diego Rodrigues de Sa e Souza
0d6add19dc refactor(translator): split openai-responses request translator into pure leaves (#5940)
Extract the shared pure primitives and the chat->Responses direction out of the
894-line openai-responses.ts request translator:
- openai-responses/helpers.ts: pure primitives (toRecord/toString/clampCallId/
  normalizeVerbosity/etc + markers/regexes/JsonRecord), zero host imports
- openai-responses/toResponses.ts: openaiToOpenAIResponsesRequest (chat->Responses),
  imports the helpers leaf

Host keeps openaiResponsesToOpenAIRequest (Responses->chat, imported by production)
plus both register() directions, and re-exports openaiToOpenAIResponsesRequest so
external importers (tests) keep working.

Host 894 -> 529 LOC (under the 800 cap). Verbatim bodies (multiset check: leaf A 54/54,
leaf B 294 lines, fn1 intact), public export set unchanged, leaves never import the host
(no cycle). Adds a split-guard test; all consumer tests stay green (responses-translation-fixes
37, verbosity 4, reasoning-effort 4, orphaned-tool-filter 8, empty-tool-name-loop 8,
headroom-responses-format 3).
2026-07-02 14:50:08 -03:00
Diego Rodrigues de Sa e Souza
a8d1e7bf78 chore(release): pipeline hardening — test-masking pre-flight gate + contributors/uncovered helpers (#5926)
* chore(ci): add test-masking PR-context gate to release-green pre-flight

Reproduce check:test-masking (vs origin/main) inside validate-release-green so
non-allowlisted net-assert reductions surface in the local pre-flight instead of
in a ~40-min CI layer on the release PR. run() now merges a per-gate opts.env so
GITHUB_BASE_REF reaches the child. HARD gate; skipped under --quick.

Context: v3.8.43 release cost 3 CI round-trips for PR-context gates (test-masking,
file-size, pr-evidence) that check:release-green did not reproduce locally.

* chore(release): add contributors generator + uncovered-commit reconciliation helpers

- scripts/release/gen-contributors.mjs: reproducible `### 🙌 Contributors` table for a
  CHANGELOG version (parenthetical-group parser → accurate per-PR attribution, noise-handle
  denylist). v3.8.43 shipped without the section (a real miss) because it was hand-built.
  npm run release:contributors <version> [--inject].
- scripts/release/list-uncovered-commits.mjs: lists commits since the last tag with no
  CHANGELOG bullet (v3.8.43 had 123/176 uncovered at reconciliation start). Advisory,
  maintainer-side. npm run release:uncovered.
- 20 unit tests (parenthetical attribution, noise exclusion, idempotent injection, coverage window).

* chore(quality): absorb web-cookie-providers-new file-size drift from #5928 (base-red on release/v3.8.44)
2026-07-02 14:27:31 -03:00
Diego Rodrigues de Sa e Souza
2e75ed28a4 refactor(translator): extract thinking-budget fitting from openai-to-claude (#5932)
Extract the thinking-budget fitting cluster (fitThinkingToMaxTokens +
private safeCapMaxOutputTokens + MIN_* constants) verbatim into the pure
leaf openai-to-claude/thinkingBudget.ts. Host re-exports fitThinkingToMaxTokens
so external importers keep working and imports it back for internal use.

Host 822 -> 738 LOC (under the 800 cap). No behavior change: byte-identical
bodies, public export set unchanged. Adds a split-guard test; all consumer
tests stay green (translator-openai-to-claude, strip-empty, minimax-m3, passthrough).
2026-07-02 14:15:39 -03:00
Diego Rodrigues de Sa e Souza
dfbc89f97b test(security): parse Kimi Web URL host instead of substring match (CodeQL #689) (#5928)
Alert js/incomplete-url-substring-sanitization: the Kimi Web executor
test asserted result.url.includes("www.kimi.com"), which a hostile host
like www.kimi.com.evil.net would also satisfy. Parse the URL and assert
on the exact hostname (new URL(result.url).hostname === "www.kimi.com"),
which is both a stronger check and clears the CodeQL warning.
2026-07-02 13:49:21 -03:00
Diego Rodrigues de Sa e Souza
be420afe1f chore(release): open v3.8.44 development cycle 2026-07-02 13:14:28 -03:00
Chirag Singhal
39349da18e fix(install): add pnpm-workspace.yaml allowBuilds + pnpm.json for pnpm 11+
pnpm 11 introduced ERR_PNPM_IGNORED_BUILDS for native addon packages.
Without explicit allowBuilds approval, these packages silently skip build scripts
and OmniRoute fails to start with missing native modules.

Changes:
- pnpm-workspace.yaml: Set allowBuilds=true for all 13 native addon packages
  (@parcel/watcher, @swc/core, better-sqlite3, core-js, esbuild, keytar, koffi,
  libxmljs2, onnxruntime-node, protobufjs, sharp, tls-client-node, unrs-resolver)
- pnpm.json: Migrate onlyBuiltDependencies from package.json (deprecated field)
  to the new pnpm.json config file per pnpm 11 spec.

Tested on: pnpm 11.9.0, Node 24, Windows 11.

Fixes: pnpm install ERR_PNPM_IGNORED_BUILDS on fresh clone with pnpm 11.
2026-07-02 14:15:36 +05:30
2027 changed files with 13470 additions and 144263 deletions

View File

@@ -84,14 +84,10 @@ PORT=20128
# API_HOST=0.0.0.0
# DASHBOARD_PORT=20128
# Connection backpressure: cap concurrent in-flight chat connections (503 + Retry-After when full).
# Used by: src/sse/utils/backpressure.ts — disabled when unset/0.
# OMNI_MAX_CONCURRENT_CONNECTIONS=0
# Port for the real-time WebSocket live monitoring server.
# Used by: src/server/ws/liveServer.ts, src/app/api/v1/ws/route.ts
# Default: 20132
# LIVE_WS_PORT=20132
# Default: 20129
# LIVE_WS_PORT=20129
# Bind address for the live WebSocket server.
# Default: 127.0.0.1 (loopback only). Set to 0.0.0.0 to expose on LAN —
@@ -116,14 +112,16 @@ PORT=20128
# Public URL for the live dashboard WebSocket (client-side, browser only).
# Set this when fronting the WS server with a reverse proxy or Cloudflare Tunnel.
# The browser will connect to this URL instead of ws://hostname:20132.
# The path portion of this URL (e.g. ws://localhost:20132/live-ws -> /live-ws) is also used by the dev proxy
# (scripts/dev/standalone-server-ws.mjs) and the handshake response to route
# WebSocket upgrades. Default path: /live-ws.
# Used by: src/hooks/useLiveDashboard.ts, src/app/api/v1/ws/route.ts,
# scripts/dev/standalone-server-ws.mjs, and scripts/start-ws-server.mjs.
# The browser will connect to this URL instead of ws://hostname:20129.
# The /live-ws path is already proxied from the main app (port 20128) to the
# live WS server (port 20129) by scripts/dev/standalone-server-ws.mjs.
# Used by: src/hooks/useLiveDashboard.ts
# Example: NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=wss://ws.my-ai.com/live-ws
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=ws://localhost:20132/live-ws
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=
# Disable the standalone live WebSocket helper used by scripts/start-ws-server.mjs.
# Used by: scripts/start-ws-server.mjs (CI/embedded harness toggle).
# OMNIROUTE_DISABLE_LIVE_WS=0
# Enable the real-time dashboard WebSocket server.
# Used by: src/server/ws/liveServer.ts, scripts/start-ws-server.mjs
@@ -176,12 +174,6 @@ OMNIROUTE_USE_TURBOPACK=1
# hints in production logs.
# OMNIROUTE_PROXY_FETCH_DEBUG=true
# Set to any non-empty value to emit `[omniroute completion]` diagnostics from
# the CLI shell-completion cache paths (read/refresh/write) in
# bin/cli/commands/completion.mjs. Off by default — these caches fail silently
# so a missing/corrupt cache never breaks tab-completion.
# OMNIROUTE_DEBUG_COMPLETION=1
# Docker production port mappings (docker-compose.prod.yml only).
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
# PROD_DASHBOARD_PORT=20130
@@ -195,13 +187,8 @@ OMNIROUTE_USE_TURBOPACK=1
# Hostname/bind address for the Next.js server.
# Used by: scripts/dev/run-next.mjs (HOST), Playwright runner (HOSTNAME).
# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests).
# NOTE: Do NOT use `HOSTNAME` — it is a POSIX shell variable automatically set to
# the machine name by bash/zsh. The .env loader cannot override it (first-wins
# semantics). Use OMNIROUTE_SERVER_HOST instead for `omniroute serve`.
# See: https://github.com/diegosouzapw/OmniRoute/issues/6194
# HOST=0.0.0.0
# HOSTNAME=127.0.0.1
# OMNIROUTE_SERVER_HOST=0.0.0.0
#HOST=0.0.0.0
#HOSTNAME=127.0.0.1
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
# Values: production | development | Default: production
@@ -213,17 +200,6 @@ NODE_ENV=production
# gives the correct fix instructions (podman unshare chown vs sudo chown).
CONTAINER_HOST=docker
# Container runtime override for skill sandboxing.
# Used by: src/lib/skills/sandbox.ts + src/lib/skills/containerProvider.ts
# Values: auto | docker | apple | wsl | orbstack | podman
# - auto: OS-aware auto-detect (apple/orbstack on macOS, wsl on Windows, podman on Linux)
# - apple: Apple Container (native OCI on macOS 26+)
# - wsl: WSL Container CLI (wslc.exe on Windows)
# - orbstack: OrbStack (high-perf Linux VM + docker shim on macOS)
# - podman: Podman (rootless, daemonless)
# - docker: Docker (default fallback)
SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 4. SECURITY & AUTHENTICATION
# ═══════════════════════════════════════════════════════════════════════════════
@@ -505,9 +481,7 @@ NEXT_PUBLIC_CLOUD_URL=
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
# OpenCode Go has no public quota API — this has no default and stays
# unset unless you explicitly opt in to a self-hosted/mirrored endpoint:
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
@@ -519,18 +493,6 @@ NEXT_PUBLIC_CLOUD_URL=
#OPENCODE_GO_AUTH_COOKIE=auth=...
#OMNIROUTE_OPENCODE_GO_AUTH_COOKIE=auth=...
# OpenCode Go/Zen VPS egress (#5997): on a datacenter VPS, Cloudflare in front of
# opencode.ai/zen/go 403s chat requests that lack OpenCode CLI identity headers.
# When your clients don't already send them, set this to synthesize the CLI headers
# (User-Agent, x-opencode-client, x-opencode-project, fresh request/session UUIDs) on
# absent keys. OFF by default — forward-only is safer when clients already send them.
# Values are overridable via OPENCODE_GO_USER_AGENT / OPENCODE_USER_AGENT / OPENCODE_CLIENT /
# OPENCODE_PROJECT (defaults: opencode-cli/1.0.0 / cli / default).
#OPENCODE_SYNTHESIZE_CLI_HEADERS=true
#OPENCODE_USER_AGENT=opencode-cli/1.0.0
#OPENCODE_CLIENT=cli
#OPENCODE_PROJECT=default
# Ollama Cloud quota scraping. Prefer configuring this per connection in
# Dashboard → Providers → Ollama Cloud. The cookie is sensitive.
#OLLAMA_USAGE_COOKIE=__Secure-session=...
@@ -987,7 +949,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
# Update these when providers release new CLI versions to avoid blocks.
CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
@@ -996,7 +958,7 @@ CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
# forward the original names verbatim (debugging only).
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)"
CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)"
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
@@ -1017,7 +979,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
# Override Codex client version sent in headers independently of the
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
# CODEX_CLIENT_VERSION=0.144.1
# CODEX_CLIENT_VERSION=0.142.0
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
# from the Codex Responses stream. These frames break the OpenAI SDK's
@@ -1219,7 +1181,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
APP_LOG_TO_FILE=true
# Path to the application log file.
# Default: <DATA_DIR>/logs/application/app.log (DATA_DIR defaults to ~/.omniroute)
# Default: logs/application/app.log (relative to project root / DATA_DIR)
# APP_LOG_FILE_PATH=logs/application/app.log
# Maximum single log file size before rotation.
@@ -1533,11 +1495,6 @@ APP_LOG_TO_FILE=true
# Timeout for fast-fail health checks (ms). Default: 2000
# PROXY_FAST_FAIL_TIMEOUT_MS=2000
# Time window (hours) for calculating the average latency of candidate proxies
# in the latency-optimized pool strategy. Default: 3
# Used by: src/lib/db/proxies.ts
# PROXY_LATENCY_WINDOW_HOURS=3
# Health check result cache TTL (ms). Default: 30000 (30s)
# PROXY_HEALTH_CACHE_TTL_MS=30000
@@ -1559,11 +1516,6 @@ APP_LOG_TO_FILE=true
# PROXY_AUTO_REMOVE=false
# Consecutive failures before an auto-remove fires. Default: 3.
# PROXY_AUTO_REMOVE_AFTER=3
# Let automated reachability probes (the scheduler + the "Test All" button) WRITE
# a proxy's status. Default "false": probes are read-only and never deactivate a
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
# assigned proxy; #6246). Set "true" to restore the legacy test-and-set behaviour.
# PROXY_HEALTH_AUTO_DEACTIVATE=false
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false.
@@ -1656,19 +1608,10 @@ APP_LOG_TO_FILE=true
# Used by: open-sse/utils/cursorImages.ts.
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
# Cursor state DB path override (for IDE cursor version detection).
# Cursor state DB path override (for cursor version detection).
# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically.
# CURSOR_STATE_DB_PATH=
# Cursor Agent CLI build id for AgentService/Run impersonation (YYYY.MM.DD-<hash>).
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: detect local install, else pin.
# CURSOR_AGENT_CLI_VERSION=2026.07.08-0c04a8a
# Cursor Agent CLI data directory override (versions live under <dir>/versions/).
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: ~/.local/share/cursor-agent (unix)
# or %LOCALAPPDATA%\cursor-agent (win32). Official agent CLI also honors this var.
# CURSOR_DATA_DIR=
# Direct Cursor bearer token used by scripts/ad-hoc/cursor-tap.cjs (developer tooling).
# CURSOR_TOKEN=
@@ -1755,20 +1698,6 @@ APP_LOG_TO_FILE=true
# Routing-decision log verbosity: 0 silences, higher values log more bypass/route
# decisions (src/mitm/server.cjs, _internal/bypass.cjs).
# MITM_VERBOSE=1
# Strip the leading `sudo` from MITM cert-trust commands (src/mitm/systemCommands.ts) —
# for root-less / user-namespaced deployments (e.g. rootless Docker/Podman)
# where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism).
# OMNIROUTE_NO_SUDO=0
# ── Test/CI-only guards (never needed in production) ──
# Set automatically by tests/_setup/isolateDataDir.ts and the CI workflows: the
# test suite must NEVER mutate the OS trust store (a fake test PEM installed via
# update-ca-certificates broke all system TLS on a persistent runner, 2026-07-05).
# OMNIROUTE_SKIP_SYSTEM_TRUST=1
# check-changelog-integrity.mjs (anti CHANGELOG-eat gate): explicit base ref
# override, and the justified-removal escape hatch for intentional bullet removals.
# CHANGELOG_BASE_REF=origin/release/v0.0.0
# ALLOW_CHANGELOG_REMOVALS=1
# ── 1Proxy egress pool ──
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
@@ -1850,15 +1779,6 @@ APP_LOG_TO_FILE=true
# SKILLS_SANDBOX_NETWORK_ENABLED=0
# SKILLS_ALLOWED_SANDBOX_IMAGES=
# Container runtime used by the skill sandbox. Accepted values:
# auto — pick the best installed runtime per host OS (default)
# docker — Docker Engine / Docker Desktop
# apple — Apple Container (macOS native, micro-VM)
# wsl — WSL Container (Windows native via wslc.exe)
# orbstack — OrbStack (high-perf Linux VM + docker shim on macOS)
# podman — Podman (rootless, daemonless)
# SKILLS_SANDBOX_RUNTIME=auto
# ═══════════════════════════════════════════════════════════════════════════════
# 25. TEST & E2E
# ═══════════════════════════════════════════════════════════════════════════════
@@ -1904,7 +1824,7 @@ APP_LOG_TO_FILE=true
# OMNIROUTE_TRANSLATION_API_URL=
# Bearer token for the translation backend (NEVER commit a real key here).
# OMNIROUTE_TRANSLATION_API_KEY=
# Model id, e.g. gpt-4o-mini or cx/gpt-5.6-sol.
# Model id, e.g. gpt-4o-mini or cx/gpt-5.4-mini.
# OMNIROUTE_TRANSLATION_MODEL=gpt-4o-mini
# Per-request timeout in milliseconds (default 60000).
# OMNIROUTE_TRANSLATION_TIMEOUT_MS=60000
@@ -2112,24 +2032,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
# BIFROST_API_KEY=
# BIFROST_STREAMING_ENABLED=true
# BIFROST_TIMEOUT_MS=30000
# ─────────────────────────────────────────────────────────────────────────────
# Account rotation config (operator-managed; consumed by open-sse/services/rotationConfig.ts)
# Lets a supervising front-end mirror its rotation rules onto the backend's account-fallback
# engine. All optional; defaults preserve the historical behavior.
# ─────────────────────────────────────────────────────────────────────────────
# OMNIROUTE_ROTATION_ENABLED=true
# OMNIROUTE_ROTATION_RATE_LIMIT_RESET_SECONDS=0
# OMNIROUTE_ROTATION_DISABLE_TAG_WITHOUT_RESET=true
# OMNIROUTE_ROTATE_ON_429=true
# OMNIROUTE_ROTATE_429_THRESHOLD=1
# OMNIROUTE_ROTATE_429_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_500=true
# OMNIROUTE_ROTATE_500_THRESHOLD=1
# OMNIROUTE_ROTATE_500_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_502=true
# OMNIROUTE_ROTATE_502_THRESHOLD=1
# OMNIROUTE_ROTATE_502_WINDOW_SECONDS=120
# OMNIROUTE_ROTATE_ON_400=false
# OMNIROUTE_ROTATE_400_THRESHOLD=1
# OMNIROUTE_ROTATE_400_WINDOW_SECONDS=120

View File

@@ -1,9 +0,0 @@
# Homologação E2E real — copie para .env.homolog (NUNCA commitar o real)
HOMOLOG_BASE_URL=http://192.168.0.15:20128
# Senha de management do dashboard da VPS (a mesma do /login)
HOMOLOG_ADMIN_PASSWORD=
# Deixe vazio: a suíte cria uma API key efêmera via admin e revoga no fim.
# Só preencha para depurar uma camada isolada com uma key fixa.
HOMOLOG_API_KEY=
# Tier crítico (chat real, max_tokens=5). Demais providers: só validação de catálogo.
HOMOLOG_CRITICAL_PROVIDERS=openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter

View File

@@ -24,15 +24,6 @@ updates:
update-types: ["version-update:semver-major"]
- dependency-name: "eslint-config-next"
update-types: ["version-update:semver-major"]
# typescript majors are peer-blocked by typescript-eslint, which pins a hard
# upper bound (8.64.0 → peerDependencies.typescript ">=4.8.4 <6.1.0"). A TS 7
# bump therefore violates the peer and takes down the whole toolchain at once —
# #7068 grouped it with 6 harmless bumps and turned Build + Lint + Quality Ratchet
# + Unit (6/8, 8/8) + Integration (1/2, 2/2) + dast-smoke red in one shot, blocking
# the innocuous updates riding along with it. Un-ignore once typescript-eslint
# widens the peer, and migrate TS majors intentionally (own PR, own CI run).
- dependency-name: "typescript"
update-types: ["version-update:semver-major"]
# jscpd v5 is a Rust rewrite (native binary, no Node.js programmatic API).
# scripts/check/check-duplication.mjs is deliberately pinned to jscpd@4 (it
# parses jscpd-report.json against a frozen baseline). A v5 major would break

View File

@@ -16,10 +16,6 @@ permissions:
contents: read
env:
# CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow
# test installed a fake PEM on a persistent self-hosted runner and broke all
# system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts.
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
CI_NODE_VERSION: "24"
CI_NODE_24_VERSION: "24"
CI_NODE_26_VERSION: "26"
@@ -33,23 +29,17 @@ jobs:
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
testsOnly: ${{ steps.classify.outputs.testsOnly }}
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
# Single source of truth: scripts/quality/classify-pr-changes.mjs
# (unit-tested). Push/dispatch always enable every lane.
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
@@ -60,18 +50,48 @@ jobs:
exit 0
fi
code=false
docs=false
i18n=false
workflow=false
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
while IFS= read -r file; do
case "$file" in
.github/workflows/*|.zizmor.yml)
workflow=true
code=true
;;
docs/*|*.md)
docs=true
;;
src/i18n/*|src/i18n/messages/*|scripts/i18n/*|config/i18n.json)
i18n=true
code=true
;;
src/*|open-sse/*|bin/*|electron/*|tests/*|scripts/*|package.json|package-lock.json|tsconfig*.json|next.config.*|vitest*.config.*|playwright.config.*)
code=true
;;
db/*|config/*)
code=true
;;
*)
code=true
;;
esac
done < changed-files.txt
{
echo "code=$code"
echo "docs=$docs"
echo "i18n=$i18n"
echo "workflow=$workflow"
} >> "$GITHUB_OUTPUT"
lint:
name: Lint
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Path filter: pure docs / pure message-catalog PRs skip the code lint bag + typecheck.
# Existence reason of this job is code regression; docs/i18n have dedicated jobs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
env:
# tsx gates below (known-symbols, route-guard-membership) import modules that
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
@@ -89,27 +109,7 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run audit:deps
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Single ESLint inventory (JSON) — quality-gate reuses the artifact instead of
# a second cold full-tree pass for eslintWarnings ratchet counts.
- name: ESLint (JSON report)
run: npm run lint:json
- name: Upload ESLint results
if: always()
uses: actions/upload-artifact@v7
with:
name: eslint-results
path: .artifacts/eslint-results.json
if-no-files-found: warn
retention-days: 7
- run: npm run lint
- run: npm run check:cycles
- run: npm run check:route-validation:t06
- run: npm run check:any-budget:t11
@@ -125,33 +125,21 @@ jobs:
- run: npm run check:route-guard-membership
- run: npm run check:test-discovery
- run: npm run check:tracked-artifacts
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
- name: hadolint (Dockerfile)
run: docker run --rm -i hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint --failure-threshold error - < Dockerfile
- run: npm run check:lockfile
- run: npm run check:licenses
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
- run: npm run typecheck:core
# #7033: typecheck:core's curated file allowlist does not cover
# src/app/(dashboard) TSX (and next.config.mjs sets ignoreBuildErrors:
# true, so `next build` never type-checks it either) — orphaned
# identifiers there (see #6625/#6909) were invisible to CI. This gate
# runs tsc scoped to the dashboard tree against a frozen baseline of
# pre-existing errors; only NEW errors fail it.
- run: npm run check:dashboard-typecheck
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
# it was advisory (continue-on-error) and largely subsumed by the blocking
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
# Run informationally for now — many pre-existing call sites still need
# explicit annotations; track in a dedicated follow-up.
- run: npm run typecheck:noimplicit:core
continue-on-error: true
quality-gate:
name: Quality Ratchet
runs-on: ubuntu-latest
# needs lint so eslint-results artifact is available (same inventory as the
# blocking lint step). Allow lint failure so other ratchets still run.
needs: [changes, test-coverage, lint]
needs: test-coverage
# Run even when test-coverage was SKIPPED/FAILED (e.g. a single flaky Coverage
# Shard breaks the shard→coverage→ratchet chain). The DETERMINISTIC ratchets
# (eslint / complexity / cognitive-complexity / duplication / codeql) do NOT need
@@ -160,8 +148,7 @@ jobs:
# release PR #4854, where the drift cascade only surfaced post-merge in #5029).
# The coverage.* metrics degrade gracefully: the download is continue-on-error and
# the ratchet runs with --allow-missing, so absent coverage is skipped, not failed.
# Path filter: code-only — pure docs/i18n PRs have nothing for these ratchets to guard.
if: ${{ !cancelled() && !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
if: ${{ !cancelled() }}
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
permissions:
@@ -176,15 +163,6 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
# continue-on-error: o artifact pode não existir se a job test-coverage foi
# SKIPPED (shard flaky). Nesse caso collect-metrics pula coverage.* (ausente sem
@@ -195,13 +173,6 @@ jobs:
with:
name: coverage-report
path: coverage/
# Prefer lint job's ESLint JSON (one inventory, two consumers).
- name: Download ESLint results
continue-on-error: true
uses: actions/download-artifact@v8
with:
name: eslint-results
path: .artifacts/
- run: npm run quality:collect
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
@@ -223,13 +194,15 @@ jobs:
# para não pesar no caminho crítico do lint.
- name: Duplication ratchet
run: npm run check:duplication
# Complexity + cognitive: one ESLint walk, two independent baselines (by ruleId).
- name: Complexity + cognitive ratchets
run: npm run check:complexity-ratchets
# Fase 7 INT: dead-code, type-coverage promovidos de advisory (quality-extended)
# para BLOQUEANTES aqui. cognitive-complexity is folded into the step above.
- name: Complexity ratchet
run: npm run check:complexity
# Fase 7 INT: dead-code, cognitive-complexity, type-coverage promovidos de
# advisory (quality-extended) para BLOQUEANTES aqui. Os 3 leem seus baseline
# de quality-baseline.json e saem 1 em regressão.
- name: Dead-code ratchet (knip)
run: npm run check:dead-code
- name: Cognitive complexity ratchet (sonarjs)
run: npm run check:cognitive-complexity
- name: Type coverage ratchet
run: npm run check:type-coverage
- name: Compression budget ratchet (F2.4 / N4)
@@ -267,11 +240,6 @@ jobs:
quality-extended:
name: Quality Gates (Extended)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Path filter: code-only (scanners/ratchets target production surface).
if: ${{ !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true')) }}
steps:
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
@@ -379,11 +347,6 @@ jobs:
docs-sync-strict:
name: Docs Sync (Strict)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
@@ -403,20 +366,16 @@ jobs:
run: npm run check:openapi-coverage
- name: OpenAPI security-tier consistency (advisory)
run: npm run check:openapi-security-tiers
# One FS inventory of src/app/api for both anti-hallucination directions.
- name: API docs refs (openapi + prose → routes)
run: npm run check:api-docs-refs
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
run: npm run check:openapi-routes
- name: Doc /api refs resolve to real routes (anti-hallucination)
run: npm run check:docs-symbols
- name: i18n translation drift (warn)
run: node scripts/i18n/check-translation-drift.mjs --warn
docs-lint:
name: Docs Lint (prose — advisory)
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Prose/markdown only — skip when the PR has no doc surface.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.docs == 'true') }}
# Advisory (warning-first): prose/markdown style must not block merges while the
# existing doc corpus is brought up to style. Promote to blocking once it converges.
continue-on-error: true
@@ -443,11 +402,6 @@ jobs:
i18n-ui-coverage:
name: i18n UI Coverage
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# UI keys move with dashboard code OR message catalogs.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
steps:
- uses: actions/checkout@v7
with:
@@ -459,20 +413,30 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
- run: node scripts/i18n/check-ui-keys-coverage.mjs --threshold=65
# D4 (plano mestre testes+CI): a matrix de ~40 jobs de <1min por idioma saturava sozinha
# a concorrência de jobs da conta (Free = 20 slots, compartilhados entre TODOS os repos)
# e pagava spin-up + arredondamento de billing por idioma. Um único job itera os idiomas
# (mesmo script), com grupo de log por idioma e artifact único de resultados nomeados por
# idioma (a matrix antiga subia 40 artifacts cujo result.txt colidia no merge-multiple).
i18n-matrix:
name: Build language matrix
runs-on: ubuntu-latest
outputs:
langs: ${{ steps.langs.outputs.langs }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- id: langs
run: |
LANG_DIR="src/i18n/messages"
LANGS=$(ls "$LANG_DIR"/*.json | xargs -n1 basename | sed 's/.json$//' | grep -v '^en$' | jq -R . | jq -s . | jq -c .)
echo "langs=${LANGS}" >> "$GITHUB_OUTPUT"
i18n:
name: i18n Validation (all languages)
name: i18n Validation
runs-on: ubuntu-latest
needs: changes
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
# Message-catalog / i18n-tooling only — pure code without i18n surface skips this lane.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
continue-on-error: true
strategy:
fail-fast: false
matrix:
lang: ${{ fromJson(needs.i18n-matrix.outputs.langs) }}
needs: i18n-matrix
steps:
- uses: actions/checkout@v7
with:
@@ -480,35 +444,27 @@ jobs:
- uses: actions/setup-python@v6
with:
python-version: "3.12"
- name: Validate all languages
- name: Validate ${{ matrix.lang }}
env:
# Pass the matrix value via env (never interpolate ${{ ... }} straight
# into the run: script body) so the shell receives a variable, not
# inlined text — zizmor template-injection mitigation. Named MATRIX_LANG
# to avoid clobbering the POSIX `LANG` locale variable.
MATRIX_LANG: ${{ matrix.lang }}
run: |
set -uo pipefail
mkdir -p i18n-results
FAIL=0
for f in src/i18n/messages/*.json; do
lang=$(basename "$f" .json)
[ "$lang" = "en" ] && continue
echo "::group::i18n $lang"
if python3 scripts/i18n/validate_translation.py quick -l "$lang" > "i18n-results/$lang.txt" 2>&1; then
echo "OK $lang"
else
FAIL=1
echo "FAIL $lang"
cat "i18n-results/$lang.txt"
fi
echo "::endgroup::"
done
exit "$FAIL"
- name: Upload results
python3 scripts/i18n/validate_translation.py quick -l "$MATRIX_LANG" > result.txt
- name: Upload result
if: always()
uses: actions/upload-artifact@v7
with:
name: i18n-results
path: i18n-results/
name: i18n-${{ matrix.lang }}
path: result.txt
pr-test-policy:
name: PR Test Policy
if: ${{ github.event_name == 'pull_request' && github.event.pull_request.draft == false }}
if: ${{ github.event_name == 'pull_request' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
@@ -519,7 +475,7 @@ jobs:
with:
node-version: ${{ env.CI_NODE_VERSION }}
- name: Fetch base branch
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
- name: Validate source changes include tests
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
@@ -539,17 +495,9 @@ jobs:
build:
name: Build
# Dynamic runner: when the release captain flips the USE_VPS_RUNNER repo var to
# 'true' (scripts/vps/release-runner-up.sh does it after the self-hosted VM is
# online), the heavy jobs run on the dedicated 32-core VPS runners (label
# omni-release) instead of queueing on the 20-concurrent-job hosted pool.
# Safety: fork PRs NEVER reach the self-hosted runner — the expression falls
# back to ubuntu-latest unless the PR head repo is this repository (push /
# dispatch events are own-origin by definition). Any failure path (VM down,
# var unset/false) also falls back to ubuntu-latest.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' }}
steps:
- uses: actions/checkout@v7
with:
@@ -560,21 +508,14 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# NOTE: the webpack `.build/next/cache` actions/cache step was removed with the
# Turbopack switch below — Turbopack does not read/write the webpack cache dir
# (its persistent FS cache is still experimental and intentionally NOT enabled),
# so restoring the old ~0.5 GB webpack cache would only waste download time.
# Rolling back to webpack = revert this commit (cache step comes back with it).
#
# Turbopack production build (Next 16, stable): benchmarked 1.9× faster than the
# webpack pass (9min0s vs 17min15s on a 32-core box; multi-core Rust vs webpack's
# single-threaded compile). Standalone output smoke-validated (server boots,
# /api/monitoring/health 200). Downstream jobs (e2e ×9, package-artifact,
# electron-package-smoke) consume this artifact, so a green run here validates
# the Turbopack artifact end-to-end.
- name: Cache Next.js build cache
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8
with:
path: .build/next/cache
key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ env.CI_NODE_VERSION }}-${{ hashFiles('package-lock.json') }}-
- run: npm run build
env:
OMNIROUTE_USE_TURBOPACK: "1"
- name: Archive Next.js build for downstream jobs
# Use tar so the archive preserves paths relative to CWD (.build/next/...).
# upload-artifact path-stripping is ambiguous when exclude patterns are used;
@@ -622,26 +563,12 @@ jobs:
- name: Assert dist/server.js exists
run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1)
- run: npm run check:pack-artifact
# WS1.2 (#7065 class): pack the real tarball, install it into a clean prefix and
# BOOT it to a healthy /api/monitoring/health — the gate that structure checks
# cannot provide (3 releases shipped boot-crashing tarballs with green lists).
- name: Boot-smoke the packed tarball
run: npm run check:pack-boot
electron-package-smoke:
name: Electron Package Smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
name: Electron Package Smoke
runs-on: ubuntu-latest
timeout-minutes: 25
needs: build
# WS1.5 (v3.8.49 plan): the Electron rebuild/spawn path previously executed for
# the FIRST time on the release tag — the v3.8.48 Windows bug (npx.cmd spawned
# without shell, CVE-2024-27980 behavior change) could only surface at release.
# windows-latest runs prepare:bundle (the ABI rebuild + spawn plan) per release
# PR; ubuntu keeps the full pack + headless smoke.
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
CSC_IDENTITY_AUTO_DISCOVERY: "false"
@@ -667,37 +594,18 @@ jobs:
working-directory: electron
run: npm install --no-audit --no-fund
- name: Pack Electron app
if: runner.os == 'Linux'
working-directory: electron
run: npm run pack
# ADVISORY while the new Windows leg matures (repo convention, dast-smoke
# precedent): its first-ever real run (2026-07-15, run 29457533565) died in
# 0.7s with the error swallowed by pwsh — bash shell captures stderr and
# continue-on-error keeps the heavy gate green while we harden it (#7336).
- name: Prepare Electron standalone (Windows ABI rebuild + spawn path)
if: runner.os == 'Windows'
working-directory: electron
continue-on-error: true
shell: bash
run: npm run prepare:bundle 2>&1
- name: Smoke packaged Electron app
if: runner.os == 'Linux'
env:
ELECTRON_SMOKE_TIMEOUT_MS: 60000
run: xvfb-run -a npm run electron:smoke:packaged
test-unit:
name: Unit Tests (${{ matrix.shard }}/8)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
timeout-minutes: 25
# needs: changes (not build) — this job never downloads the next-build artifact;
# gating it on Build only serialized ~20min of wall-clock for nothing. Jobs that
# DO consume the artifact (e2e, package-artifact, electron-package-smoke) keep
# needs: build. The `if` mirrors Build's own skip condition so docs-only PRs
# still skip the suite.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
runs-on: ubuntu-latest
timeout-minutes: 15
needs: build
strategy:
fail-fast: false
matrix:
@@ -716,39 +624,13 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# QW-d (plano mestre): fonte única — o MESMO npm script dos runs locais (adiciona o
# setupPolyfill que o comando inline omitia; o glob canônico vive só no package.json).
# D3 (plano mestre): a coverage é coletada NESTE mesmo run (c8/NODE_V8_COVERAGE propaga
# aos filhos através do npm) — elimina a matrix Coverage Shard ×8, que re-executava a
# suíte inteira só para medir o gate. Padrão usado pelo CI do próprio nodejs/node.
- name: Unit tests (shard ${{ matrix.shard }}/8) with V8 coverage
env:
TEST_SHARD: ${{ matrix.shard }}/8
run: |
rm -rf coverage-shard coverage-shard-report
npx c8 \
--temp-directory=coverage-shard \
--reports-dir=coverage-shard-report \
--reporter=json \
--exclude=tests/** \
--exclude=**/*.test.* \
npm run test:unit:ci:shard
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-shard-${{ matrix.shard }}
path: coverage-shard/*.json
if-no-files-found: error
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
test-vitest:
name: Vitest (MCP / autoCombo / UI components)
# Same dynamic-runner rule as Build (own-origin only; fallback ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
needs: build
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
@@ -764,37 +646,145 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
# WS5.2/5.3 (v3.8.49 plan): JUnit output feeds Trunk Flaky Tests (advisory upload
# below). node:test stays OUT of the first wave (fd1-sensitive reporter stream).
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-mcp.xml
# vitest:ui went back to 870/870 green in the v3.8.49 quality plan (WS6.1,
# PR #7127 — 69 fails triaged: matchMedia polyfill, node:testvitest migration,
# CompareTab D22 cap). Promoted to BLOCKING per the plan's post-merge step.
- run: npm run test:vitest:ui -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-ui.xml
# Trunk Flaky Tests upload — advisory (never blocks), own-origin only (fork PRs
# have no TRUNK_TOKEN). Pinned by SHA (tag v2.1.2).
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
- run: npm run test:vitest
# vitest:ui is RED today (14 fails — UI component drift accumulated while the
# suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+)
# fixes the components/tests; then drop continue-on-error to make it blocking.
- run: npm run test:vitest:ui
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: trunk-junit/**/*.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
# Node 24/26 compatibility matrices moved to .github/workflows/nightly-compat.yml
# (plano mestre testes+CI, Eixo D2 — they cost ~28% of every heavy run to catch a
# failure class that rarely originates in a PR; nightly catches it within 24h and
# the release gate can still exercise them via workflow_dispatch when needed).
node-24-compat:
name: Node 24 Compatibility Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_24_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
node-26-compat-build:
name: Node 26 Compatibility Build
runs-on: ubuntu-latest
timeout-minutes: 25
needs: build
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_26_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Next.js build cache
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8
with:
path: .build/next/cache
key: nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('src/**/*', 'open-sse/**/*', 'db/**/*', 'next.config.mjs', 'tsconfig*.json', 'postcss.config.*', 'tailwind.config.*') }}
restore-keys: |
nextjs-${{ runner.os }}-node-${{ env.CI_NODE_26_VERSION }}-${{ hashFiles('package-lock.json') }}-
- run: npm run build
node-26-compat:
name: Node 26 Compatibility Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 20
needs: node-26-compat-build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_26_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
test-coverage-shard:
name: Coverage Shard (${{ matrix.shard }}/8)
runs-on: ubuntu-latest
timeout-minutes: 25
needs: build
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4, 5, 6, 7, 8]
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Run c8 over shard ${{ matrix.shard }}/8
run: |
rm -rf coverage-shard coverage-shard-report
# `--temp-directory` (writable via NODE_V8_COVERAGE) is what the merge
# job reads with `c8 report --temp-directory ...`. Using `--output-dir`
# only produces the final json *report* and leaves the raw v8 files in
# `coverage/tmp`, so uploading `coverage-shard/` was empty. Pin the temp
# dir so the raw coverage files live there and the artifact upload picks
# them up regardless of `--test-force-exit` timing.
npx c8 \
--temp-directory=coverage-shard \
--reports-dir=coverage-shard-report \
--reporter=json \
--exclude=tests/** \
--exclude=**/*.test.* \
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
- name: Upload raw shard coverage
if: always()
uses: actions/upload-artifact@v7
with:
name: coverage-shard-${{ matrix.shard }}
path: coverage-shard/*.json
if-no-files-found: error
test-coverage:
name: Coverage
runs-on: ubuntu-latest
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
timeout-minutes: 20
needs: test-unit
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
timeout-minutes: 10
needs: test-coverage-shard
if: ${{ !cancelled() && needs.test-coverage-shard.result == 'success' }}
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
@@ -842,7 +832,6 @@ jobs:
--merge-async \
--reporter=text-summary \
--reporter=json-summary \
--reporter=lcov \
--exclude=tests/** \
--exclude=**/*.test.* \
--check-coverage \
@@ -864,18 +853,6 @@ jobs:
> coverage/coverage-report.md
fi
cat coverage/coverage-report.md >> "$GITHUB_STEP_SUMMARY"
# WS5.6 (D7, v3.8.49 plan): patch coverage on the PR diff via Codecov —
# informational during calibration (codecov.yml sets informational: true);
# promote to blocking only after ~2 weeks without false blocks. The lcov
# reporter above also fixes coverage/lcov.info being silently absent
# (if-no-files-found: warn) — Sonar consumes the same file.
- name: Upload coverage to Codecov (informational)
if: always()
uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5
with:
files: coverage/lcov.info
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: false
- name: Upload coverage artifacts
if: always()
uses: actions/upload-artifact@v7
@@ -900,14 +877,10 @@ jobs:
with:
persist-credentials: false
fetch-depth: 0
# The upload strips the common `coverage/` prefix, so the artifact root holds
# lcov.info directly — download into coverage/ so it lands at coverage/lcov.info,
# where sonar.javascript.lcov.reportPaths expects it (path: . left the Sonar
# new-code coverage at 0% every scan).
- uses: actions/download-artifact@v8
with:
name: coverage-report
path: coverage/
path: .
- name: Explain SonarQube skip
if: ${{ github.event_name != 'pull_request' || env.SONAR_TOKEN == '' || env.SONAR_HOST_URL == '' }}
run: |
@@ -942,7 +915,7 @@ jobs:
coverage-pr-comment:
name: PR Coverage Comment
runs-on: ubuntu-latest
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.draft == false && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }}
if: ${{ !cancelled() && github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false && needs.changes.outputs.code == 'true' }}
needs:
- changes
- pr-test-policy
@@ -1026,12 +999,7 @@ jobs:
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
# Heavy shard target: ≤20min (was ~40min). Timeout 45min to cover slow runners.
timeout-minutes: 45
needs: [build, changes]
# WS3.1 hotfix fast-lane: the 9-shard E2E matrix is the CI critical path (~25min).
# It skips for (a) PRs labeled `hotfix` (entry policy in docs/ops/RELEASE_CHECKLIST.md:
# production-broken only, full-suite evidence from the previous green run linked in the
# PR) and (b) tests-only diffs outside tests/e2e/ (cannot change the served app).
if: ${{ needs.changes.outputs.testsOnly != 'true' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
needs: build
strategy:
fail-fast: false
matrix:
@@ -1052,7 +1020,7 @@ jobs:
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- name: Cache Playwright browsers
uses: actions/cache@v6
uses: actions/cache@v6.0.0
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
@@ -1066,41 +1034,13 @@ jobs:
- name: Extract Next.js build artifact
run: |
tar -xzf /tmp/e2e-build.tar.gz
# WS4.1: duration-balanced shards (LPT over config/quality/e2e-timings.json).
# Measured skew of plain --shard was 14× (24m47s vs 1m47s) — E2E was the CI
# critical path. The balancer self-verifies completeness and exits non-zero on
# any inconsistency, falling back to plain --shard (never fewer specs).
- name: Run E2E tests (duration-balanced shard)
env:
SHARD: ${{ matrix.shard }}
PLAYWRIGHT_JUNIT_OUTPUT_NAME: junit-e2e-results.xml
run: |
if FILES=$(node scripts/quality/balance-e2e-shards.mjs "$SHARD" 9); then
if [ -z "$FILES" ]; then echo "[e2e-balance] shard $SHARD has no files"; exit 0; fi
echo "[e2e-balance] shard $SHARD runs:"; echo "$FILES"
# shellcheck disable=SC2086 — FILES is our own newline-separated path list
npx playwright test $(echo "$FILES" | tr '\n' ' ') --reporter=line,junit
else
echo "[e2e-balance] balancer unavailable — plain --shard fallback"
npx playwright test tests/e2e/*.spec.ts --shard="$SHARD"/9 --reporter=line,junit
fi
# WS5.2/5.3: Trunk Flaky Tests upload — advisory, own-origin only, SHA-pinned.
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: junit-e2e-results.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9
test-integration:
name: Integration Tests (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
timeout-minutes: 15
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
needs: build
strategy:
fail-fast: false
matrix:
@@ -1121,15 +1061,12 @@ jobs:
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
# (tsx/esm = QW-b; o alinhamento de ESCOPO do integration com o npm script fica p/ follow-up)
- run: node --import tsx/esm --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
- run: node --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=1 --test-shard=${{ matrix.shard }}/2 tests/integration/*.test.ts
test-security:
name: Security Tests
runs-on: ubuntu-latest
# needs: changes (not build) — no artifact consumed; see test-unit note.
needs: changes
if: ${{ github.event_name != 'pull_request' || (needs.changes.outputs.code == 'true' && github.event.pull_request.draft == false) }}
needs: build
env:
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-test-api-key-secret-long
@@ -1161,6 +1098,9 @@ jobs:
- package-artifact
- electron-package-smoke
- test-unit
- node-24-compat
- node-26-compat-build
- node-26-compat
- test-coverage
- sonarqube
- coverage-pr-comment
@@ -1210,12 +1150,15 @@ jobs:
echo "| Build Matrix | $(status '${{ needs.build.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Package Artifact | $(status '${{ needs.package-artifact.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Electron Package Smoke | $(status '${{ needs.electron-package-smoke.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility Build | $(status '${{ needs.node-26-compat-build.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "## 🧪 Tests" >> "$GITHUB_STEP_SUMMARY"
echo "| Suite | Status |" >> "$GITHUB_STEP_SUMMARY"
echo "|-------|--------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Unit | $(status '${{ needs.test-unit.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 24 Compatibility Tests | $(status '${{ needs.node-24-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Node 26 Compatibility Tests | $(status '${{ needs.node-26-compat.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| Coverage | $(status '${{ needs.test-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| PR Coverage Comment | $(status '${{ needs.coverage-pr-comment.result }}') |" >> "$GITHUB_STEP_SUMMARY"
echo "| E2E | $(status '${{ needs.test-e2e.result }}') |" >> "$GITHUB_STEP_SUMMARY"

View File

@@ -22,10 +22,10 @@ jobs:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
- uses: github/codeql-action/init@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
languages: javascript-typescript
queries: security-extended
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
- uses: github/codeql-action/analyze@8aad20d150bbac5944a9f9d289da16a4b0d87c1e # v4.36.2
with:
category: "/language:javascript-typescript"

View File

@@ -10,10 +10,7 @@ jobs:
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
continue-on-error: true
# Build CLI bundle alone varies 6-11min on GitHub-hosted runners (3 consecutive
# timeouts observed on 2026-07-14 with the old 12min cap killing schemathesis
# mid-run) — 25min leaves real headroom for the actual DAST steps.
timeout-minutes: 25
timeout-minutes: 12
env:
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa

View File

@@ -94,7 +94,7 @@ jobs:
cache: npm
- name: Cache node_modules
uses: actions/cache@v6.1.0
uses: actions/cache@v6.0.0
with:
path: node_modules
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
@@ -201,12 +201,6 @@ jobs:
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
done
fi
# electron-updater manifests (latest.yml / latest-mac.yml / latest-linux.yml)
# must be published alongside the installers, or autoUpdater fails with
# "Cannot find latest.yml in the latest release artifacts" (#6766).
for file in latest*.yml; do
[ -f "$file" ] && cp "$file" ../../release-assets/
done
- name: Upload artifacts
uses: actions/upload-artifact@v7
@@ -269,7 +263,6 @@ jobs:
release-assets/*.AppImage
release-assets/*.deb
release-assets/*.blockmap
release-assets/*.yml
release-assets/*.source.tar.gz
release-assets/*.source.zip
env:

View File

@@ -1,126 +0,0 @@
name: Nightly Node Compat
# Plano mestre testes+CI (Eixo D2, aprovado 2026-07-04): as matrizes de compatibilidade
# Node 24/26 custavam ~28% de CADA run do CI pesado (2 execuções completas da suíte por
# sync da release-PR) para pegar uma classe de quebra que raramente nasce num PR típico.
# Elas rodam aqui 1×/dia contra o tip da release ativa (mesmo alvo do nightly-release-green)
# e continuam obrigatórias no gate de release via workflow_dispatch do ci.yml se preciso.
# fail-fast desligado: numa quebra queremos saber TODAS as versões afetadas de uma vez.
on:
schedule:
- cron: "47 6 * * *" # 06:47 UTC diário — slot distinto dos demais nightlies
workflow_dispatch:
inputs:
branch:
description: "Branch to validate (default: highest release/vX.Y.Z)"
required: false
type: string
permissions:
contents: read
issues: write
concurrency:
group: nightly-compat
cancel-in-progress: true
jobs:
resolve-branch:
name: Resolve active release branch
runs-on: ubuntu-latest
outputs:
target: ${{ steps.branch.outputs.target }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
persist-credentials: false
- name: Resolve active release branch
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
else
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
| sed 's#origin/##' \
| sort -t/ -k2 -V \
| tail -1)
fi
case "$TARGET" in
release/v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "Refusing non-canonical branch name: $TARGET"; exit 1 ;;
esac
echo "target=$TARGET" >> "$GITHUB_OUTPUT"
compat-build-26:
name: Node 26 Compatibility Build
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "26"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run build
compat-tests:
name: Node ${{ matrix.node }} Compat Tests (${{ matrix.shard }}/4)
runs-on: ubuntu-latest
timeout-minutes: 25
needs: resolve-branch
strategy:
fail-fast: false
matrix:
node: [24, 26]
shard: [1, 2, 3, 4]
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
TEST_SHARD: ${{ matrix.shard }}/4
steps:
- uses: actions/checkout@v7
with:
ref: ${{ needs.resolve-branch.outputs.target }}
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ matrix.node }}
cache: npm
- uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime
- run: npm run test:unit:ci:shard
report:
name: Open / update tracking issue on failure
runs-on: ubuntu-latest
if: ${{ !cancelled() && (needs.compat-tests.result == 'failure' || needs.compat-build-26.result == 'failure') }}
needs: [resolve-branch, compat-build-26, compat-tests]
permissions:
issues: write
steps:
- name: Open or update issue
env:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ needs.resolve-branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
TITLE="🌙 nightly-compat: Node 24/26 failures on $TARGET"
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open --search "$TITLE in:title" --json number --jq '.[0].number')
BODY="Nightly Node-compat run failed on \`$TARGET\`: $RUN_URL — triage which Node version/shard broke (fail-fast off, all versions reported)."
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body "$BODY"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body "$BODY"
fi

View File

@@ -113,7 +113,7 @@ jobs:
cache: npm
- run: npm ci
- name: Restore Stryker incremental cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
uses: actions/cache@2c8a9bd7457de244a408f35966fab2fb45fda9c8 # v6.0.0
with:
path: reports/mutation/stryker-incremental.json
key: stryker-incremental-${{ matrix.batch.name }}-${{ github.run_id }}

View File

@@ -1,18 +1,11 @@
name: Release-Green (continuous)
name: Nightly Release-Green
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
#
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
# accrue silently on release/** and explode — in layers — at release time. This
# workflow reproduces the release-equivalent validation on the release branch and,
# when there are HARD failures, opens/updates a single tracking issue.
#
# WS5.1 (v3.8.49 quality plan) — two modes:
# push to release/v* (code paths) → --quick (fast HARD gates, ~5-8min). Catches the
# captain's direct pushes (sync-back — the one ungated write path) AND the merged
# COMBINATION right after every PR merge, attributing the offending push range in
# the issue. Base-red MTTD drops from ≤24h to ≤~15min after the offending push.
# schedule (3×/day) → full --with-build --full-ci (the deep sweep incl. build+suites).
# nightly reproduces the release-equivalent validation on the active release branch
# HEAD and, when there are HARD failures, opens/updates a single tracking issue.
#
# It is NOT a required status check and never touches a contributor PR — it only
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
@@ -21,23 +14,8 @@ name: Release-Green (continuous)
# package-artifact) flip the issue open.
on:
push:
branches: ["release/v*", "main"]
paths:
- "src/**"
- "open-sse/**"
- "bin/**"
- "electron/**"
- "scripts/**"
- "tests/**"
- "config/**"
- "package.json"
- "package-lock.json"
- "tsconfig*.json"
schedule:
- cron: "23 5 * * *" # full sweep — off-peak, distinct from other nightlies
- cron: "23 12 * * *" # full sweep — midday (WS5.1: 3×/day instead of 1×)
- cron: "23 18 * * *" # full sweep — evening
- cron: "23 5 * * *" # 05:23 UTC daily — off-peak, distinct from other nightlies
workflow_dispatch:
inputs:
branch:
@@ -50,25 +28,13 @@ permissions:
issues: write
concurrency:
# push storms during merge campaigns collapse to the newest commit per branch;
# scheduled full sweeps keep their own single lane.
group: release-green-${{ github.event_name }}-${{ github.ref }}
group: nightly-release-green
cancel-in-progress: true
env:
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
jobs:
release-green:
name: Validate active release branch
# On a push, only run for release/* pushes — a push to main is handled by the
# main-green job below. Schedule/dispatch always run (they validate the highest release).
if: ${{ github.event_name != 'push' || startsWith(github.ref_name, 'release/') }}
# Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight)
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
# no local noauth CLIs => zero machine-specific false positives) and no contention.
# Nightly cron normally finds the var false (VM off) and falls back to hosted.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
@@ -83,15 +49,10 @@ jobs:
id: branch
env:
INPUT_BRANCH: ${{ github.event.inputs.branch }}
EVENT_NAME: ${{ github.event_name }}
PUSHED_REF: ${{ github.ref_name }}
run: |
set -euo pipefail
if [ -n "${INPUT_BRANCH:-}" ]; then
TARGET="$INPUT_BRANCH"
elif [ "$EVENT_NAME" = "push" ]; then
# validate exactly what was pushed, not the highest branch
TARGET="$PUSHED_REF"
else
# highest release/vX.Y.Z by semver among remote branches
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
@@ -125,26 +86,9 @@ jobs:
- name: Release-green validation (full)
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set +e
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
# operator env; hosted ignores the unknown flag before #6300 lands).
# push → --quick: fast HARD gates only (~5-8min), per-merge signal.
# schedule/dispatch → --with-build --full-ci: ALSO run every static gate from
# ci.yml's gate jobs (lint, quality-gate, quality-extended, docs-sync-strict,
# pr-test-policy) + build + full suites. PRs into release/** only get the
# fast-gates, so these accrue silently and explode in layers on the release PR
# (v3.8.46: 11 static base-reds leaked).
if [ "$EVENT_NAME" = "push" ]; then
MODE="--quick"
else
MODE="--with-build --full-ci"
fi
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
node scripts/quality/validate-release-green.mjs --json --with-build \
1> release-green.json 2> release-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
@@ -156,28 +100,15 @@ jobs:
GH_TOKEN: ${{ github.token }}
TARGET: ${{ steps.branch.outputs.target }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT_NAME: ${{ github.event_name }}
BEFORE_SHA: ${{ github.event.before }}
AFTER_SHA: ${{ github.event.after }}
run: |
set -euo pipefail
TITLE="🔴 Release branch not green: ${TARGET}"
{
echo "The **release-green** validation found HARD failures on \`${TARGET}\`."
echo "The nightly **release-green** validation found HARD failures on \`${TARGET}\`."
echo "These are real defects that would block the release PR — fix them in the"
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
echo ""
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
# WS5.1 attribution: on push events the offending change IS this push's range
# (one merge per push in the normal queue), so name it — no bisect needed.
if [ "$EVENT_NAME" = "push" ] && [ -n "${BEFORE_SHA:-}" ] && \
git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
echo ""
echo "**Offending push range** (\`${BEFORE_SHA:0:9}..${AFTER_SHA:0:9}\`):"
echo '```'
git log --no-decorate --oneline "${BEFORE_SHA}..${AFTER_SHA}" | head -20
echo '```'
fi
echo "**Run:** ${RUN_URL}"
echo ""
echo '```'
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
@@ -197,107 +128,10 @@ jobs:
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: release-green-report
path: |
release-green.json
release-green.log
if-no-files-found: ignore
# Companion arm for `main`. Under the parallel-cycle model, main only receives merged
# work at the release squash — so a gate/infra fix that lands only on release leaves
# main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines)
# turn EVERY PR into main red on a check unrelated to its diff. This detects that and
# opens a "🔴 main not green" tracking issue. The PREVENTION is the companion-PR reflex
# (Hard Rule #21 area / _shared/merge-gates.md §8); this is the automated backstop.
main-green:
name: Validate main branch
# On a push, only run for a push to main — a push to release/* is handled by
# release-green above. Schedule/dispatch always run (they also sweep main).
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
env:
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-nightly-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@v7
with:
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: "24"
cache: npm
- uses: ./.github/actions/npm-ci-retry
- name: Main-green validation
id: validate
env:
EVENT_NAME: ${{ github.event_name }}
run: |
set +e
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
if [ "$EVENT_NAME" = "push" ]; then
MODE="--quick"
else
MODE="--with-build --full-ci"
fi
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
# shellcheck disable=SC2086 — MODE is an intentional flag list
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
1> main-green.json 2> main-green.log
echo "exit=$?" >> "$GITHUB_OUTPUT"
echo "------- report -------"
cat main-green.log
- name: Open / update tracking issue on HARD failure
if: steps.validate.outputs.exit != '0'
env:
GH_TOKEN: ${{ github.token }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
EVENT_NAME: ${{ github.event_name }}
run: |
set -euo pipefail
TITLE="🔴 main branch not green"
{
echo "The **main-green** validation found HARD failures on \`main\`."
echo ""
echo "Because \`main\` only receives merged work at the release squash, a gate/infra"
echo "fix that landed only on the release branch leaves \`main\` broken for the whole"
echo "cycle — and repo-wide gates (CodeQL alert count, ratchet baselines) then turn"
echo "**every open PR into main** red on a check unrelated to its diff. The fix is a"
echo "companion PR \`--base main\` carrying the release-side fix (see"
echo "\`_shared/merge-gates.md\` §8), NOT chasing each contributor PR."
echo ""
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
echo ""
echo '```'
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
echo '```'
echo ""
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
} > issue-body.md
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
if [ -n "$EXISTING" ]; then
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
echo "Updated existing issue #$EXISTING"
else
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
fi
- name: Upload report artifact
if: always()
uses: actions/upload-artifact@v7
with:
name: main-green-report
path: |
main-green.json
main-green.log
if-no-files-found: ignore

View File

@@ -100,7 +100,7 @@ jobs:
cache: npm
- run: npm ci
- name: Cache Playwright browsers
uses: actions/cache@v6.1.0
uses: actions/cache@v6.0.0
with:
path: ~/.cache/ms-playwright
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}

View File

@@ -22,14 +22,6 @@ on:
- latest
- next
- historic
publish_mode:
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
required: false
default: "staged"
type: choice
options:
- staged
- direct
workflow_call:
inputs:
version:
@@ -174,34 +166,8 @@ jobs:
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
# real tarball and fails the publish before anything reaches the registry.
- name: Boot-smoke the tarball before ANY publish
- name: Publish to npm
if: steps.resolve.outputs.skip != 'true'
run: npm run check:pack-boot
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
# parks the exact bytes on the registry WITHOUT making them installable; the
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
# human gate to AFTER the proof instead of before it. Requires npm >= 11.15
# (staged publishing GA 2026-05-22). publish_mode=direct is the emergency
# fallback (legacy immediate publish) via workflow_dispatch.
- name: Ensure npm supports staged publishing
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
run: |
set -euo pipefail
CUR=$(npm --version)
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
# Pinned exact version (supply-chain: never float @latest in the publish
# job); bump deliberately when a newer npm is required.
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
npm install -g --ignore-scripts npm@11.15.0
fi
npm --version
- name: Publish to npm (staged — owner approves with 2FA)
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
env:
VERSION: ${{ steps.resolve.outputs.version }}
TAG: ${{ steps.resolve.outputs.tag }}
@@ -209,32 +175,10 @@ jobs:
run: |
set -euo pipefail
# Always pass --tag explicitly. Defense in depth: even if VERSION is
# accidentally an older release, the historic tag will NOT claim `@latest`.
npm stage publish --provenance --access public --tag "$TAG"
{
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
echo ""
echo "The exact bytes are parked on the registry. To release them:"
echo '```'
echo "npm stage list omniroute # find the stage id"
echo "npm stage approve <id> # owner 2FA — THE publish"
echo '```'
echo "To verify the staged bytes first: npm stage download <id> → run"
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
echo "To discard: npm stage reject <id>."
} >> "$GITHUB_STEP_SUMMARY"
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
- name: Publish to npm (DIRECT — emergency fallback)
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
env:
VERSION: ${{ steps.resolve.outputs.version }}
TAG: ${{ steps.resolve.outputs.tag }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
set -euo pipefail
# accidentally an older release, `npm publish --tag historic` will
# NOT promote it to `@latest`.
npm publish --provenance --access public --tag "$TAG"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
- name: Publish to GitHub Packages
if: steps.resolve.outputs.skip != 'true'

View File

@@ -14,80 +14,12 @@ permissions:
contents: read
env:
# CI must never mutate the runner's OS trust store (2026-07-05: a cert-flow
# test installed a fake PEM on a persistent self-hosted runner and broke all
# system TLS). Belt-and-suspenders with tests/_setup/isolateDataDir.ts.
OMNIROUTE_SKIP_SYSTEM_TRUST: "1"
CI_NODE_VERSION: "24"
jobs:
# Same classifier as ci.yml (scripts/quality/classify-pr-changes.mjs) so PR→release
# path filters share existence reasons: code / docs / i18n / workflow.
changes:
name: Change Classification
runs-on: ubuntu-latest
outputs:
code: ${{ steps.classify.outputs.code }}
docs: ${{ steps.classify.outputs.docs }}
i18n: ${{ steps.classify.outputs.i18n }}
workflow: ${{ steps.classify.outputs.workflow }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
- id: classify
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
{
echo "code=true"
echo "docs=true"
echo "i18n=true"
echo "workflow=true"
} >> "$GITHUB_OUTPUT"
exit 0
fi
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
docs-gates:
name: Docs Gates (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
- run: npm run check:api-docs-refs
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
fast-gates:
name: Fast Quality Gates
needs: changes
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
# tsx gates (known-symbols, route-guard-membership) import modules that open
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
env:
@@ -104,18 +36,12 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- run: npm run check:provider-consistency
- run: npm run check:fetch-targets
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
- run: npm run check:openapi-routes
- run: npm run check:docs-symbols
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
run: npm run check:docs-all
- run: npm run check:deps
- run: npm run check:file-size
- run: npm run check:error-helper
@@ -138,39 +64,25 @@ jobs:
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
# the release PR's heavy Package Artifact job.
- run: npm run check:pack-policy
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
- run: npm run check:complexity-ratchets
# Complexity + cognitive-complexity ratchets on the fast-path (PR→release) so
# cycle drift is rebaselined PER-PR instead of cascading onto the release PR's
# Quality Ratchet (v3.8.36: +30 complexity / +15 cognitive surfaced only post-merge).
- run: npm run check:complexity
- run: npm run check:cognitive-complexity
- name: Typecheck (core)
run: npm run typecheck:core
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
- name: Typecheck (dashboard)
run: npm run check:dashboard-typecheck
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
# Promote to the blocking gate after ~1 week of parity with the step above.
- name: Typecheck (core) — TS7 native shadow (advisory)
continue-on-error: true
run: |
RC=0
START=$(date +%s)
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
exit $RC
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
# selector returns __RUN_ALL__ — full-suite authority is the parallel
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
# unit tests impacted by this PR's changed files. Fail-safe runs the FULL
# unit suite on hub/unmapped changes — TIA accelerates, never replaces, the net.
#
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
# coverage remains required via `Unit Tests fast-path` (fast-unit).
- name: Impacted unit tests (TIA subset; blocking)
# BLOCKING (flipped 2026-06-17). The pre-existing release unit test-debt that kept
# this advisory was cleared: #4030 (16 Zod/registry reds, lossless restore) and
# #4063 (the last red — the LiveWS boot test — root-caused as a real event-loop
# stall in the WS sidecar, fixed + relocated to the integration suite). A full
# ci.yml run on release/v3.8.28 then showed all 8 unit shards green, so PR->release
# now blocks on unit-test regressions in the impacted set (typecheck:core already
# blocked above). Fail-safe still runs the FULL unit suite on hub/unmapped changes.
- name: Impacted unit tests (TIA, fail-safe full; blocking)
env:
GITHUB_BASE_REF: ${{ github.base_ref }}
run: |
@@ -184,39 +96,15 @@ jobs:
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
# run race-free regardless of concurrency.
if echo "$SEL" | grep -q "__RUN_ALL__"; then
echo "Fail-safe: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
exit 0
echo "Fail-safe: running FULL unit suite (CI concurrency)"; npm run test:unit:ci; exit $?
fi
echo "Running impacted tests:"; echo "$SEL"
mapfile -t FILES <<< "$SEL"
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
# under `--import tsx` (CJS transform — required for ESM-only deep imports like
# @lobehub/icons/es/* reached via lobeProviderIcons.ts); everything else under
# `--import tsx/esm`. A single tsx/esm invocation false-reds every dashboard
# module-shape test the impact map selects ("Unexpected token 'export'").
DASH=(); REST=()
for f in "${FILES[@]}"; do
case "$f" in
tests/unit/dashboard/*) DASH+=("$f") ;;
*) REST+=("$f") ;;
esac
done
RC=0
if [ ${#REST[@]} -gt 0 ]; then
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${REST[@]}" || RC=$?
fi
if [ ${#DASH[@]} -gt 0 ]; then
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
fi
exit $RC
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}"
fast-vitest:
name: Vitest (fast-path)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
runs-on: ubuntu-latest
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -230,33 +118,15 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
# which is where flaky-detection volume actually comes from (ci.yml's heavy
# jobs only run on the release PR). Advisory upload, own-origin only.
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
- name: Upload test results to Trunk (advisory)
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
continue-on-error: true
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
with:
junit-paths: trunk-junit/**/*.xml
org-slug: omniroute
token: ${{ secrets.TRUNK_TOKEN }}
- run: npm run test:vitest
fast-unit:
name: Unit Tests fast-path (${{ matrix.shard }}/4)
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
# runner box). Node's native --test-shard=N/total takes any denominator — only
# this matrix and the TEST_SHARD env below encode the shard count.
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
name: Unit Tests fast-path (${{ matrix.shard }}/2)
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shard: [1, 2, 3, 4]
shard: [1, 2]
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
@@ -270,85 +140,9 @@ jobs:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
- run: npm run test:unit:ci:shard
env:
TEST_SHARD: ${{ matrix.shard }}/4
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
# config/quality/eslint-suppressions.json congela as violações EXISTENTES por
# arquivo+regra; qualquer warning NOVO aparece e o --max-warnings 0 falha o job — o
# drift de +41/+88 warnings por ciclo passa a morrer no PR que o introduz, em vez de
# ser rebaselinado às cegas na release. Aperto do baseline (na reconciliação da
# release): npx eslint . --prune-suppressions --suppressions-location config/quality/eslint-suppressions.json
#
# Princípio Zero: bloqueante SÓ para branches internas (as campanhas/sessões são a
# origem do drift). PR de FORK roda em modo report (continue-on-error → o job fica
# verde com anotação; a campanha /green-prs aplica o fix via co-autoria — o
# contribuidor NUNCA é bloqueado nem cobrado).
lint-guard:
name: No new ESLint warnings
needs: changes
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: actions/setup-node@v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: Restore ESLint file cache
uses: actions/cache@v6
with:
path: |
.eslintcache
.eslintcache-complexity
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
restore-keys: |
eslint-${{ runner.os }}-
- name: ESLint (baseline congelado — warning novo = vermelho)
# lint:json writes the report; --max-warnings 0 keeps no-new-warnings policy.
run: npm run lint:json -- --max-warnings 0
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
# bullets vizinhos/seções inteiras (incidente #6193, 2026-07-05: 212 linhas /
# 130 bullets); o checkout de PR é refs/pull/N/merge, então comparar contra a
# base detecta o eat ANTES do merge. (2) SKILL.md gerado stale vs o catálogo de
# agent-skills (#6186 mergeou um id de catálogo sem rodar o gerador → 8 reds de
# integration invisíveis até a release).
#
# Princípio Zero: bloqueante SÓ para branches internas; PR de FORK roda em modo
# report (continue-on-error) — a campanha corrige via co-autoria, o contribuidor
# nunca é bloqueado.
merge-integrity:
name: Merge integrity (changelog + generated skills)
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
runs-on: ubuntu-latest
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
env:
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
API_KEY_SECRET: ci-lint-api-key-secret-long
DISABLE_SQLITE_AUTO_BACKUP: "true"
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
with:
node-version: ${{ env.CI_NODE_VERSION }}
cache: npm
- run: npm ci
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
run: npm run check:changelog-integrity
- name: Agent-skills generator sync (SKILL.md gerado ≡ catálogo)
run: npm run check:agent-skills-sync
- run: >
node --max-old-space-size=4096 --import tsx
--import ./tests/_setup/isolateDataDir.ts
--test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2
tests/unit/*.test.ts
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"

17
.gitignore vendored
View File

@@ -72,7 +72,6 @@ yarn-error.log*
# env files (can opt-in for committing if needed)
.env*
!.env.example
!.env.homolog.example
# Provider API keys (never commit)
*.api-key
.nvidia-api-key
@@ -233,19 +232,3 @@ omniroute.md
# mise configuration
mise.toml
_artifacts/
.claude-flow/
# ESLint file cache (npm run lint --cache / complexity ratchets)
.eslintcache
.eslintcache-complexity
# CI/local quality artifacts (eslint-results.json, etc.)
.artifacts/
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
.env.homolog
tests/homolog/.auth/
tests/homolog/ui/.auth/
homolog-report/

View File

@@ -74,16 +74,3 @@
# '''tests/unit/''',
# ]
#
[[rules]]
# Falsos-positivos comprovados do generic-api-key — zerados em 2026-07-13 (WS6/D3,
# plano v3.8.49). Revisar em v3.9.0. Nenhum é credencial: dois são NOMES DE CAMPO
# de métricas de latência; o terceiro é o valor PÚBLICO de um beta header da API
# da Anthropic (documentado publicamente, não é segredo).
id = "generic-api-key"
[rules.allowlist]
description = "Field names + public Anthropic beta-header value (não são segredos)"
regexes = [
'''latencyP\d{2}Ms''',
'''interleaved-thinking-2025-05-14''',
]

View File

@@ -1,9 +1,7 @@
#!/usr/bin/env sh
# .husky/pre-push — intentionally light.
# any-budget + tracked-artifacts already run on pre-commit; re-running them on
# every push only doubles local wall time for the same existence reason (CI still
# enforces both). Keep this hook as a PATH/npm sanity check + reminder.
# Intentionally excludes test:unit / typecheck (slow; covered by CI).
# .husky/pre-push — fast deterministic gates (<10s total)
# Intentionally excludes test:unit (slow; covered by CI pre-push remote run).
# Activated: 2026-06-13 (6A.12 — replaced commented-out test:unit stub)
if ! command -v npm >/dev/null 2>&1; then
echo "⚠️ npm not found in PATH — skipping pre-push hooks"
@@ -11,5 +9,4 @@ if ! command -v npm >/dev/null 2>&1; then
exit 0
fi
# No-op success: real local gates live in pre-commit; CI owns the rest.
exit 0
npm run check:any-budget:t11 && npm run check:tracked-artifacts

View File

@@ -1,55 +0,0 @@
# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan.
#
# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE
# identity. The manual merge-train validated batches by hand; this queue automates
# it with batching + automatic batch bisection (a red batch of N costs ~log2(N)
# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo.
#
# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's
# pre-merge ⭐ gate):
# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a
# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label
# IS the merge approval; Mergify only executes it.
# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs
# targeting the frozen branch — the freeze is a human-honored coordination signal
# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21).
# • Never label a PR another session is actively working (Hard Rule #22b).
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
queue_rules:
- name: release
# Any current or future release branch — the reason GitHub's native queue was
# rejected (no wildcard support on personal-account repos).
queue_conditions:
- base~=^release/v\d+\.\d+\.\d+$
- label=queue
- -draft
- -conflict
# "Everything that ran is green, nothing still running, AND the always-on
# anchor check succeeded" — robust to the path-filtered fast-gates (docs-only
# PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with
# zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY
# non-draft PR (quality.yml) and must be an affirmative success. Review approval
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
# approval in this repo's single-maintainer model (see governance header).
merge_conditions:
- "#check-failure=0"
- "#check-pending=0"
- "#check-success>=1"
- check-success=Merge integrity (changelog + generated skills)
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
# don't hold a lone PR hostage waiting for siblings.
batch_size: 10
batch_max_wait_time: 5 min
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
merge_method: squash
pull_request_rules:
- name: clean up the queue label after merge
conditions:
- merged
actions:
label:
remove:
- queue

View File

@@ -1,5 +1,2 @@
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
docs/reference/ENVIRONMENT.md
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
open-sse/config/freeModelCatalog.data.ts

10
.vscode/settings.json vendored
View File

@@ -48,19 +48,11 @@
"**/.build",
"**/dist",
"**/coverage",
"**/.worktrees",
"**/.claude/worktrees",
"**/electron",
"**/_references",
"**/_mono_repo",
"**/_tasks"
"**/.worktrees"
]
},
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
// (MANTIDO comentado — o dono precisa ver _references/_mono_repo/_tasks na árvore.
// A performance é resolvida por watcherExclude + search.exclude + tsserver, sem
// precisar escondê-los do Explorer.)
// "files.exclude": {
// "**/.worktrees": true,
// "**/coverage": true,

View File

@@ -23,7 +23,7 @@
"scripts": {
"build": "tsup",
"clean": "rm -rf dist",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts",
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
"prepublishOnly": "npm run clean && npm run build && npm test"
},
"keywords": [

View File

@@ -238,26 +238,9 @@ function trimLeadingDashes(value: string): string {
*/
export function resolveOmniRoutePluginOptions(
opts?: OmniRoutePluginOptions
): Required<
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">
> & {
/**
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
* `providerId` above is auto-prefixed with "opencode-" ONLY to satisfy OC
* 1.17.8+'s native-adapter gate ({openai, anthropic, opencode*}) — that
* prefixed value is OC-internal and must be used ONLY for AuthHook.provider
* and provider-registration keys (the OC config-hook top-level
* `provider.<id>` block). `omnirouteProviderId` MUST be used everywhere an
* identifier reaches or represents something OmniRoute's own server parses
* (model `id` prefix, `ModelV2.providerID`, combo catalog keys in the
* dynamic provider hook) — OmniRoute's `parseModel()` has no alias for
* "opencode-<x>", so a prefixed id there is unrecoverable and credential
* lookup fails with "No credentials for opencode-<x>".
*/
omnirouteProviderId: string;
} & Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
): Required<Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">> &
Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
const rawProviderId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
const omnirouteProviderId = trimLeadingOpencodePrefix(rawProviderId);
// OC 1.17.8+ native-adapter gate rejects providerID not in
// {openai, anthropic, opencode*}. Silently prefix so existing
// configs (providerId: "omniroute") keep working.
@@ -275,7 +258,6 @@ export function resolveOmniRoutePluginOptions(
: DEFAULT_MODEL_CACHE_TTL_MS;
return {
providerId,
omnirouteProviderId,
displayName,
modelCacheTtl,
baseURL: opts?.baseURL,
@@ -283,18 +265,6 @@ export function resolveOmniRoutePluginOptions(
};
}
/**
* Strip a leading "opencode-" prefix (added only for the OC native-adapter
* gate — see `resolveOmniRoutePluginOptions`) so the returned id is safe to
* embed in anything OmniRoute's own server parses. A user-supplied
* `providerId: "opencode-omniroute"` (already prefixed) resolves to the same
* unprefixed "omniroute" as the default, matching `providerId`'s own
* idempotent-prefix handling above.
*/
function trimLeadingOpencodePrefix(rawProviderId: string): string {
return rawProviderId.startsWith("opencode-") ? rawProviderId.slice("opencode-".length) : rawProviderId;
}
/**
* Strict parse of raw plugin options (as received from opencode.json or a
* direct factory call) into the validated `OmniRoutePluginOptions` shape.
@@ -2691,8 +2661,7 @@ export function createOmniRouteProviderHook(
if (canonicalDedup.has(entry.id)) continue;
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
const model = mapRawModelToModelV2(entry, {
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerId: resolved.omnirouteProviderId,
providerId: resolved.providerId,
baseURL,
apiFormat: resolved.features?.apiFormat,
});
@@ -2857,8 +2826,7 @@ export function createOmniRouteProviderHook(
const mapped = mapComboToModelV2(
combo,
memberEntries,
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
resolved.omnirouteProviderId,
resolved.providerId,
baseURL,
features.apiFormat
);
@@ -2877,8 +2845,7 @@ export function createOmniRouteProviderHook(
}
}
// #6859: server-facing key — NOT the OC-gate-prefixed `resolved.providerId`.
const comboKey = buildComboKey(combo, usedComboKeys, resolved.omnirouteProviderId);
const comboKey = buildComboKey(combo, usedComboKeys, resolved.providerId);
// Collision policy: combos win. Warn ONCE per (cacheKey, comboKey)
// when overwriting a same-key raw model so the operator can spot
@@ -2980,8 +2947,7 @@ export function createOmniRouteProviderHook(
},
status: "active",
release_date: "",
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
providerID: resolved.omnirouteProviderId,
providerID: resolved.providerId,
options: {},
headers: {},
};

View File

@@ -447,14 +447,14 @@ test("models() returns combo entries merged into the map", async () => {
// 3 raw models + 1 combo = 4 entries
assert.equal(Object.keys(out).length, 4);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["omniroute/gemini-3-flash"]);
assert.ok(out["omniroute/claude-tier"]);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
assert.ok(out["opencode-omniroute/claude-tier"]);
const combo = out["omniroute/claude-tier"];
const combo = out["opencode-omniroute/claude-tier"];
assert.equal(combo.name, "Claude Tier");
assert.equal(combo.providerID, "omniroute");
assert.equal(combo.providerID, "opencode-omniroute");
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
assert.equal(combo.limit.context, 100_000);
assert.equal(combo.capabilities.reasoning, false);
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["omniroute/phantom-combo"]);
assert.ok(out["opencode-omniroute/phantom-combo"]);
// With zero resolvable members, LCD = all-false (defensive posture).
assert.equal(out["omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["omniroute/phantom-combo"].limit.context, 0);
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
});
test("models(): hidden combos are excluded from the map", async () => {
@@ -505,8 +505,8 @@ test("models(): hidden combos are excluded from the map", async () => {
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.ok(out["omniroute/visible"]);
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
assert.ok(out["opencode-omniroute/visible"]);
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
});
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
@@ -530,8 +530,8 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
});
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
assert.ok(out["omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
// No collision warning fires — dedup makes keys disjoint.
const collisionWarns = warnings.filter((w) => {
@@ -565,8 +565,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
// First combo gets the bare slug; second gets disambiguated.
assert.ok(out["omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
});
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
@@ -583,8 +583,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
// Catalog includes the models but NOT any combo entries.
assert.equal(Object.keys(out).length, 2);
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["omniroute/claude-secondary"]);
assert.ok(out["opencode-omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-secondary"]);
// Soft-fail warning surfaced.
const softFail = warnings.find((w) => {
@@ -609,7 +609,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
assert.ok(second["omniroute/claude-tier"]);
assert.ok(second["opencode-omniroute/claude-tier"]);
});
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
@@ -701,7 +701,7 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
{ fetcher: modelsFetcher, combosFetcher }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
const masterLight = out["omniroute/master-light"];
const masterLight = out["opencode-omniroute/master-light"];
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
assert.equal(
masterLight.limit.context,

View File

@@ -376,8 +376,7 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "enrichment fetcher called once");
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId.
const m = out["omniroute/claude-sonnet-4-6"];
const m = out["opencode-omniroute/claude-sonnet-4-6"];
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
assert.equal(m.cost.input, 3, "enrichment pricing applied");
assert.equal(m.cost.output, 15);
@@ -403,7 +402,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
assert.equal(
out["omniroute/claude-sonnet-4-6"].name,
out["opencode-omniroute/claude-sonnet-4-6"].name,
"claude-sonnet-4-6",
"raw id preserved"
);
@@ -464,7 +463,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
);
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
assert.equal(called, 1, "compression metadata fetcher called");
const combo = out["omniroute/claude-primary"];
const combo = out["opencode-omniroute/claude-primary"];
assert.ok(combo, "combo entry present");
assert.match(
combo.name,

View File

@@ -1,99 +0,0 @@
/**
* Regression test for #6859.
*
* `resolveOmniRoutePluginOptions()` auto-prefixes `providerId` with
* `"opencode-"` (commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate
* accepts it as an OC-registered provider id. That prefixed value must stay
* OC-internal (AuthHook.provider / provider registration keys) — it must
* NEVER leak into the identifiers OmniRoute's own server parses to resolve
* credentials (`mapRawModelToModelV2`'s `id`/`providerID`,
* `mapComboToModelV2`'s `providerID`, and the dynamic-hook catalog keys).
*
* OmniRoute's server-side `parseModel()` (open-sse/services/model.ts) splits
* a dispatched model string on `/` to recover the provider name and look up
* credentials. If the plugin embeds the OC-gate-prefixed id in that string,
* the server looks up credentials for a provider named "opencode-omniroute"
* (which never exists in `src/shared/constants/providers.ts`) instead of
* "omniroute" — producing the exact "No credentials for opencode-omniroute" /
* "No active credentials for provider: opencode-omniroute" errors reported
* in #6859.
*/
import test from "node:test";
import assert from "node:assert/strict";
import {
createOmniRouteProviderHook,
mapRawModelToModelV2,
resolveOmniRoutePluginOptions,
} from "../src/index.js";
/**
* Minimal stand-in for OmniRoute's own `parseModel()` (open-sse/services/
* model.ts), which splits a dispatched `<providerID>/<modelID>` string on the
* FIRST "/" to recover the provider name used for credential lookup. Kept
* local (rather than cross-importing the real module) so this package's
* self-contained test suite (`cd @omniroute/opencode-plugin && npm test`)
* doesn't depend on the root repo's `@/*` path-alias resolution.
*/
function splitProviderFromDispatchedModel(modelStr: string): string {
const idx = modelStr.indexOf("/");
return idx === -1 ? modelStr : modelStr.slice(0, idx);
}
const apiAuth = (key: string) => ({ type: "api" as const, key });
test("#6859: server-facing model id/providerID must resolve to the unprefixed provider name", () => {
const resolved = resolveOmniRoutePluginOptions();
// The OC-gate-compatible id stays prefixed — it is legitimate for
// AuthHook.provider / provider registration.
assert.equal(resolved.providerId, "opencode-omniroute");
// A second, unprefixed id must be exposed for anything that reaches
// OmniRoute's own server (model id prefix, ModelV2.providerID, combo keys).
assert.equal(
resolved.omnirouteProviderId,
"omniroute",
"resolveOmniRoutePluginOptions() must expose an unprefixed omnirouteProviderId"
);
// A bare raw /v1/models entry (no existing "/" in its id — the common
// case for OmniRoute's catalog) mapped with the server-facing id.
const model = mapRawModelToModelV2(
{ id: "claude-opus-4-7" },
{ providerId: resolved.omnirouteProviderId, baseURL: "http://localhost:20128" }
);
assert.equal(model.providerID, "omniroute");
assert.equal(model.id, "omniroute/claude-opus-4-7");
// OpenCode dispatches back to OmniRoute using `providerID/modelKey`
// (matches the issue's own repro: `-m opencode-omniroute/oc/big-pickle`).
const dispatchedModelString = `${model.providerID}/claude-opus-4-7`;
const parsedProvider = splitProviderFromDispatchedModel(dispatchedModelString);
assert.equal(
parsedProvider,
"omniroute",
`server-side provider split resolved '${parsedProvider}', expected 'omniroute' — ` +
`credentials lookup would fail for an OC-gate-prefixed provider id`
);
});
test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID never carry the OC-gate prefix", async () => {
const hook = createOmniRouteProviderHook(
{ baseURL: "https://or.example.com/v1" },
{
fetcher: async () => [{ id: "claude-opus-4-7" }],
combosFetcher: async () => [],
}
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-test") as never });
const model = out["omniroute/claude-opus-4-7"];
assert.ok(model, "catalog keyed under the unprefixed provider name");
assert.equal(model.providerID, "omniroute");
assert.ok(
!model.providerID.startsWith("opencode-"),
"the OC-gate prefix must never leak into ModelV2.providerID"
);
});

View File

@@ -101,10 +101,7 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
assert.equal(fetcher.callCount(), 1);
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
assert.equal(Object.keys(out).length, 3);
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
// that prefix must never leak into anything OmniRoute's server parses.
assert.ok(out["omniroute/claude-primary"]);
assert.ok(out["opencode-omniroute/claude-primary"]);
});
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
@@ -155,17 +152,13 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
{ fetcher, combosFetcher: async () => [] }
);
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
// #6859: dynamic-hook catalog keys/ids/providerID use the unprefixed
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
// anything OmniRoute's own server parses for credential lookup.
const claude = out["omniroute/claude-primary"];
const claude = out["opencode-omniroute/claude-primary"];
assert.ok(claude, "claude-primary present");
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
// static-catalog reader resolves `(providerID, modelID)` from the key.
assert.equal(claude.id, "omniroute/claude-primary");
assert.equal(claude.id, "opencode-omniroute/claude-primary");
assert.equal(claude.name, "claude-primary");
assert.equal(claude.providerID, "omniroute");
assert.equal(claude.providerID, "opencode-omniroute");
assert.equal(claude.api.id, "openai-compatible");
assert.equal(claude.api.url, "https://or.example.com/v1");
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");

View File

@@ -3,12 +3,12 @@
## Project
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
with **250 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
> **Live counts (v3.8.47)**: providers 250 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> **Live counts (v3.8.43)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
@@ -539,7 +539,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
| Auto-Combo (12-factor, 18 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |

File diff suppressed because it is too large Load Diff

View File

@@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
## Project at a Glance
**OmniRoute** — unified AI proxy/router. One endpoint, 250 LLM providers, auto-fallback.
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
| Layer | Location | Purpose |
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
**Combo routing** (`open-sse/services/combo.ts`): 17 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
---
@@ -221,7 +221,7 @@ connection continue serving other models.
- **2 spaces**, semicolons, double quotes, 100 char width, es5 trailing commas (enforced by lint-staged via Prettier)
- **Imports**: external → internal (`@/`, `@omniroute/open-sse`) → relative
- **Naming**: files=camelCase/kebab, components=PascalCase, constants=UPPER_SNAKE
- **ESLint**: `no-eval`, `no-implied-eval`, `no-new-func` = error everywhere; `no-explicit-any` = **error** in `open-sse/` and `tests/` (since #6218 — pre-existing violations are frozen in `config/quality/eslint-suppressions.json`, new ones must be fixed; `npm run lint` applies the suppressions and is what CI runs)
- **ESLint**: `no-eval`, `no-implied-eval`, `no-new-func` = error everywhere; `no-explicit-any` = warn in `open-sse/` and `tests/`
- **TypeScript**: `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types.
### Database
@@ -332,7 +332,7 @@ For any non-trivial change, read the matching deep-dive first:
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
| Architecture | `docs/architecture/ARCHITECTURE.md` |
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
| Auto-Combo (12-factor scoring, 18 strategies) | `docs/routing/AUTO-COMBO.md` |
| Auto-Combo (12-factor scoring, 17 strategies) | `docs/routing/AUTO-COMBO.md` |
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
| Skills framework | `docs/frameworks/SKILLS.md` |
@@ -428,10 +428,8 @@ git push -u origin feat/your-feature
**Husky hooks**:
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11` + `check:tracked-artifacts`
- **pre-push**: intentionally light (PATH/npm sanity only). `any-budget` + `tracked-artifacts`
already run on pre-commit; re-running them on every push was pure double-pay. CI still
enforces both. (Was Fase 6A.12 full pre-push gate; folded into pre-commit in #6716.)
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
- **pre-push**: fast deterministic gates (`check:any-budget:t11` + `check:tracked-artifacts`); intentionally excludes `test:unit` (slow — covered by the CI `test-unit` job). Activated 2026-06-13 (Quality Gates Fase 6A.12).
### Worktree isolation (MANDATORY for every development task)
@@ -542,10 +540,10 @@ the stale-enforcement added in Fase 6A.3.
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-prs`, `/review-group-prs`, `/merge-prs`, `/triage-fix-bugs`, `/implement-fix-bugs`, `/triage-features`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
21. **Release-freeze — the release branch is frozen to campaign merges while a `/generate-release` is running.** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a) and closes it once the release PR squash-merges to `main`. Before merging **any** PR into the active `release/vX.Y.Z` branch, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active, **HOLD the merge** (leave the PR ready and open; do NOT merge to the release branch), tell the operator, and resume once the freeze lifts. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create _this_ session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create *this* session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
---

View File

@@ -341,7 +341,7 @@ Write unit tests in `tests/unit/` covering at minimum:
- [ ] Error responses route through `buildErrorBody()` / `sanitizeErrorMessage()` — no raw stack traces in response bodies (see [`docs/security/ERROR_SANITIZATION.md`](./docs/security/ERROR_SANITIZATION.md))
- [ ] Shell commands (`exec` / `spawn`) pass runtime values via `env`, not via string interpolation
- [ ] All inputs validated with Zod schemas
- [ ] Changelog **fragment** added under `changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md` for user-facing changes (see [`changelog.d/README.md`](./changelog.d/README.md)) — do **not** edit `CHANGELOG.md` directly; fragments are aggregated at release time and never conflict between PRs
- [ ] CHANGELOG updated (if user-facing change)
- [ ] Documentation updated (if applicable)
- [ ] No new CodeQL / Secret-Scanning alerts opened, or each one dismissed with technical justification referencing the relevant `docs/security/` doc
- [ ] Routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) classified as `isLocalOnlyPath()` in `src/server/authz/routeGuard.ts` — see [Hard Rule #15](docs/security/ROUTE_GUARD_TIERS.md)

View File

@@ -8,8 +8,8 @@ WORKDIR /app
# that already have a fix published in trixie. CVEs without an upstream fix yet
# (local-only TOCTOU, etc.) remain until the distro patches them and the image
# is rebuilt; none are reachable from the proxy's request surface at runtime.
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
@@ -29,8 +29,8 @@ FROM base AS builder
# Build tools for native module compilation
# apt-get update needed here because base's rm -rf clears the shared cache
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=shared \
RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
--mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
apt-get update \
&& apt-get install -y --no-install-recommends python3 make g++ \
&& rm -rf /var/lib/apt/lists/*
@@ -55,51 +55,32 @@ ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
# are reproducible.
RUN test -f package-lock.json \
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
# `npm rebuild <pkg>` re-runs the package's own install script, so under npm 11 +
# `--ignore-scripts` on the parent `npm ci` it depends on npm's script-allowlist
# machinery correctly re-enabling that one package's script. Some self-hosted build
# environments (e.g. Dokploy) hit a broken/incomplete better-sqlite3 native binding
# from that indirection. Invoking `node-gyp rebuild` directly inside the package
# directory bypasses npm's script-running layer entirely and is deterministic
# regardless of npm version or ignore-scripts allowlist behavior.
# node-gyp comes from npm's own bundled copy (deterministic, already in the image)
# instead of `npx --yes`, which would install an arbitrary registry version
# on-demand and run its lifecycle scripts (Sonar docker:S6505).
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
RUN --mount=type=cache,target=/root/.npm \
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
&& (cd node_modules/better-sqlite3 \
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
&& npm rebuild better-sqlite3 \
&& node -e "require('better-sqlite3')(':memory:').close()"
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era
# TurbopackInternalError panic ("entered unreachable code: there must be a path to a
# root" in ImportTracer::get_traces) no longer reproduces on Next 16.2.9 — validated
# 2026-07-05 with clean amd64 (12min14s, image smoke-tested: /api/monitoring/health
# 200) and arm64 (qemu, exit 0, zero panic strings) builds. Turbopack cut the bare
# build from 17min to 9min on the same 32-core box. Webpack stays available as the
# escape hatch: `--build-arg`/-e OMNIROUTE_USE_TURBOPACK=0.
# Build with webpack (stable). Turbopack hit a non-recoverable internal panic on this
# Next.js version during the v3.8.27 release build — TurbopackInternalError "entered
# unreachable code: there must be a path to a root" in ImportTracer::get_traces, on both
# linux/amd64 and linux/arm64. Webpack is the proven engine (build:release / VPS / CI Build
# all green). Re-enable Turbopack (=1) once the upstream tracer bug is fixed.
# See docs/ops/QUALITY_GATE_PLAYBOOK.md Parte 6.
ENV OMNIROUTE_USE_TURBOPACK=1
# Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert
# access), so keep @/mitm/manager on the graceful stub (#3390). This flag is
# Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344).
ENV OMNIROUTE_MITM_STUB=1
ENV OMNIROUTE_USE_TURBOPACK=0
# Raise the V8 heap ceiling for the build. The webpack production optimization
# pass needs more than V8's default ceiling (~2 GB) for a codebase this size; a
# memory-constrained Docker build otherwise dies with "FATAL ERROR: ... JavaScript
# heap out of memory" during the builder stage (#4076). Turbopack's compile is
# native (Rust) and less V8-heap-bound, but the prerender/export phase still runs
# on V8, so keep the ceiling. NODE_OPTIONS propagates to the spawned `next build`
# child (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env).
# Build-only; the runtime heap is set separately on the runner stage
# (OMNIROUTE_MEMORY_MB). Override: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
# pass (forced above since Turbopack panics) needs more than V8's default ceiling
# (~2 GB) for a codebase this size; a memory-constrained Docker build otherwise
# dies with "FATAL ERROR: ... JavaScript heap out of memory" during the builder
# stage (#4076). NODE_OPTIONS propagates to the spawned `next build` child
# (build-next-isolated.mjs → resolveNextBuildEnv spreads process.env). Build-only;
# the runtime heap is set separately on the runner stage (OMNIROUTE_MEMORY_MB).
# Override for hosts with more/less RAM: `--build-arg OMNIROUTE_BUILD_MEMORY_MB=6144`.
ARG OMNIROUTE_BUILD_MEMORY_MB=4096
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
COPY . ./
RUN --mount=type=cache,id=next-cache,target=/app/.build/next/cache \
RUN --mount=type=cache,target=/app/.build/next/cache \
mkdir -p /app/data && npm run build
# ── Runner base ────────────────────────────────────────────────────────────
@@ -114,12 +95,6 @@ LABEL org.opencontainers.image.title="omniroute" \
ENV NODE_ENV=production
ENV PORT=20128
ENV HOSTNAME=0.0.0.0
# Runtime heap ceiling. 1024MB is enough for normal traffic but can be tight
# for large fusion-combo panels (many models fanned out in parallel, each
# response buffered in full — see open-sse/services/fusion.ts::FUSION_DEFAULTS
# .maxPanel, issue #1905). Override at `docker run` time with
# `-e OMNIROUTE_MEMORY_MB=2048` (or higher) if you raise fusionTuning.maxPanel
# above the default cap.
ENV OMNIROUTE_MEMORY_MB=1024
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
@@ -201,8 +176,8 @@ COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
# browsers land under /home/node which persists across image layers and is
# accessible to the non-root runtime user.
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update \
&& node node_modules/playwright/cli.js install chromium --with-deps \
&& chown -R node:node /home/node/.cache \
@@ -218,15 +193,15 @@ FROM runner-base AS runner-cli
USER root
# Install system dependencies required by openclaw (git+ssh references).
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends git ca-certificates docker.io docker-compose \
&& rm -rf /var/lib/apt/lists/* \
&& git config --system url."https://github.com/".insteadOf "ssh://git@github.com/"
# Install CLI tools globally. Separate layer from apt for better cache reuse.
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
RUN --mount=type=cache,target=/root/.npm \
npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest
USER node

259
README.md
View File

@@ -6,7 +6,7 @@
# 🚀 OmniRoute — The Free AI Gateway
### Never stop coding. Connect every AI tool to **250 providers** — **90+ free** — through one endpoint.
### Never stop coding. Connect every AI tool to **237 providers** — **90+ free** — through one endpoint.
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
<br/>
@@ -21,21 +21,16 @@
<h3>
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier.
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier. [![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
</h3>
[![Stars](https://img.shields.io/github/stars/diegosouzapw/OmniRoute?style=social)](https://github.com/diegosouzapw/OmniRoute)
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
[![Star History Rank](https://api.star-history.com/badge?repo=diegosouzapw/OmniRoute&theme=dark)](https://www.star-history.com/diegosouzapw/omniroute)
</br>
[![250 AI Providers](https://img.shields.io/badge/250-AI_Providers-6C5CE7?style=for-the-badge)](#-250-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-250-ai-providers--90-free)
[![237 AI Providers](https://img.shields.io/badge/237-AI_Providers-6C5CE7?style=for-the-badge)](#-237-ai-providers--90-free)
[![90+ Free](https://img.shields.io/badge/90%2B-Free_Tiers-00B894?style=for-the-badge)](#-237-ai-providers--90-free)
[![1.6B Free Tokens/mo](https://img.shields.io/badge/1.6B-Free_Tokens%2Fmo-00B894?style=for-the-badge)](docs/reference/FREE_TIERS.md)
[![Token Savings](https://img.shields.io/badge/up_to_95%25-Token_Savings-E17055?style=for-the-badge)](#%EF%B8%8F-save-1595-tokens--automatically)
[![18 Strategies](https://img.shields.io/badge/18-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![17 Strategies](https://img.shields.io/badge/17-Routing_Strategies-0984E3?style=for-the-badge)](#-combos--the-flagship)
[![$0 to start](https://img.shields.io/badge/%240-To_Start-FDCB6E?style=for-the-badge&logoColor=black)](#-quick-start)
<br/>
@@ -61,7 +56,7 @@
![Docker Pulls](https://img.shields.io/docker/pulls/diegosouzapw/omniroute?label=docker%20pulls&logo=docker&color=2496ED)
![Electron Downloads](https://img.shields.io/github/downloads/diegosouzapw/omniroute/total?style=flat&label=electron%20downloads&logo=electron&color=47848F)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-250-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-237-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
[💥 The Promise](#-the-promise) • [🤔 Why](#-why-omniroute) • [🏆 What Sets Apart](#-what-sets-omniroute-apart) • [🤖 Compatible CLIs](#-compatible-clis--coding-agents) • [🖥️ Where It Runs](#%EF%B8%8F-where-omniroute-runs--anywhere) • [🔒 Private](#-private--local-first) • [🎬 In Action](#-omniroute-in-action) • [📚 Explore More](#-explore-more) • [📧 Support](#-support--community)
@@ -71,53 +66,53 @@
<b>🌐 In 42+ languages</b>
<table>
<tr>
<td align="center"><a href="README.md"><img src="docs/assets/flags/us.svg" width="26" alt="English (en)"></a></td>
<td align="center"><a href="docs/i18n/pt-BR/README.md"><img src="docs/assets/flags/br.svg" width="26" alt="Português — Brasil (pt-BR)"></a></td>
<td align="center"><a href="docs/i18n/pt/README.md"><img src="docs/assets/flags/pt.svg" width="26" alt="Português (pt)"></a></td>
<td align="center"><a href="docs/i18n/es/README.md"><img src="docs/assets/flags/es.svg" width="26" alt="Español (es)"></a></td>
<td align="center"><a href="docs/i18n/fr/README.md"><img src="docs/assets/flags/fr.svg" width="26" alt="Français (fr)"></a></td>
<td align="center"><a href="docs/i18n/it/README.md"><img src="docs/assets/flags/it.svg" width="26" alt="Italiano (it)"></a></td>
<td align="center"><a href="docs/i18n/de/README.md"><img src="docs/assets/flags/de.svg" width="26" alt="Deutsch (de)"></a></td>
<td align="center"><a href="docs/i18n/nl/README.md"><img src="docs/assets/flags/nl.svg" width="26" alt="Nederlands (nl)"></a></td>
<td align="center"><a href="docs/i18n/ru/README.md"><img src="docs/assets/flags/ru.svg" width="26" alt="Русский (ru)"></a></td>
<td align="center"><a href="docs/i18n/uk-UA/README.md"><img src="docs/assets/flags/ua.svg" width="26" alt="Українська (uk-UA)"></a></td>
<td align="center"><a href="docs/i18n/pl/README.md"><img src="docs/assets/flags/pl.svg" width="26" alt="Polski (pl)"></a></td>
<td align="center"><a href="docs/i18n/cs/README.md"><img src="docs/assets/flags/cz.svg" width="26" alt="Čeština (cs)"></a></td>
<td align="center"><a href="docs/i18n/sk/README.md"><img src="docs/assets/flags/sk.svg" width="26" alt="Slovenčina (sk)"></a></td>
<td align="center"><a href="docs/i18n/ro/README.md"><img src="docs/assets/flags/ro.svg" width="26" alt="Română (ro)"></a></td>
<td align="center"><a href="docs/i18n/hu/README.md"><img src="docs/assets/flags/hu.svg" width="26" alt="Magyar (hu)"></a></td>
<td align="center"><a href="README.md">🇺🇸</a></td>
<td align="center"><a href="docs/i18n/pt-BR/README.md">🇧🇷</a></td>
<td align="center"><a href="docs/i18n/pt/README.md">🇵🇹</a></td>
<td align="center"><a href="docs/i18n/es/README.md">🇪🇸</a></td>
<td align="center"><a href="docs/i18n/fr/README.md">🇫🇷</a></td>
<td align="center"><a href="docs/i18n/it/README.md">🇮🇹</a></td>
<td align="center"><a href="docs/i18n/de/README.md">🇩🇪</a></td>
<td align="center"><a href="docs/i18n/nl/README.md">🇳🇱</a></td>
<td align="center"><a href="docs/i18n/ru/README.md">🇷🇺</a></td>
<td align="center"><a href="docs/i18n/uk-UA/README.md">🇺🇦</a></td>
<td align="center"><a href="docs/i18n/pl/README.md">🇵🇱</a></td>
<td align="center"><a href="docs/i18n/cs/README.md">🇨🇿</a></td>
<td align="center"><a href="docs/i18n/sk/README.md">🇸🇰</a></td>
<td align="center"><a href="docs/i18n/ro/README.md">🇷🇴</a></td>
<td align="center"><a href="docs/i18n/hu/README.md">🇭🇺</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/bg/README.md"><img src="docs/assets/flags/bg.svg" width="26" alt="Български (bg)"></a></td>
<td align="center"><a href="docs/i18n/da/README.md"><img src="docs/assets/flags/dk.svg" width="26" alt="Dansk (da)"></a></td>
<td align="center"><a href="docs/i18n/fi/README.md"><img src="docs/assets/flags/fi.svg" width="26" alt="Suomi (fi)"></a></td>
<td align="center"><a href="docs/i18n/no/README.md"><img src="docs/assets/flags/no.svg" width="26" alt="Norsk (no)"></a></td>
<td align="center"><a href="docs/i18n/sv/README.md"><img src="docs/assets/flags/se.svg" width="26" alt="Svenska (sv)"></a></td>
<td align="center"><a href="docs/i18n/zh-CN/README.md"><img src="docs/assets/flags/cn.svg" width="26" alt="中文 — 简体 (zh-CN)"></a></td>
<td align="center"><a href="docs/i18n/zh-TW/README.md"><img src="docs/assets/flags/tw.svg" width="26" alt="中文 — 繁體 (zh-TW)"></a></td>
<td align="center"><a href="docs/i18n/ja/README.md"><img src="docs/assets/flags/jp.svg" width="26" alt="日本語 (ja)"></a></td>
<td align="center"><a href="docs/i18n/ko/README.md"><img src="docs/assets/flags/kr.svg" width="26" alt="한국어 (ko)"></a></td>
<td align="center"><a href="docs/i18n/th/README.md"><img src="docs/assets/flags/th.svg" width="26" alt="ไทย (th)"></a></td>
<td align="center"><a href="docs/i18n/vi/README.md"><img src="docs/assets/flags/vn.svg" width="26" alt="Tiếng Việt (vi)"></a></td>
<td align="center"><a href="docs/i18n/id/README.md"><img src="docs/assets/flags/id.svg" width="26" alt="Bahasa Indonesia (id)"></a></td>
<td align="center"><a href="docs/i18n/ms/README.md"><img src="docs/assets/flags/my.svg" width="26" alt="Bahasa Melayu (ms)"></a></td>
<td align="center"><a href="docs/i18n/phi/README.md"><img src="docs/assets/flags/ph.svg" width="26" alt="Filipino (phi)"></a></td>
<td align="center"><a href="docs/i18n/bg/README.md">🇧🇬</a></td>
<td align="center"><a href="docs/i18n/da/README.md">🇩🇰</a></td>
<td align="center"><a href="docs/i18n/fi/README.md">🇫🇮</a></td>
<td align="center"><a href="docs/i18n/no/README.md">🇳🇴</a></td>
<td align="center"><a href="docs/i18n/sv/README.md">🇸🇪</a></td>
<td align="center"><a href="docs/i18n/zh-CN/README.md">🇨🇳</a></td>
<td align="center"><a href="docs/i18n/zh-TW/README.md">🇹🇼</a></td>
<td align="center"><a href="docs/i18n/ja/README.md">🇯🇵</a></td>
<td align="center"><a href="docs/i18n/ko/README.md">🇰🇷</a></td>
<td align="center"><a href="docs/i18n/th/README.md">🇹🇭</a></td>
<td align="center"><a href="docs/i18n/vi/README.md">🇻🇳</a></td>
<td align="center"><a href="docs/i18n/id/README.md">🇮🇩</a></td>
<td align="center"><a href="docs/i18n/ms/README.md">🇲🇾</a></td>
<td align="center"><a href="docs/i18n/phi/README.md">🇵🇭</a></td>
</tr>
<tr>
<td align="center"><a href="docs/i18n/in/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="हिन्दी (in)"></a></td>
<td align="center"><a href="docs/i18n/hi/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="हिन्दी (hi)"></a></td>
<td align="center"><a href="docs/i18n/gu/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="ગુજરાતી (gu)"></a></td>
<td align="center"><a href="docs/i18n/mr/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="मराठी (mr)"></a></td>
<td align="center"><a href="docs/i18n/ta/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="தமிழ் (ta)"></a></td>
<td align="center"><a href="docs/i18n/te/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="తెలుగు (te)"></a></td>
<td align="center"><a href="docs/i18n/bn/README.md"><img src="docs/assets/flags/bd.svg" width="26" alt="বাংলা (bn)"></a></td>
<td align="center"><a href="docs/i18n/ur/README.md"><img src="docs/assets/flags/pk.svg" width="26" alt="اردو (ur)"></a></td>
<td align="center"><a href="docs/i18n/fa/README.md"><img src="docs/assets/flags/ir.svg" width="26" alt="فارسی (fa)"></a></td>
<td align="center"><a href="docs/i18n/ar/README.md"><img src="docs/assets/flags/sa.svg" width="26" alt="العربية (ar)"></a></td>
<td align="center"><a href="docs/i18n/he/README.md"><img src="docs/assets/flags/il.svg" width="26" alt="עברית (he)"></a></td>
<td align="center"><a href="docs/i18n/tr/README.md"><img src="docs/assets/flags/tr.svg" width="26" alt="Türkçe (tr)"></a></td>
<td align="center"><a href="docs/i18n/az/README.md"><img src="docs/assets/flags/az.svg" width="26" alt="Azərbaycan (az)"></a></td>
<td align="center"><a href="docs/i18n/sw/README.md"><img src="docs/assets/flags/tz.svg" width="26" alt="Kiswahili (sw)"></a></td>
<td align="center"><a href="docs/i18n/in/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/hi/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/gu/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/mr/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/ta/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/te/README.md">🇮🇳</a></td>
<td align="center"><a href="docs/i18n/bn/README.md">🇧🇩</a></td>
<td align="center"><a href="docs/i18n/ur/README.md">🇵🇰</a></td>
<td align="center"><a href="docs/i18n/fa/README.md">🇮🇷</a></td>
<td align="center"><a href="docs/i18n/ar/README.md">🇸🇦</a></td>
<td align="center"><a href="docs/i18n/he/README.md">🇮🇱</a></td>
<td align="center"><a href="docs/i18n/tr/README.md">🇹🇷</a></td>
<td align="center"><a href="docs/i18n/az/README.md">🇦🇿</a></td>
<td align="center"><a href="docs/i18n/sw/README.md">🇹🇿</a></td>
</tr>
</table>
</div>
@@ -149,18 +144,18 @@
</div>
> One endpoint. **250 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
<table>
<tr>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 250 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 1595% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>90+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
</tr>
<tr>
<td width="33%" valign="top"><b>🔌 Every tool works</b><br/><sub>24+ coding agents — Claude Code, Codex, Cursor, Cline, Copilot, Antigravity — through one config.</sub></td>
<td width="33%" valign="top"><b>🧩 One endpoint</b><br/><sub>OpenAI ↔ Claude ↔ Gemini ↔ Responses API translation. Point any tool at <code>/v1</code> and it just works.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (94 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (95 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
</tr>
</table>
@@ -194,7 +189,7 @@
┌──────────────────────────────────────────────────────────┐
│ OmniRoute — Smart Router │
│ RTK + Caveman compression · 18 routing strategies │
│ RTK + Caveman compression · 17 routing strategies │
│ Circuit breakers · TLS stealth · MCP · A2A · Guardrails │
└─────────────────────────┬──────────────────────────────────┘
┌─────────────┬────┴────────┬─────────────┐
@@ -232,9 +227,9 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
##
### 🔀 Or build your own — 18 routing strategies
### 🔀 Or build your own — 17 routing strategies
All **18** strategies — mix & match per combo step:
All **17** strategies — mix & match per combo step:
| # | Strategy | What it does |
| --- | ------------------- | ---------------------------------------------------------------- |
@@ -253,11 +248,10 @@ All **18** strategies — mix & match per combo step:
| 13 | `context-relay` | Hand off context across targets for long conversations 🧠 |
| 14 | `context-optimized` | Pick the best fit for the current context size |
| 15 | `lkgp` | Last-Known-Good Path — sticky to the last successful target |
| 16 | `auto` | 12-factor live scoring across every connection 🤖 |
| 16 | `auto` | 9-factor live scoring across every connection 🤖 |
| 17 | `fusion` | Fan out to a panel of models + a judge synthesizes one answer 🧬 |
| 18 | `pipeline` | Chain steps — each target's output feeds the next one 🔗 |
<sub>The Auto-Combo engine scores every candidate on **12 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
<sub>The Auto-Combo engine scores every candidate on **9 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
##
@@ -281,7 +275,7 @@ Generous mode (<50% pool used) → idle shares are lent out
Strict mode (≥50% pool used) → each key held to its fair share
```
<sub>Enforced in the hot path **before** the request leaves OmniRoute, with per-(key, model) caps + session stickiness for prompt-cache integrity (now with a per-combo / global disable toggle). 📖 [Quota Sharing Engine](docs/routing/QUOTA_SHARE.md)</sub>
<sub>Enforced in the hot path **before** the request leaves OmniRoute, with per-(key, model) caps + session stickiness for prompt-cache integrity. 📖 [Quota Sharing Engine](docs/routing/QUOTA_SHARE.md)</sub>
##
@@ -314,11 +308,11 @@ Result: 4 layers of fallback = zero downtime
| Feature | OmniRoute | Other routers |
| -------------------------------------- | ------------------------------------------------------------------- | ------------- |
| 🌐 Providers | **250** | 20100 |
| 🌐 Providers | **237** | 20100 |
| 🆓 Free providers | **90+ (11 free forever)** | 15 |
| 🔀 Routing strategies | **18** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 13 |
| 🗜️ Token compression | **RTK + Caveman stacked (1595%)** | None / 2040% |
| 🧰 Built-in MCP server | **94 tools, 3 transports, 30 scopes** | Rare |
| 🧰 Built-in MCP server | **95 tools, 3 transports, 30 scopes** | Rare |
| 🤝 A2A agent protocol | **6 skills, JSON-RPC 2.0** | None |
| 🧠 Memory (FTS5 + vector) | **Yes** | Rare |
| 🛡️ Guardrails (PII, injection, vision) | **Yes** | Rare |
@@ -337,23 +331,23 @@ Result: 4 layers of fallback = zero downtime
</div>
> Recent highlights from **v3.8.20 → v3.8.47**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
> Recent highlights from **v3.8.20 → v3.8.44**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
- **🗜️ Compression hardening** — a default-on **inflation guard** (discard the stacked result and send the verbatim original whenever compression would _grow_ the prompt), completed **Caveman rule packs** for German / French / Japanese (dedup + ultra) plus a new **Chinese (文言 / wényán) input pack** with zh-vs-ja auto-detection, and **RTK filters for Gradle & .NET (`dotnet`)** build output. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **💸 Honest flat-rate cost** — subscription / coding-plan providers (ChatGPT Web, grok-web, the Minimax / Kimi / GLM / Alibaba Coding plans, Xiaomi MiMo…) now read **$0** in cost analytics instead of an inflated per-token estimate, while budget / quota / routing keep estimating unchanged. → [API Reference](docs/reference/API_REFERENCE.md)
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key, model) caps, session stickiness for prompt-cache integrity (now with a per-combo / global disable toggle), and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key, model) caps, session stickiness for prompt-cache integrity, and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md)
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing (now with **per-model web-search/web-fetch interception rules**), native **xAI Grok `/v1/responses`** routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). Embeddings-only and rerank-only models (JinaAI, OpenRouter custom, reranker models…) no longer disappear from the combo builder's model picker. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **🗜️ Pluggable compression** — an async pipeline of **10 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), the vendored **GCF codec updated to spec v3.2** (nested flattening — deeply-nested payloads go from ~3% to ~32% compression vs JSON), a new **omniglyph** engine (context-as-image, ~10× fewer tokens on the converted block), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), a unified panel with named profiles + an active-profile selector, an opt-in **per-engine pipeline circuit-breaker**, an opt-in **LLM-tier engine** (a model pass for higher-ratio semantic compression), a **read-lifecycle engine** that collapses superseded file reads, **usage-observed prefix freeze**, a graduated **CCR retrieval-feedback ramp**, a `preserveSystemPrompt` mode enum, and a **drag-reorder pipeline editor** in the studio. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). → [Auto-Combo](docs/routing/AUTO-COMBO.md)
- **🗜️ Pluggable compression** — an async pipeline of **10 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), a unified panel with named profiles + an active-profile selector, an opt-in **per-engine pipeline circuit-breaker**, an opt-in **LLM-tier engine** (a model pass for higher-ratio semantic compression), a **read-lifecycle engine** that collapses superseded file reads, **usage-observed prefix freeze**, a graduated **CCR retrieval-feedback ramp**, a `preserveSystemPrompt` mode enum, and a **drag-reorder pipeline editor** in the studio. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture & translate traffic from CLIs that ignore proxy env vars, with a per-SNI certificate authority and a trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), opt-in **typed memory decay** (aged low-value memories fade on a per-type schedule), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), server-side configured-only / available-only filters on the Free Provider Rankings page, and **Traditional Chinese (zh-TW)** localization for the frontend + CLI. → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 250-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, **bulk API-key add for Cloudflare Workers AI**, and **OpenVecta** (AI inference gateway). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, **Webshare** added as a paid fourth source in the free-proxy provider framework, and **shorthand proxy formats + protocol header mode** for bulk proxy import. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, and per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only). → [Environment](docs/reference/ENVIRONMENT.md)
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), and Codex account import from a raw ChatGPT access token. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
<br/>
@@ -395,11 +389,11 @@ Result: 4 layers of fallback = zero downtime
<div align="center">
# 🌐 250 AI Providers — 90+ Free
# 🌐 237 AI Providers — 90+ Free
</div>
> The most complete catalog of any open-source router: **250 providers**, **90+ with a free tier**, **11 free forever**.
> The most complete catalog of any open-source router: **237 providers**, **90+ with a free tier**, **11 free forever**.
<div align="center">
@@ -544,7 +538,7 @@ Expose OmniRoute over **MCP** or **A2A** and any capable agent gets the keys to
| Protocol | Endpoint | Use it for |
| ------------------ | ----------------------------------------------- | ------------------------------------------------------ |
| 🧰 **MCP (stdio)** | `omniroute --mcp` | Plug into Claude Desktop, Cursor, any MCP client |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **94 tools**, 30 scopes, full audit trail |
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **95 tools**, 30 scopes, full audit trail |
| 📡 **MCP (SSE)** | `http://localhost:20128/api/mcp/sse` | Streaming MCP transport |
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 6 skills |
@@ -574,7 +568,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) |
| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand |
| 3 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) |
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays, flat or nested (~30%), via a vendored **GCF** codec (spec v3.2) |
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays (~30%+) |
| 5 | **Relevance** | Extractive sentence scoring against the last user query |
| 6 | **Caveman** | Rule-based prose compression (~6575% on output) |
| 7 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async |
@@ -881,9 +875,9 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
<br/>
**Routing:** 18 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Routing:** 17 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
**Compatibility:** OpenAI ↔ Claude ↔ Gemini ↔ Responses API · auto OAuth refresh (PKCE, 8 providers) · multi-account round-robin · Batch + Files API · live OpenAPI 3.0.
**Protocols:** MCP (94 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Protocols:** MCP (95 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
**Plugins:** custom plugin marketplace (system-configured registry URL with SSRF-guarded fetch) · install / enable / disable · Notion + Obsidian knowledge-base integrations (WebDAV file server, vault search, note CRUD).
**Embedded services:** one-click install & lifecycle management of local sidecar services (CLIProxy, NineRouter).
**Quality & Ops:** built-in **Evals** (golden-set: exact/contains/regex/custom) · guardrails (PII, injection, vision) · health dashboard · p50/p95/p99 telemetry · webhooks · compliance audit.
@@ -907,7 +901,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 250 providers.
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
@@ -995,56 +989,66 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
### 📘 Getting Started
- **[User Guide](docs/guides/USER_GUIDE.md)** — Providers, combos, CLI integration, deployment
- **[Setup Guide](docs/guides/SETUP_GUIDE.md)** — Full install methods, CLI tool configs, protocol setup, timeout tuning
- **[CLI Tools Guide](docs/reference/CLI-TOOLS.md)** — Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot
- **[Remote Mode](docs/guides/REMOTE-MODE.md)** — Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens
- **[Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md)** — Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles
- **[Quick Start](README.md#-quick-start)** — 3-step install → connect → configure
| Document | Description |
| -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
| [Remote Mode](docs/guides/REMOTE-MODE.md) | Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens |
| [Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md) | Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles |
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
### 🔧 Operations & Deployment
- **[Docker Guide](docs/guides/DOCKER_GUIDE.md)** — Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags
- **[Podman Guide](contrib/podman/README.md)** — Quadlet systemd integration, podman-compose, SELinux
- **[VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md)** — Complete guide: VM + nginx + Cloudflare setup
- **[Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md)** — Deploy to Fly.io with persistent storage
- **[Termux Guide](docs/guides/TERMUX_GUIDE.md)** — Run OmniRoute on Android via Termux
- **[PWA Guide](docs/guides/PWA_GUIDE.md)** — Progressive Web App install, caching, architecture
- **[Uninstall Guide](docs/guides/UNINSTALL.md)** — Clean removal for all install methods
- **[Environment Config](docs/reference/ENVIRONMENT.md)** — Complete `.env` variables and references
| Document | Description |
| -------------------------------------------------------- | -------------------------------------------------------------- |
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
| [PWA Guide](docs/guides/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
### 🧠 Features & Architecture
- **[Architecture](docs/architecture/ARCHITECTURE.md)** — System architecture, data flow, and internals
- **[Compression Guide](docs/compression/COMPRESSION_GUIDE.md)** — 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked
- **[RTK Compression](docs/compression/RTK_COMPRESSION.md)** — Command-output compression, filters, trust, verify, raw-output recovery
- **[Compression Engines](docs/compression/COMPRESSION_ENGINES.md)** — Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces
- **[Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md)** — JSON rule-pack schemas for Caveman and RTK filters
- **[Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md)** — Language detection and Caveman rule-pack authoring
- **[Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)** — Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing
- **[Auto-Combo Engine](docs/routing/AUTO-COMBO.md)** — 12-factor scoring, mode packs, self-healing
- **[Proxy Guide](docs/ops/PROXY_GUIDE.md)** — 3-level proxy system, 1proxy marketplace, registry CRUD
- **[Free Tiers](docs/reference/FREE_TIERS.md)** — 25+ free API providers consolidated directory
- **[Features Gallery](docs/guides/FEATURES.md)** — Visual dashboard tour with screenshots
- **[Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md)** — Beginner-friendly codebase walkthrough
| Document | Description |
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture, data flow, and internals |
| [Compression Guide](docs/compression/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
| [RTK Compression](docs/compression/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
| [Compression Engines](docs/compression/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 9-factor scoring, mode packs, self-healing |
| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
### 🤖 Protocols & APIs
- **[API Reference](docs/reference/API_REFERENCE.md)** — All endpoints with examples
- **[OpenAPI Spec](docs/openapi.yaml)** — OpenAPI 3.0 specification
- **[MCP Server](open-sse/mcp-server/README.md)** — 95 MCP tools, IDE configs, Python/TS/Go clients
- **[MCP Server Guide](docs/frameworks/MCP-SERVER.md)** — MCP installation, transports, and tool reference
- **[A2A Server](src/lib/a2a/README.md)** — JSON-RPC 2.0 protocol, skills, streaming, task mgmt
- **[A2A Server Guide](docs/frameworks/A2A-SERVER.md)** — A2A agent card, tasks, skills, and streaming
| Document | Description |
| ------------------------------------------------- | --------------------------------------------------- |
| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
| [MCP Server](open-sse/mcp-server/README.md) | 95 MCP tools, IDE configs, Python/TS/Go clients |
| [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference |
| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
| [A2A Server Guide](docs/frameworks/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
### 📋 Project & Quality
- **[Contributing](CONTRIBUTING.md)** — Development setup and guidelines
- **[Changelog](CHANGELOG.md)** — Full per-version release history
- **[Security Policy](SECURITY.md)** — Vulnerability reporting and security practices
- **[i18n Guide](docs/guides/I18N.md)** — 40+ language support, translation workflow, RTL
- **[Release Checklist](docs/ops/RELEASE_CHECKLIST.md)** — Pre-release validation steps
- **[Coverage Plan](docs/ops/COVERAGE_PLAN.md)** — Test coverage strategy and 21,000+ test suite
| Document | Description |
| -------------------------------------------------- | ----------------------------------------------- |
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
| [Changelog](CHANGELOG.md) | Full per-version release history |
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
| [i18n Guide](docs/guides/I18N.md) | 40+ language support, translation workflow, RTL |
| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 21,000+ test suite |
<br/>
@@ -1146,13 +1150,14 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes
## 📊 Stars
<a href="https://www.star-history.com/?repos=diegosouzapw%2FOmniRoute&type=date&legend=top-left">
<a href="https://www.star-history.com/?repos=diegosouzapw%2Fomniroute&type=date&legend=top-left">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&theme=dark&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
</picture>
</a>
</div>
<br/>
@@ -1206,7 +1211,7 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
| Project | ⭐ | How it inspired OmniRoute |
| ---------------------------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.7k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. |
| **[GCF Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | First inspired our tabular compaction stage; now its zero-dependency, lossless generic-profile encoder is **vendored directly** as the Headroom codec (MIT, SPDX-marked), current with GCF spec v3.2. |
| **[GCF Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. |
| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 421 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. |
| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 1.0k | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. |
| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 110 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. |
@@ -1250,12 +1255,6 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
| ------------------------------------------------------------------------------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| **[awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)** · tldrsec | 708 | A curated list of secure-by-default libraries that guides our security choices (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink). |
### 🧭 Complementary tools
| Project | How it composes with OmniRoute |
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **[CodeWebChat](https://github.com/robertpiosik/CodeWebChat)** · robertpiosik | Editor-side companion — VS Code + browser extension that autofills 15+ chatbot web UIs with editor context. Owns the free-web-UI rail alongside OmniRoute's API rail; can point its API mode at OmniRoute. |
## ❤️ Support
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:

View File

@@ -15,11 +15,7 @@ function readCache() {
try {
const raw = JSON.parse(readFileSync(cachePath(), "utf8"));
if (raw && typeof raw.ts === "number" && Date.now() - raw.ts < CACHE_TTL_MS) return raw;
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] readCache failed:", err?.message ?? err);
}
}
} catch {}
return null;
}
@@ -45,20 +41,12 @@ async function refreshCache(opts = {}) {
const j = await mr.value.json();
models = (Array.isArray(j) ? j : j.data || []).map((m) => m.id).filter(Boolean);
}
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] refreshCache failed:", err?.message ?? err);
}
}
} catch {}
const data = { combos, providers, models, ts: Date.now() };
try {
mkdirSync(dirname(cachePath()), { recursive: true });
writeFileSync(cachePath(), JSON.stringify(data));
} catch (err) {
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
console.error("[omniroute completion] writeCache failed:", err?.message ?? err);
}
}
} catch {}
return data;
}

View File

@@ -24,7 +24,7 @@ async function restCompressionStatus() {
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
return {
strategy: settings.defaultMode || "standard",
engine: settings.engine ?? null,
settings,
combos: combosBody.combos ?? combosBody,
analytics,
@@ -33,10 +33,7 @@ async function restCompressionStatus() {
async function restCompressionConfigure(config) {
const body = { ...config };
if (body.strategy) {
body.defaultMode = body.strategy === "caveman" ? "standard" : normalizeEngine(body.strategy);
delete body.strategy;
}
if (body.engine) body.engine = normalizeEngine(body.engine);
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
@@ -46,10 +43,9 @@ async function restCompressionConfigure(config) {
}
async function restSetEngine(name) {
const normalized = normalizeEngine(name);
const res = await apiFetch("/api/settings/compression", {
method: "PUT",
body: { defaultMode: normalized === "caveman" ? "standard" : normalized },
body: { engine: normalizeEngine(name) },
});
if (!res.ok) {
process.stderr.write(`Error: ${res.status}\n`);
@@ -107,11 +103,7 @@ export async function runCompressionStatus(opts, cmd) {
export async function runCompressionConfigure(opts, cmd) {
const config = {};
// #6571 — both the MCP tool schema (compressionConfigureInput) and
// handleCompressionConfigure expect `strategy`, not `engine`; a non-strict
// MCP schema silently strips an unrecognized `engine` key on the primary
// (MCP-mounted) path, so this must be `strategy` on both paths.
if (opts.engine) config.strategy = normalizeEngine(opts.engine);
if (opts.engine) config.engine = opts.engine;
if (opts.cavemanAggressiveness !== undefined)
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
@@ -171,7 +163,7 @@ export function registerCompression(program) {
engine.command("set <name>").action(runCompressionEngineSet);
engine.command("get").action(async (opts, cmd) => {
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
process.stdout.write(`${data.strategy ?? "(default)"}\n`);
process.stdout.write(`${data.engine ?? "(default)"}\n`);
});
const combos = cmp.command("combos").description(t("compression.combos.description"));

View File

@@ -380,61 +380,18 @@ function resolveLivenessUrl(options = {}) {
return `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/api/health/degradation`;
}
async function probeUrl(url) {
try {
const response = await fetchWithTimeout(url);
return { ok: response.ok, status: response.status };
} catch {
return { ok: false, status: 0 };
}
}
async function checkServerLiveness(options = {}) {
const url = resolveLivenessUrl(options);
// First attempt: configured health endpoint (may require auth token).
const primary = await probeUrl(url);
if (primary.ok) {
return ok("Server liveness", "Server health endpoint is reachable", { url, status: primary.status });
}
// #6162: /api/health and /api/health/degradation require a management token.
// When unauthenticated, fall back to probing a publicly served static asset
// (favicon.ico) to confirm the Next.js server is alive and reachable.
// Derive the fallback URL from the primary URL (preserving protocol/host/port)
// so custom liveness URL configurations are honored. Fall back to defaults
// only if the primary URL can't be parsed.
let fallbackUrl;
try {
const parsed = new URL(url);
parsed.pathname = "/favicon.ico";
parsed.search = "";
parsed.hash = "";
fallbackUrl = parsed.toString();
const response = await fetchWithTimeout(url);
if (!response.ok) {
return warn("Server liveness", `Server responded with HTTP ${response.status}`, { url });
}
return ok("Server liveness", "Server health endpoint is reachable", { url });
} catch {
const port = parsePort(process.env.PORT || "20128", 20128);
const dashboardPort = parsePort(process.env.DASHBOARD_PORT || String(port), port);
const host = String(options.livenessHost || process.env.OMNIROUTE_DOCTOR_HOST || "127.0.0.1")
.trim()
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "");
fallbackUrl = `http://${formatHostForUrl(host || "127.0.0.1")}:${dashboardPort}/favicon.ico`;
return warn("Server liveness", "Server health endpoint is not reachable", { url });
}
const fallback = await probeUrl(fallbackUrl);
if (fallback.ok) {
return ok(
"Server liveness",
`Server reachable (health endpoint returned ${primary.status}, likely requires MANAGEMENT_TOKEN)`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
return warn(
"Server liveness",
`Server health endpoint returned HTTP ${primary.status || "no-response"} and fallback probe failed`,
{ primaryUrl: url, primaryStatus: primary.status, fallbackUrl, fallbackStatus: fallback.status }
);
}
export async function collectDoctorChecks(context = {}, options = {}) {

View File

@@ -48,11 +48,7 @@ export async function runHealthCommand(opts = {}) {
}
try {
const res = await apiFetch("/api/monitoring/health", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
if (!res.ok) {
console.error(t("common.error", { message: `HTTP ${res.status}` }));
return 1;
@@ -70,22 +66,29 @@ export async function runHealthCommand(opts = {}) {
if (health.uptime) console.log(t("health.uptime", { uptime: health.uptime }));
if (health.version) console.log(` Version: ${health.version}`);
if (health.activeConnections !== undefined) {
console.log(t("health.requests", { count: health.activeConnections }));
if (health.requests !== undefined) {
console.log(t("health.requests", { count: health.requests }));
}
if (health.circuitBreakers && opts.verbose) {
if (health.breakers && opts.verbose) {
console.log("\n \x1b[1mCircuit Breakers\x1b[0m");
const { open = 0, halfOpen = 0, closed = 0 } = health.circuitBreakers;
console.log(` \x1b[32m● closed\x1b[0m ${closed}`);
console.log(` \x1b[33m○ half-open\x1b[0m ${halfOpen}`);
console.log(` \x1b[31m○ open\x1b[0m ${open}`);
for (const [name, status] of Object.entries(health.breakers)) {
const state =
status.state === "closed" ? "\x1b[32m● closed\x1b[0m" : "\x1b[33m○ open\x1b[0m";
console.log(` ${name.padEnd(20)} ${state}`);
}
}
if (opts.verbose && health.memoryUsage) {
if (health.cache && opts.verbose) {
console.log("\n \x1b[1mCache\x1b[0m");
console.log(` Semantic hits: ${health.cache.semanticHits || 0}`);
console.log(` Signature hits: ${health.cache.signatureHits || 0}`);
}
if (opts.verbose && health.memory) {
console.log("\n \x1b[1mMemory\x1b[0m");
console.log(` RSS: ${health.memoryUsage.rss || "N/A"}`);
console.log(` Heap used: ${health.memoryUsage.heapUsed || "N/A"}`);
console.log(` RSS: ${health.memory.rss || "N/A"}`);
console.log(` Heap used: ${health.memory.heapUsed || "N/A"}`);
}
return 0;
@@ -97,17 +100,13 @@ export async function runHealthCommand(opts = {}) {
export async function runHealthComponentsCommand(opts = {}) {
try {
const res = await apiFetch("/api/monitoring/health", {
retry: false,
timeout: 5000,
acceptNotOk: true,
});
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
if (!res.ok) {
console.error(`HTTP ${res.status}`);
return 1;
}
const health = await res.json();
const components = health.components || health.circuitBreakers || {};
const components = health.components || health.breakers || {};
for (const [name, info] of Object.entries(components)) {
const status =
typeof info === "object" ? info.state || info.status || "unknown" : String(info);

View File

@@ -19,16 +19,6 @@ const STRIPPED_CODEX_ENV_KEYS = [
/** Placeholder so codex's `env_key` is always satisfied when the backend is open. */
const NO_AUTH_SENTINEL = "omniroute-no-auth";
// On Windows the `codex` binary is an npm `.cmd` shim that `spawn` cannot resolve
// without a shell (bare "codex" → ENOENT). Mirror the qodercli Windows fix (#6263):
// spawn `codex.cmd` through a shell on win32, and the bare binary elsewhere.
export function resolveCodexSpawn(platform) {
if (platform === "win32") {
return { command: "codex.cmd", shell: true };
}
return { command: "codex", shell: undefined };
}
function stripTrailingSlash(value) {
let s = String(value);
let end = s.length;
@@ -136,10 +126,10 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
if (!(await healthCheck(baseUrl))) {
console.error(
(
t("launch.notRunning") ||
"OmniRoute is not reachable at {port}. Start it with 'omniroute serve'."
).replace("{port}", baseUrl)
(t("launch.notRunning") || "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.").replace(
"{port}",
baseUrl
)
);
return 1;
}
@@ -152,12 +142,7 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
const env = buildCodexEnv(process.env, authToken);
return await new Promise((resolve) => {
const { command: codexLaunch, shell: shellValue } = resolveCodexSpawn(process.platform);
const child = spawn(codexLaunch, extraArgs, {
env,
stdio: "inherit",
shell: shellValue,
});
const child = spawn("codex", extraArgs, { env, stdio: "inherit" });
child.on("error", (err) => {
if (err?.code === "ENOENT") {
console.error(
@@ -180,16 +165,10 @@ export function registerLaunchCodex(program) {
t("launchCodex.description") || "Launch Codex CLI pointed at OmniRoute (local or remote VPS)"
)
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
.option(
"--remote <url>",
"Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)"
)
.option("--remote <url>", "Remote OmniRoute base URL, e.g. http://192.168.0.15:20128 (overrides --port + context)")
.option("--profile <name>", "Codex profile to activate (passed as --profile <name>)")
.option("-p, --p <name>", "Alias for --profile")
.option(
"--api-key <key>",
"OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)"
)
.option("--api-key <key>", "OmniRoute API key (overrides OMNIROUTE_API_KEY env var for this invocation)")
.allowUnknownOption(true)
.allowExcessArguments(true)
.argument("[codexArgs...]", "arguments passed through to the codex binary")

View File

@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { join, dirname } from "node:path";
import { fileURLToPath } from "node:url";
import { platform, totalmem, hostname as osHostname } from "node:os";
import { platform, totalmem } from "node:os";
import { t } from "../i18n.mjs";
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
@@ -170,16 +170,7 @@ export async function runServe(opts = {}) {
PORT: String(dashboardPort),
DASHBOARD_PORT: String(dashboardPort),
API_PORT: String(apiPort),
// #6194: POSIX shells (bash/zsh) auto-set HOSTNAME to the machine name — the
// .env loader (first-wins) can never override it. Ignore HOSTNAME when it
// matches the OS-reported hostname (the auto-set signature). OMNIROUTE_SERVER_HOST
// takes precedence; legacy HOSTNAME values that don't match os.hostname() are
// still honoured for backward compatibility (e.g. Windows CMD/PowerShell users
// who set HOSTNAME in .env where it is NOT auto-set).
HOSTNAME:
process.env.OMNIROUTE_SERVER_HOST ||
(process.env.HOSTNAME !== osHostname() ? process.env.HOSTNAME : undefined) ||
"0.0.0.0",
HOSTNAME: process.env.HOSTNAME || "0.0.0.0",
NODE_ENV: "production",
// #5238: preserve a user-set NODE_OPTIONS (incl. their own
// `--max-old-space-size=…`) instead of clobbering it with the calibrated
@@ -351,35 +342,11 @@ async function runWithSupervisor(
if (up) {
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
onReady(dashboardPort, apiPort, noOpen, startedAt);
} else {
reportReadinessTimeout(dashboardPort, supervisor);
}
});
}
}
// #6321: waitForServer resolving `false` used to fall through silently — the CLI
// printed the banner + "⏳ Starting server..." and then produced ZERO further
// output forever, even though the child process may well have crashed or be
// stuck (issue reports show the server sometimes actually comes up later, or is
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
// plus whatever stdout/stderr the child buffered instead of going silent.
export function reportReadinessTimeout(dashboardPort, supervisor) {
console.error(
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
` have failed silently.`
);
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);
const recentLog = supervisor?.getRecentLog?.() ?? [];
if (recentLog.length) {
console.error("--- Recent server output ---");
recentLog.forEach((l) => console.error(l));
console.error("--- End recent output ---\n");
}
}
let _killTray = null;
function killTrayIfActive() {
if (_killTray) {

File diff suppressed because it is too large Load Diff

View File

@@ -37,7 +37,6 @@ function inferSchema(sample) {
function formatCell(v, col) {
if (v == null) return "";
if (col.formatter) return col.formatter(v);
if (typeof v === "object") return JSON.stringify(v);
return String(v);
}

View File

@@ -52,31 +52,6 @@ export function hasModule(name) {
return existsSync(join(runtimeModules(), name, "package.json"));
}
/**
* Probe whether a native addon (.node) file can actually be dlopen'd by the Node runtime that
* is going to load it. Runs in a throwaway subprocess so a real ABI mismatch (which can segfault
* the process instead of throwing) never takes down the caller — only the probe subprocess.
*/
function probeNativeBinaryLoadable(binary) {
try {
const res = spawnSync(
process.execPath,
[
"-e",
"try { require(process.argv[1]); process.exit(0); } catch (e) { process.exit(1); }",
binary,
],
{ timeout: 10_000, stdio: "ignore" }
);
// status === 0 means require() (and therefore dlopen) succeeded. Anything else — a thrown
// ERR_DLOPEN_FAILED/NODE_MODULE_VERSION mismatch (status 1) or a crash (status null with a
// signal, e.g. SIGSEGV) — means the binary is not safe to load.
return res.status === 0;
} catch {
return false;
}
}
export function isBetterSqliteBinaryValid() {
const binary = join(
runtimeModules(),
@@ -93,18 +68,10 @@ export function isBetterSqliteBinaryValid() {
closeSync(fd);
const magic = buf.toString("hex");
const os = platform();
let formatOk;
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
else if (os === "darwin")
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
else formatOk = true;
if (!formatOk) return false;
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI
// (NODE_MODULE_VERSION) that will load it — a stale/foreign-ABI binary passes the header
// check and then crashes (segfault) on load instead of triggering a rebuild. Actually
// attempt to load it, isolated in a subprocess.
return probeNativeBinaryLoadable(binary);
if (os === "linux") return magic.startsWith("7f454c46"); // ELF
if (os === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
if (os === "win32") return magic.startsWith("4d5a"); // PE/MZ
return true;
} catch {
return false;
}

View File

@@ -35,32 +35,22 @@ export class ServerSupervisor {
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
// calibrated heap is already carried by env.NODE_OPTIONS either way.
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
// wasn't set (the default) — any debug/pino output written to stdout vanished
// silently, so a boot that never becomes ready looked like a dead hang with zero
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
// stderr so a readiness timeout can surface what the child actually printed.
this.child = spawn("node", [...heapArgs, this.serverPath], {
cwd: dirname(this.serverPath),
env: this.env,
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
});
writePidFile("server", this.child.pid);
const bufferOutput = (data) => {
const lines = data.toString().split("\n").filter(Boolean);
this.crashLog.push(...lines);
if (this.crashLog.length > CRASH_LOG_LINES) {
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
}
};
if (this.child.stdout) {
this.child.stdout.on("data", bufferOutput);
}
if (this.child.stderr) {
this.child.stderr.on("data", bufferOutput);
this.child.stderr.on("data", (data) => {
const lines = data.toString().split("\n").filter(Boolean);
this.crashLog.push(...lines);
if (this.crashLog.length > CRASH_LOG_LINES) {
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
}
});
}
this.child.on("error", (err) => this.handleExit(-1, err));
@@ -117,12 +107,6 @@ export class ServerSupervisor {
}, delay);
}
// #6321: exposes the buffered stdout+stderr lines so a caller (e.g. a readiness
// timeout) can print what the child actually said instead of silence.
getRecentLog() {
return [...this.crashLog];
}
dumpCrashLog() {
console.error("\n--- Server crash log ---");
this.crashLog.forEach((l) => console.error(l));

View File

@@ -65,52 +65,34 @@ export function sleep(ms) {
// cold start due to filesystem watchers, antivirus, etc.) get a working
// "server ready" signal instead of a phantom timeout while the server is
// still booting. TCP fallback marks the server as ready when the port
// has been listening for >= 3s consecutively AND the health route is
// actively rejecting/resetting connections fast (route not mounted yet,
// but the HTTP server is clearly alive and responsive) — never for a
// socket that merely accepts TCP and then hangs without ever completing
// a single request (#6800: that's a still-booting/CPU-bound process, not
// a "route not mounted" gap, and must NOT be reported as ready).
// has been listening for >= 3s consecutively but /api/monitoring/health
// has not yet been mounted — common during dev cold start.
export async function waitForServer(port, timeout = 60000) {
const start = Date.now();
let tcpListeningSince = null;
while (Date.now() - start < timeout) {
const outcome = await pollHealthOnce(port);
if (outcome === "ready") return true;
if (outcome === "fast-reject") {
try {
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
signal: AbortSignal.timeout(2000),
});
if (res.ok) return true;
// Server responded but health endpoint is not ready yet — keep
// polling, but the fact that we got a response means TCP is open.
if (tcpListeningSince === null) tcpListeningSince = Date.now();
if (Date.now() - tcpListeningSince >= 3000) return true;
} else {
// "hanging" (request timed out with no response at all) or
// "not-listening" — neither counts toward the grace window.
tcpListeningSince = null;
} catch {
const listening = await isPortListening(port).catch(() => false);
if (listening) {
if (tcpListeningSince === null) tcpListeningSince = Date.now();
if (Date.now() - tcpListeningSince >= 3000) return true;
} else {
tcpListeningSince = null;
}
}
await sleep(500);
}
return false;
}
// Polls /api/monitoring/health once and classifies the outcome:
// - "ready": got a 2xx HTTP response.
// - "fast-reject": got a non-2xx HTTP response, or the connection was
// actively refused/reset (not a timeout) — the HTTP server is alive and
// answering quickly, just not routing this endpoint yet (#2460).
// - "hanging": the request timed out waiting for any response — the
// process accepted the TCP connection but never answered (#6800).
// - "not-listening": nothing is accepting connections on the port at all.
async function pollHealthOnce(port) {
try {
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
signal: AbortSignal.timeout(2000),
});
return res.ok ? "ready" : "fast-reject";
} catch (err) {
if (err?.name === "TimeoutError") return "hanging";
const listening = await isPortListening(port).catch(() => false);
return listening ? "fast-reject" : "not-listening";
}
}
async function isPortListening(port) {
const net = await import("node:net");
return new Promise((resolve) => {

View File

@@ -6,7 +6,6 @@
* Special bypasses (handled before Commander):
* --mcp Start MCP server over stdio
* reset-encrypted-columns Recovery tool for broken encrypted credentials
* reset-password Reset the admin/management password
*
* All other commands are routed through Commander (bin/cli/program.mjs).
*/
@@ -211,15 +210,6 @@ if (process.argv.includes("reset-encrypted-columns")) {
process.exit(exitCode ?? 0);
}
if (process.argv.includes("reset-password")) {
// bin/reset-password.mjs self-executes its `main()` on import and calls
// process.exit() on completion/error. Await a never-resolving promise so
// control never falls through to Commander (which would then reject
// `reset-password` as an unknown command). See #6261.
await import(pathToFileURL(join(ROOT, "bin", "reset-password.mjs")).href);
await new Promise(() => {});
}
try {
const { createProgram } = await import(
pathToFileURL(join(ROOT, "bin", "cli", "program.mjs")).href

View File

@@ -5,15 +5,10 @@
*
* Usage:
* node bin/reset-password.mjs
* omniroute reset-password
*
* Non-interactive / scripted usage (piped stdin, e.g. CI or Docker):
* printf 'NewPass123\nNewPass123\n' | omniroute reset-password
* printf 'NewPass123' | omniroute reset-password --password-stdin
* npx omniroute reset-password
*
* Resets the admin password for OmniRoute.
* Prompts for a new password (interactive TTY) or reads it from stdin
* (non-TTY) and updates the database directly.
* Prompts for a new password and updates the database directly.
*
* @module bin/reset-password
*/
@@ -26,61 +21,19 @@ import { readManagementPasswordState, resetManagementPassword } from "./cli/sqli
const DATA_DIR = resolveDataDir();
const DB_PATH = resolveStoragePath(DATA_DIR);
const MIN_PASSWORD_LENGTH = 8;
const rl = createInterface({
input: process.stdin,
output: process.stdout,
});
/** Read the entire stdin stream as a UTF-8 string (used for non-TTY input). */
function readAllStdin() {
return new Promise((resolve) => {
let data = "";
process.stdin.setEncoding("utf8");
process.stdin.on("data", (chunk) => {
data += chunk;
});
process.stdin.on("end", () => resolve(data));
process.stdin.on("error", () => resolve(data));
// Resuming is implied by attaching a 'data' listener, but be explicit so a
// paused stream (some spawn setups) still flows to EOF.
process.stdin.resume();
});
function ask(question) {
return new Promise((resolve) => rl.question(question, resolve));
}
/**
* Obtain the new password (and its confirmation).
*
* - `--password-stdin`: the ENTIRE stdin is the password, no confirmation.
* - non-TTY stdin (piped): read all of stdin once; first line is the password,
* second line — when present — is the confirmation, else the first line is
* reused (a single-line pipe means "no separate confirmation").
* - interactive TTY: two sequential prompts (unchanged behavior).
*
* The non-TTY path exists because two sequential `rl.question` promises never
* settle under a piped EOF — the second read blocks forever, so the reset was
* silently never applied (#6258).
*/
async function collectPassword() {
if (process.argv.includes("--password-stdin")) {
const raw = await readAllStdin();
const password = raw.replace(/[\r\n]+$/, "");
return { password, confirm: password };
}
if (!process.stdin.isTTY) {
const raw = await readAllStdin();
const lines = raw.split(/\r?\n/);
const password = lines[0] ?? "";
const confirm = lines[1] ? lines[1] : password;
return { password, confirm };
}
const rl = createInterface({ input: process.stdin, output: process.stdout });
try {
const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
const password = await ask("Enter new password (min 8 chars): ");
const confirm = await ask("Confirm new password: ");
return { password, confirm };
} finally {
rl.close();
}
function exitWithError(message) {
console.error(message);
rl.close();
process.exit(1);
}
console.log("\n🔑 OmniRoute — Password Reset\n");
@@ -101,34 +54,27 @@ async function main() {
console.log(" No password is currently set.");
}
const { password, confirm } = await collectPassword();
const password = await ask("Enter new password (min 8 chars): ");
if (!password || password.length < MIN_PASSWORD_LENGTH) {
console.error(`\n❌ Password must be at least ${MIN_PASSWORD_LENGTH} characters.\n`);
process.exit(1);
if (!password || password.length < 8) {
exitWithError("\n❌ Password must be at least 8 characters.\n");
}
const confirm = await ask("Confirm new password: ");
if (password !== confirm) {
console.error("\n❌ Passwords do not match.\n");
process.exit(1);
exitWithError("\n❌ Passwords do not match.\n");
}
await resetManagementPassword(password, DB_PATH);
rl.close();
console.log("\n✅ Password reset successfully!");
console.log(" Restart OmniRoute for changes to take effect.\n");
}
main()
.then(() => {
// Explicit exit(0) so a caller that imports this module (bin/omniroute.mjs
// routes `omniroute reset-password` here) terminates cleanly instead of
// hanging / exiting with code 13 on an unsettled wrapper await. On POSIX,
// console.log to a pipe is synchronous, so the success line is already
// flushed by the time we exit.
process.exit(0);
})
.catch((err) => {
console.error(`\n❌ Error: ${err.message}\n`);
process.exit(1);
});
main().catch((err) => {
console.error(`\n❌ Error: ${err.message}\n`);
rl.close();
process.exit(1);
});

View File

@@ -1,42 +0,0 @@
# changelog.d/ — changelog fragments
**A PR never edits `CHANGELOG.md` directly during the cycle.** Instead it adds ONE new
file here — its changelog entry as a *fragment*. Two PRs never touch the same file, so
changelog merge conflicts (the "CHANGELOG-eat" cascade that forced a re-sync push + full
CI re-run after every sibling merge) are structurally impossible.
## Convention
| Directory | Aggregates under |
| -------------- | ----------------------- |
| `features/` | `### ✨ New Features` |
| `fixes/` | `### 🐛 Bug Fixes` |
| `maintenance/` | `### 📝 Maintenance` |
- **Filename**: `<PR-number>-<short-slug>.md` (e.g. `fixes/6700-dockerfile-better-sqlite3.md`).
The PR number prefix keeps aggregation order deterministic.
- **Content**: the exact bullet line(s) that should land in `CHANGELOG.md`, starting with
`- `. Multi-line (continuation) bullets are fine. Keep the repo's credit format:
`(#PR — thanks @user)`.
- One fragment per PR (rarely more, e.g. a PR that both fixes and adds).
## Example
`changelog.d/fixes/6496-cloudflare-relay-worker-syntax.md`:
```markdown
- **fix(providers):** Cloudflare relay Worker deploys use Service Worker syntax with `body_part` metadata ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)) — thanks @SeaXen
```
## Aggregation
The release captain (or `/generate-release`) folds all fragments into `CHANGELOG.md` and
deletes them:
```bash
node scripts/release/aggregate-changelog.mjs # write + delete fragments
node scripts/release/aggregate-changelog.mjs --dry-run # preview only
```
Fragment well-formedness is enforced by `npm run check:changelog-integrity` (the same
gate that guards against CHANGELOG-eat for legacy direct edits).

View File

@@ -1 +0,0 @@
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).

View File

@@ -1 +0,0 @@
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 5970% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).

View File

@@ -1 +0,0 @@
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).

View File

@@ -1 +0,0 @@
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)

View File

@@ -1 +0,0 @@
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)

View File

@@ -1 +0,0 @@
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).

View File

@@ -1 +0,0 @@
- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc).

View File

@@ -1 +0,0 @@
- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell).

View File

@@ -1 +0,0 @@
- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn).

View File

@@ -1 +0,0 @@
- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).

View File

@@ -1 +0,0 @@
- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`.

View File

@@ -1 +0,0 @@
- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan).

View File

@@ -1 +0,0 @@
- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127).

View File

@@ -1 +0,0 @@
- **feat(dashboard):** search box on the Playground's raw model `<select>` (#4086) — the shared `ModelSelectModal` (combo builder + CLI-code cards) already had search, but Playground's `StudioConfigPane` model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via `matchesSearch`), while the currently selected model always stays pinned in the list even if it no longer matches 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 translation key needed. Regression guard: `tests/unit/playground-model-selection-3731.test.ts` (`filterModelsByQuery`), `tests/unit/ui/playground-model-search-4086.test.tsx`.

View File

@@ -1 +0,0 @@
- **feat(usage):** Antigravity/agy quota widget now surfaces the **weekly** window alongside the existing per-model 5-hour window ([#4017](https://github.com/diegosouzapw/OmniRoute/issues/4017)) — the weekly limit isn't part of the per-model `retrieveUserQuota` response the fetcher already calls; it only appears in a separate, undocumented `retrieveUserQuotaSummary` RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new `usage/antigravityWeeklyQuota.ts` leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into `gemini_weekly`/`claude_gpt_weekly` quota entries, merged into the same `quotas` map the widget already renders generically. Regression guard: `tests/unit/antigravity-weekly-quota-4017.test.ts` (bucket parsing, the alternate `quotaSummary`-nested envelope, and end-to-end merge via `getUsageForProvider`).

View File

@@ -1 +0,0 @@
- **feat(codex):** 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 ([#3697](https://github.com/diegosouzapw/OmniRoute/issues/3697)). `openaiToOpenAIResponsesResponse` (`open-sse/translator/response/openai-responses.ts`) now threads the upstream model into the Responses event objects; a new `isCodexOriginatedHeaders()` (`open-sse/config/codexIdentity.ts`, reusing PR #3481's `originator`/User-Agent detection) makes chatCore's existing opt-in `echoRequestedModelName` (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when `codex/gpt-5.5-xhigh` is routed through a combo to a non-codex upstream; `echoModelInObject`/`echoModelInSseLine` (`open-sse/services/responseModelEcho.ts`) now also rewrite the nested `response.model` field the Responses API uses. `/v1/models` still returns `models: []` for Codex (unchanged). Regression guard: `tests/unit/codex-effort-model-echo-3697.test.ts`.

View File

@@ -1 +0,0 @@
- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `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 (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)

View File

@@ -1 +0,0 @@
- **feat(compression):** update the vendored **GCF** codec behind the **Headroom** engine to spec **v3.2 (nested flattening)** ([#6837](https://github.com/diegosouzapw/OmniRoute/issues/6837)). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via `>`-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (`meta:{...}`, `tags:[...]`) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, `cl100k_base`), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the `[N]:` inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a `__proto__`/`constructor` path segment never mutates `Object.prototype`, and keys shadowing built-ins like `toString` now round-trip correctly instead of misparsing). Regression guard: `tests/unit/compression/headroom-smartcrusher.test.ts` (deep-nested + prototype-pollution cases).

View File

@@ -1,2 +0,0 @@
- **feat(providers):** Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
- **chore(providers):** Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun

View File

@@ -1 +0,0 @@
- **Homologation suite**: new `npm run homolog` runs the full release-homologation battery against the deployed VPS — health/version parity, API + real SSE streaming with an ephemeral API key (created and revoked by the run), minimal-cost real-provider smoke (promptfoo generated from the live catalog), and a Playwright sweep that loads every dashboard route and exercises the API-key UI flow — emitting a unified CTRF report that backs the release STOP #2 checklist

View File

@@ -1 +0,0 @@
- **fix(api):** Vercel Relay deploy now checks the Deployment Protection (SSO) PATCH response and surfaces `ssoProtectionWarning` when Vercel rejects it, instead of silently activating a relay that later returns an undiagnosed `403 Access denied`. (thanks @ricatix)

View File

@@ -1 +0,0 @@
- **fix(combo):** streaming Claude responses whose content block opens (`content_block_start`) and closes with no usable text/tool_use — a shape some upstreams return for tool-heavy requests on HTTP 200 — are now detected by `validateResponseQuality`'s SSE peek and trigger combo failover instead of being forwarded to the client as a silent empty completion (thanks @heishen6).

View File

@@ -1 +0,0 @@
- **fix(codex):** strip regex `pattern` lookaround (lookahead/lookbehind) from tool JSON Schemas on the Codex/OpenAI native passthrough path — previously only the translated-request path coerced tool schemas, so a `pattern` like `^(?=.*@).+$` reached OpenAI unmodified and was rejected with `regex lookaround is not supported`. (thanks @evinjohnn) (#7100)

View File

@@ -1 +0,0 @@
- **fix(cli):** `stopMitm()` now removes /etc/hosts DNS-spoof entries before killing the MITM server process, closing the window where a client's DNS still resolved a target host to `127.0.0.1` while nothing was listening there — the cause of `connect ECONNREFUSED 127.0.0.1:443` right after stopping the MITM proxy (thanks @dionisius95).

View File

@@ -1 +0,0 @@
- **fix(sse):** Cursor Composer/Auto tool calls that separate the arg name and value with a space instead of a newline (e.g. `path /Users/.../test`) no longer produce empty-valued, malformed argument keys, fixing silent no-op Write/tool calls. (thanks @way-art)

View File

@@ -1 +0,0 @@
- **fix(dashboard):** the "Custom Models" add/edit form now has a "Vision capable" toggle so a custom OpenAI-compatible model can be manually flagged as vision-capable when the provider's discovery metadata doesn't report an image input modality (thanks @nguyenphi37)

View File

@@ -1 +0,0 @@
- **fix(combos):** fusion combos now reject an oversized panel (>40 models by default, tunable via `fusionTuning.maxPanel`) with a clean 400 before fanning out, instead of buffering dozens of concurrent full responses in memory and OOM-crashing the whole container. (thanks @fontvu)

View File

@@ -1 +0,0 @@
- **fix(providers):** the OpenAI-compatible "Check" validation flow now surfaces a warning when the chat-completions probe returns `404` (e.g. `model_not_found`) instead of silently passing as `Valid` — a bogus/non-standard model id (Featherless/OpenRouter-style `vendor/model` typos) previously went undetected at Check time and only surfaced once a real request tripped the per-model lockout. (thanks @advane204f)

View File

@@ -1 +0,0 @@
- **fix(executors):** forward agent-supplied `X-Session-ID`/`X-Title` metadata headers to upstream providers — previously dropped for every client outside the `x-opencode-*` allowlist. (thanks @chitholian) (#7104)

View File

@@ -1 +0,0 @@
- **fix(sse):** Antigravity streaming requests that hit a non-ok upstream response (e.g. a 403) no longer pipe the raw upstream bytes straight through to the client — a binary/non-UTF8 error body (observed as gzip-magic-byte garbage) is now routed through the same sanitized `buildAntigravityUpstreamError()` path the non-streaming branch already used, instead of corrupting the client-visible error message. Regression guard: `tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts` — thanks @Duongkhanhtool

View File

@@ -1 +0,0 @@
- **fix(cli):** the runtime self-heal now verifies a cached `better-sqlite3` native binary actually loads for the running Node before trusting it — the old check only inspected the file's magic bytes (ELF/Mach-O/PE header), so a binary built for a different Node ABI passed validation and segfaulted the process on first use instead of triggering a rebuild. (thanks @mrprohack) (#7105)

View File

@@ -1 +0,0 @@
- fix(providers): preserve relayAuth for vercel/deno/cloudflare relay proxies referenced by-id from the no-auth-provider Proxy Pool dropdown (#5716)

View File

@@ -1 +0,0 @@
- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142)

View File

@@ -1 +0,0 @@
- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272)

View File

@@ -1 +0,0 @@
- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276)

View File

@@ -1 +0,0 @@
- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun

View File

@@ -1 +0,0 @@
- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2).

View File

@@ -1 +0,0 @@
- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev).

View File

@@ -1 +0,0 @@
- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev).

View File

@@ -1 +0,0 @@
- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343)

View File

@@ -1 +0,0 @@
- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377)

View File

@@ -1 +0,0 @@
- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799)

View File

@@ -1 +0,0 @@
- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409)

View File

@@ -1 +0,0 @@
- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491)

View File

@@ -1 +0,0 @@
- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524)

View File

@@ -1 +0,0 @@
- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky).

View File

@@ -1 +0,0 @@
- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)).

View File

@@ -1 +0,0 @@
- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev).

View File

@@ -1 +0,0 @@
- fix(sse): sanitize non-Latin1 characters before embedding combo diagnostics in HTTP headers, preventing a ByteString crash on quality-check failure (#6612)

View File

@@ -1 +0,0 @@
- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623)

View File

@@ -1,2 +0,0 @@
- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628)
- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802)

View File

@@ -1 +0,0 @@
- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari).

View File

@@ -1 +0,0 @@
- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634)

Some files were not shown because too many files have changed in this diff Show More