mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-02 21:02:14 +03:00
Compare commits
2 Commits
fix/v3851-
...
feat/11655
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
235235f31c | ||
|
|
806280ec2c |
100
.env.example
100
.env.example
@@ -45,16 +45,6 @@ INITIAL_PASSWORD=CHANGEME
|
||||
# executor's on-disk thread-sticky session cache. Leave unset to rely on DATA_DIR.
|
||||
# OMNIROUTE_DATA_DIR=/var/lib/omniroute
|
||||
|
||||
# Directory the runtime plugin scanner reads, overriding the home-derived default (#11827).
|
||||
# Used by: src/lib/plugins/scanner.ts — getDefaultPluginDir(); it is also the root the
|
||||
# plugin manager installs into. Set it in Docker/K8s to point straight at the bind-mounted
|
||||
# plugin tree, instead of moving HOME (which changes every other HOME-relative behaviour)
|
||||
# just to relocate the scan path. Unset = <HOME>/.omniroute/plugins, and
|
||||
# /tmp/.omniroute/plugins when the process exports no home at all.
|
||||
# Distinct from the CLI-only variable in section 9 that points the omniroute-cmd-* command
|
||||
# loader (bin/cli/plugins.mjs) at a package tree — this one drives the server-side scanner.
|
||||
# OMNIROUTE_PLUGINS_DIR=/opt/omniroute/plugins
|
||||
|
||||
# Escape hatch for the test-context DATA_DIR guard (#10428). A test run that never
|
||||
# chose a DATA_DIR is redirected to a throwaway temp dir so it cannot open the
|
||||
# operator's real database. Set to 1 only for a deliberate run against the real
|
||||
@@ -81,11 +71,6 @@ INITIAL_PASSWORD=CHANGEME
|
||||
# Never set this for the running server. Used by: src/lib/buildPhase.ts, src/lib/db/core.ts
|
||||
# OMNIROUTE_BUILDING=1
|
||||
|
||||
# Skip the optional native-dependency prebuild check for exotic vendored trees.
|
||||
# This does not make a missing dependency buildable. Used by: scripts/check/check-native-deps.mjs
|
||||
# Default: 0 | Set to 1 only when native dependencies are supplied out of band.
|
||||
# OMNIROUTE_SKIP_NATIVE_DEP_CHECK=0
|
||||
|
||||
# Encryption key for SQLite database encryption at rest.
|
||||
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
|
||||
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.
|
||||
@@ -240,7 +225,7 @@ PORT=20128
|
||||
# Used by: src/app/api/v1/relay/chat/completions/route.ts
|
||||
# RELAY_IP_PER_MINUTE=30
|
||||
|
||||
# Bundler selection for `npm run dev` and `npm run build`. Set to 0 to fall back to webpack.
|
||||
# Bundler selection for `npm run dev`. Set to 0 to fall back to webpack.
|
||||
# Default is 1 (Turbopack). PR #4092 had forced webpack because earlier
|
||||
# Turbopack 16.2.x panicked on the OmniRoute module graph with "internal error:
|
||||
# entered unreachable code: there must be a path to a root"
|
||||
@@ -250,9 +235,8 @@ PORT=20128
|
||||
# /api/v1/models, /api/mcp) and repeated HMR rebuilds: zero panics. Turbopack
|
||||
# also keeps dev memory far lower on the edit→rebuild loop (HMR rebuild RSS stays
|
||||
# ~flat vs webpack's monotonic growth), which mitigates the dev-server OOM on
|
||||
# this 60+ route app. The production build (scripts/build/build-next-isolated.mjs)
|
||||
# reads the same flag: Turbopack by default, 0 builds with webpack (`npm run
|
||||
# build:contributor` sets it for you).
|
||||
# this 60+ route app. The production build still uses webpack (build pipeline is
|
||||
# unaffected by this dev-only flag).
|
||||
OMNIROUTE_USE_TURBOPACK=1
|
||||
|
||||
# Disable systemd sd_notify (Type=notify / WatchdogSec=) even when running
|
||||
@@ -674,11 +658,21 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
# open-sse/services/usage.ts.
|
||||
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
|
||||
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
|
||||
# Official OpenCode Go usage endpoint, authenticated with the connection API key.
|
||||
# Override only for relays or test fixtures.
|
||||
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/usage
|
||||
#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_DASHBOARD_URL=https://opencode.ai/workspace
|
||||
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
|
||||
|
||||
# OpenCode Go dashboard quota scraping. Prefer configuring these per connection
|
||||
# in Dashboard → Providers → OpenCode Go. Env vars are useful for headless
|
||||
# deployments or shared server defaults. The cookie is sensitive.
|
||||
#OPENCODE_GO_WORKSPACE_ID=wrk_...
|
||||
#OMNIROUTE_OPENCODE_GO_WORKSPACE_ID=wrk_...
|
||||
#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
|
||||
@@ -813,14 +807,9 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_CRUSH_BIN=crush
|
||||
# CLI_OMP_BIN=omp
|
||||
# CLI_LETTA_BIN=letta
|
||||
# CLI_PRIME_AGENT_BIN=prime-agent
|
||||
# Windsurf has no default binary — set this to enable binary detection for it.
|
||||
# CLI_WINDSURF_BIN=windsurf
|
||||
# CLI_AUGGIE_BIN=auggie
|
||||
# CLI_5DIVE_BIN=5dive
|
||||
# 5dive keeps root-owned auth profiles under a system state dir (its own STATE_DIR,
|
||||
# default /var/lib/5dive); override here when it lives elsewhere.
|
||||
# CLI_5DIVE_STATE_DIR=/var/lib/5dive
|
||||
# AUGGIE_BIN=auggie
|
||||
|
||||
# ── ZCode (Z.ai GLM coding-plan CLI) local provider ──
|
||||
@@ -980,11 +969,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
|
||||
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
|
||||
|
||||
# Cron expression for the call-log export job (destinations configured in the
|
||||
# dashboard under Integrations > Log export). Default: hourly, on the hour.
|
||||
# Used by: src/lib/jobs/logExportJob.ts. Timezone: UTC.
|
||||
#OMNIROUTE_LOG_EXPORT_CRON=0 * * * *
|
||||
|
||||
# Emergency budget-exhaustion fallback (set false or 0 to disable the reroute to
|
||||
# nvidia/openai/gpt-oss-120b when a request fails with a 402 budget error).
|
||||
# Used by: open-sse/services/emergencyFallback.ts. Default: enabled.
|
||||
@@ -1152,6 +1136,11 @@ CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
||||
# Trae OAuth token override. Used by: open-sse/executors/trae.ts.
|
||||
# TRAE_TOKEN=
|
||||
|
||||
# ── The Old LLM (theoldllm) ──
|
||||
# Playwright navigation timeout (ms) for the browser-backed token capture.
|
||||
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
|
||||
# THEOLDLLM_NAV_TIMEOUT_MS=30000
|
||||
|
||||
# ── Gemini / Antigravity (Google-based) ──
|
||||
# These providers ship public OAuth client_id/secret values embedded in their
|
||||
# public CLIs. Defaults are baked into the code via
|
||||
@@ -1760,7 +1749,6 @@ APP_LOG_TO_FILE=true
|
||||
|
||||
# Custom directory for CLI plugin discovery (omniroute-cmd-* packages).
|
||||
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
|
||||
# CLI-only: the server-side plugin scanner is pointed by OMNIROUTE_PLUGINS_DIR (section 2).
|
||||
# OMNIROUTE_PLUGIN_PATH=
|
||||
|
||||
# ── Prompt cache (system prompt deduplication) ──
|
||||
@@ -1825,7 +1813,7 @@ APP_LOG_TO_FILE=true
|
||||
# short alias prefix and the canonical provider prefix for each model (cc/claude-sonnet-4-6
|
||||
# AND claude/claude-sonnet-4-6) so client configs that hardcoded either form keep working —
|
||||
# which roughly doubles the catalog. "alias" emits one id per model; "canonical" emits only
|
||||
# the full provider-id prefix (providers whose alias is already canonical keep their one id).
|
||||
# the full provider-id prefix (and drops providers whose alias is already canonical).
|
||||
# A client can override per request with GET /v1/models?prefix=alias instead.
|
||||
# Also configurable from Dashboard > Settings > Feature Flags.
|
||||
# Used by: src/shared/constants/featureFlagDefinitions.ts, src/app/api/v1/models/catalog.ts
|
||||
@@ -2029,8 +2017,6 @@ APP_LOG_TO_FILE=true
|
||||
# CLIPROXYAPI_HOST=127.0.0.1
|
||||
# CLIPROXYAPI_PORT=5544
|
||||
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
|
||||
# Data-plane key fallback; the cliproxyapi_api_key setting takes precedence.
|
||||
# CLIPROXYAPI_API_KEY=
|
||||
# Management key for an externally managed instance. Embedded instances use
|
||||
# OmniRoute's encrypted service key.
|
||||
# CLIPROXYAPI_MANAGEMENT_KEY=
|
||||
@@ -2134,12 +2120,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/services/rateLimitManager.ts
|
||||
# RATE_LIMIT_MAX_WAIT_MS=15000
|
||||
|
||||
# Limiter-managed execution backstop (Bottleneck `expiration`): bounds a job's
|
||||
# post-dispatch execution, never queue wait. Must stay ABOVE upstream
|
||||
# fetch-start timeouts on non-incremental gateways. Default: 600000 (10 min)
|
||||
# Used by: open-sse/services/rateLimitManager.ts
|
||||
# RATE_LIMIT_EXECUTION_MAX_WAIT_MS=600000
|
||||
|
||||
# Rate limit queue admission cap: reject with 429 queue_full once this many requests
|
||||
# are already queued (0 = disabled/unbounded, the default). Used by: open-sse/services/rateLimitManager.ts
|
||||
# RATE_LIMIT_MAX_QUEUE_DEPTH=0
|
||||
@@ -2263,16 +2243,6 @@ APP_LOG_TO_FILE=true
|
||||
# Cursor image-generation wall clock (ms). Default: 210000.
|
||||
# CURSOR_IMG_TIMEOUT_MS=210000
|
||||
|
||||
# UC (uncensored.com) image-generation result-poll cadence + wall clock (ms).
|
||||
# Used by: open-sse/handlers/imageGeneration/providers/ucImage.ts. Defaults: 2000 / 60000.
|
||||
# UC_IMAGE_POLL_INTERVAL_MS=2000
|
||||
# UC_IMAGE_POLL_TIMEOUT_MS=60000
|
||||
|
||||
# UC (uncensored.com) video-generation result-poll cadence + wall clock (ms).
|
||||
# Used by: open-sse/handlers/videoGeneration/providers/ucVideo.ts. Defaults: 3000 / 300000.
|
||||
# UC_VIDEO_POLL_INTERVAL_MS=3000
|
||||
# UC_VIDEO_POLL_TIMEOUT_MS=300000
|
||||
|
||||
# Shared-seat concurrency gate for Cursor image jobs. Default: 2.
|
||||
# CURSOR_IMG_MAX_CONCURRENT=2
|
||||
|
||||
@@ -2404,9 +2374,6 @@ APP_LOG_TO_FILE=true
|
||||
# Bundled Codeium/language-server extension_version, distinct from Desktop.
|
||||
# Must use x.y.z format; invalid/unset values use the bundled default 1.48.2.
|
||||
# DEVIN_DESKTOP_EXTENSION_VERSION=1.48.2
|
||||
# Optional override for the Codeium seat-management API used by Devin CLI quota.
|
||||
# Used by: open-sse/services/usage/devinCli.ts. Default: https://server.codeium.com
|
||||
# DEVIN_SEAT_API_URL=https://server.codeium.com
|
||||
|
||||
# ── Command Code (custom CLI) callback ──
|
||||
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
||||
@@ -2476,6 +2443,14 @@ APP_LOG_TO_FILE=true
|
||||
# When enabled, the node authenticates with the API key stored on its connection.
|
||||
# AUDIO_REMOTE_PROVIDER_NODES=false
|
||||
|
||||
# ── 1Proxy egress pool ──
|
||||
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
|
||||
# CrofAI 1Proxy service. Disable, override URL, or tune the import quality.
|
||||
# ONEPROXY_ENABLED=true
|
||||
# ONEPROXY_API_URL=https://1proxy-api.aitradepulse.com
|
||||
# ONEPROXY_MAX_PROXIES=500
|
||||
# ONEPROXY_MIN_QUALITY_THRESHOLD=50
|
||||
|
||||
# ── Free Proxy Pool (auto-sync scheduler) ──
|
||||
# Background refresh of the free-proxy pool. Opt-in, OFF by default (parallels
|
||||
# Hard Rule #20's default-off posture for data-mutating background features).
|
||||
@@ -2894,14 +2869,6 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# PROMPTQL_TOKEN_REFRESH_URL=https://auth.pro.ql.app/ddn/project/token
|
||||
# PROMPTQL_POLL_TIMEOUT_MS=180000
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Kilo Code usage quotas (src/shared/constants/providers/kilocode.ts)
|
||||
# Personal USD balance and Kilo Pass usage lookup. Optional — the default
|
||||
# points at the public Kilo API; override only for a relay/test fixture.
|
||||
# Authentication uses the connection's existing OAuth access token.
|
||||
# Used by: open-sse/services/usage/kilocode.ts
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# KILO_API_URL=https://api.kilo.ai
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# HyperAgent web provider (Unofficial/Experimental — src/shared/constants/providers/web-cookie.ts)
|
||||
# Reverse-engineered session bridge for hyperagent.com. Optional — defaults
|
||||
@@ -2921,12 +2888,7 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# CHATGPT_WEB_CODEX_CDP_URL=http://chatgpt-web-codex-browser:9223
|
||||
# CHATGPT_WEB_CODEX_TUNNEL_ID=tunnel_0123456789abcdef0123456789abcdef
|
||||
# CHATGPT_WEB_CODEX_RUNTIME_KEY=
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex v2
|
||||
# CODEX_CHATGPT_WEB_HOME=/var/lib/omniroute/chatgpt-web-codex
|
||||
# CODEX_CHATGPT_WEB_BROWSER_DIAGNOSTICS=0
|
||||
# CODEX_CHATGPT_WEB_LAUNCHER=/absolute/path/to/codex-chatgpt-web
|
||||
# CODEX_CHATGPT_WEB_BUN=/absolute/path/to/bun
|
||||
# CODEX_WEB_GPT_BUN=/absolute/path/to/bun
|
||||
# CHATGPT_WEB_CODEX_CONNECTOR_NAME=OmniRoute Codex
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Browser-login VNC sessions (optional — src/lib/vncSession/manifest.ts)
|
||||
|
||||
48
.github/actions/npm-ci-retry/action.yml
vendored
48
.github/actions/npm-ci-retry/action.yml
vendored
@@ -1,45 +1,9 @@
|
||||
name: npm ci with retry
|
||||
description: >-
|
||||
Install dependencies. Restores node_modules from the Actions cache when the exact
|
||||
lockfile / runner / Node version / postinstall inputs match; otherwise runs npm ci
|
||||
with retries for transient registry/network failures and saves the tree for the
|
||||
next run.
|
||||
inputs:
|
||||
cache:
|
||||
description: Set to "false" to skip the node_modules cache and always run npm ci.
|
||||
required: false
|
||||
default: "true"
|
||||
description: Run npm ci with retries for transient registry/network failures.
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: Resolve Node version for the cache key
|
||||
id: node
|
||||
shell: bash
|
||||
run: echo "version=$(node --version)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# #8084 D3 (plan 3.8.51 task 5): every job used to pay ~80-90 s of `npm ci` even
|
||||
# with setup-node's npm tarball cache warm — 36 jobs per ci.yml run, ~55 min of
|
||||
# runner time per run just installing. A node_modules cache keyed on EVERYTHING
|
||||
# that shapes the tree lets a hit skip the install entirely.
|
||||
#
|
||||
# No restore-keys on purpose (same rule as the ESLint cache, #11600): a partial
|
||||
# tree from another lockfile / Node / postinstall script is exactly the kind of
|
||||
# silent drift a lockfile-pinned CI must never inherit. Exact key or a full npm ci.
|
||||
#
|
||||
# postinstall (scripts/build/postinstall.mjs + helpers) only mutates node_modules
|
||||
# on a plain install — its dist/ branch is gated on dist/ existing, which never
|
||||
# holds at install time in CI — so the cached tree already carries its effects.
|
||||
- name: Restore node_modules
|
||||
id: node-modules
|
||||
if: inputs.cache == 'true'
|
||||
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
||||
with:
|
||||
path: node_modules
|
||||
key: node-modules-${{ runner.os }}-${{ runner.arch }}-${{ steps.node.outputs.version }}-${{ hashFiles('package-lock.json', '.npmrc', 'scripts/build/postinstall.mjs', 'scripts/build/postinstallSupport.mjs', 'scripts/build/colocateOptionals.mjs', 'scripts/build/fixTlsClientNodeBinary.mjs', 'scripts/build/fixPlaywrightAndroid.mjs', 'scripts/build/native-binary-compat.mjs') }}
|
||||
|
||||
- name: npm ci (with retry)
|
||||
if: steps.node-modules.outputs.cache-hit != 'true'
|
||||
shell: bash
|
||||
- shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
@@ -51,8 +15,7 @@ runs:
|
||||
echo "npm ci attempt $attempt/$max_attempts after transient failure"
|
||||
fi
|
||||
|
||||
# --no-audit: `audit:deps` is its own gate; the inline audit only adds latency.
|
||||
if npm ci --no-audit --no-fund; then
|
||||
if npm ci; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -64,8 +27,3 @@ runs:
|
||||
sleep "$delay_seconds"
|
||||
delay_seconds=$((delay_seconds * 2))
|
||||
done
|
||||
|
||||
- name: node_modules restored from cache
|
||||
if: steps.node-modules.outputs.cache-hit == 'true'
|
||||
shell: bash
|
||||
run: echo "node_modules restored from cache (key hit) — npm ci skipped"
|
||||
|
||||
16
.github/dependabot.yml
vendored
16
.github/dependabot.yml
vendored
@@ -58,22 +58,6 @@ updates:
|
||||
# on the VPS — so keep auto-bumps frozen (no update-types = ignore every version).
|
||||
# Migrate it intentionally, not via dependabot (#4050).
|
||||
- dependency-name: "@huggingface/transformers"
|
||||
# onnxruntime-node is the OTHER HALF of the @huggingface/transformers pair frozen
|
||||
# above: the hoisted copy must equal the exact version transformers pins, or npm
|
||||
# nests a second ABI-incompatible native copy (contract test
|
||||
# tests/unit/onnxruntime-single-copy.test.ts, pair established in #9962). A solo
|
||||
# bump can never be correct — it only ever moves together with transformers, in
|
||||
# the same deliberate migration PR. Freezing it keeps the production group PRs
|
||||
# (e.g. #12219) from being born red on the pair contract.
|
||||
- dependency-name: "onnxruntime-node"
|
||||
# eslint-plugin-react-hooks is pinned to 7.0.1 by a contract test
|
||||
# (tests/unit/eslint-react-hooks-version-pinned.test.ts) until the 7.1.1 rule set
|
||||
# is adopted deliberately — that adoption needs a full cold lint run and its own
|
||||
# PR (the #12146 react-hooks migration finished on 2026-09-01, so the path is
|
||||
# open; the bump still must not ride a dependabot group, where it reds the
|
||||
# development group PRs, e.g. #12220). Remove this ignore in the adoption PR
|
||||
# together with the pin test.
|
||||
- dependency-name: "eslint-plugin-react-hooks"
|
||||
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
|
||||
36
.github/workflows/api-route-typecheck.yml
vendored
36
.github/workflows/api-route-typecheck.yml
vendored
@@ -1,36 +0,0 @@
|
||||
name: API Route Typecheck
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
- "release/**"
|
||||
types: [opened, synchronize, reopened, ready_for_review]
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
api-typecheck:
|
||||
name: API Route Typecheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- name: Reject new API-route TypeScript diagnostics
|
||||
run: node scripts/check/check-api-typecheck.mjs
|
||||
- name: API typecheck gate unit tests
|
||||
run: node --import tsx/esm --test tests/unit/build/check-api-typecheck.test.ts
|
||||
20
.github/workflows/ci.yml
vendored
20
.github/workflows/ci.yml
vendored
@@ -109,11 +109,8 @@ jobs:
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
# No restore-keys fallback on purpose (#11600, P-II.1 of the v3.8.50 postmortem): a
|
||||
# cache built under a different suppressions file / lint config / lockfile reports
|
||||
# stale per-file verdicts, which is exactly how 215 pre-existing errors stayed
|
||||
# invisible for a whole cycle. Exact key or a cold full lint (~13 min) — never a
|
||||
# partial cache from another configuration.
|
||||
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)
|
||||
@@ -212,11 +209,8 @@ jobs:
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
# No restore-keys fallback on purpose (#11600, P-II.1 of the v3.8.50 postmortem): a
|
||||
# cache built under a different suppressions file / lint config / lockfile reports
|
||||
# stale per-file verdicts, which is exactly how 215 pre-existing errors stayed
|
||||
# invisible for a whole cycle. Exact key or a cold full lint (~13 min) — never a
|
||||
# partial cache from another configuration.
|
||||
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
|
||||
@@ -627,9 +621,9 @@ jobs:
|
||||
# 13:50Z the kernel OOM-killed main's build while a PR build ran beside it
|
||||
# (five Build jobs had been queued by a burst of PRs). Two lanes: main keeps
|
||||
# its own so a release is never queued behind PR traffic; PR builds serialize
|
||||
# among themselves. docker-publish.yml's amd64 leg joins `heavy-build-main`
|
||||
# so a :next image build waits beside this artefact instead of becoming the
|
||||
# third heavy (#11976). GitHub keeps one running + one pending per group.
|
||||
# among themselves. GitHub keeps one running + one pending per group and
|
||||
# CANCELS older pendings — a cancelled PR build is re-runnable; a dead main
|
||||
# build costs the publish its artefact and a 40-minute rebuild that OOMs.
|
||||
concurrency:
|
||||
group: heavy-build-${{ github.ref == 'refs/heads/main' && 'main' || 'pr' }}
|
||||
cancel-in-progress: false
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
- uses: github/codeql-action/init@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
queries: security-extended
|
||||
- uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
|
||||
- uses: github/codeql-action/analyze@ff2f1c621b7f889edc0d3c761ac2e6a3f8cdb0dd # v4.37.7
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
44
.github/workflows/docker-publish.yml
vendored
44
.github/workflows/docker-publish.yml
vendored
@@ -26,14 +26,6 @@ on:
|
||||
type: boolean
|
||||
default: false
|
||||
|
||||
# One publish per ref. A merge storm used to fan out 8 concurrent hosted builds,
|
||||
# every one OOM-killing `npm run build` inside BuildKit (#11976). The :next
|
||||
# channel only needs the newest SHA; cancel-in-progress is the same pattern as
|
||||
# quality.yml / nightly-release-green.
|
||||
concurrency:
|
||||
group: docker-publish-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
# Least-privilege default: read-only at the top level; the build and merge jobs that
|
||||
# push to GHCR grant packages: write themselves (Scorecard TokenPermissions).
|
||||
permissions:
|
||||
@@ -126,23 +118,7 @@ jobs:
|
||||
name: Build Docker (${{ matrix.platform }})
|
||||
needs: prepare
|
||||
if: needs.prepare.outputs.skip != 'true'
|
||||
# amd64: the .113 omni-build pool (31 GB / 32 cores, ONE listener since
|
||||
# #12048). Hosted ubuntu-24.04 is ~7 GB and dies ResourceExhausted (#11976).
|
||||
# Falls back to hosted when USE_VPS_RUNNER is off. arm64: no ARM box — stay
|
||||
# on GitHub's ubuntu-24.04-arm.
|
||||
# Webpack on BOTH arches: Turbopack on omniroute-113-6 hit
|
||||
# TurbopackInternalError "there must be a path to a root" after 26 min
|
||||
# (run 33253576569). The same tree's arm64 webpack build on hosted ARM
|
||||
# succeeded (run 33264823398). Dockerfile already documents webpack as the
|
||||
# Docker escape hatch (OMNIROUTE_USE_TURBOPACK=0).
|
||||
runs-on: ${{ matrix.arch == 'amd64' && (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-build"]') || 'ubuntu-24.04') || 'ubuntu-24.04-arm' }}
|
||||
# Share the 1-slot omni-build ceiling (#12048) with ci.yml `Build` /
|
||||
# npm-publish. Same group as main's Build so a :next publish waits beside
|
||||
# the artefact instead of sitting next to it. arm64 is hosted — its own
|
||||
# group, cancelled by the workflow-level concurrency.
|
||||
concurrency:
|
||||
group: ${{ matrix.arch == 'amd64' && 'heavy-build-main' || format('docker-publish-arm-{0}', github.ref) }}
|
||||
cancel-in-progress: ${{ matrix.arch != 'amd64' }}
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
@@ -151,8 +127,10 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-24.04
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
env:
|
||||
IMAGE_NAME: diegosouzapw/omniroute
|
||||
@@ -165,9 +143,6 @@ jobs:
|
||||
ref: ${{ github.event_name == 'workflow_dispatch' && format('refs/tags/v{0}', inputs.version) || '' }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Assert Docker Engine
|
||||
run: docker info
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v4
|
||||
|
||||
@@ -191,8 +166,6 @@ jobs:
|
||||
context: .
|
||||
target: runner-base
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
OMNIROUTE_USE_TURBOPACK=0
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
@@ -210,8 +183,6 @@ jobs:
|
||||
context: .
|
||||
target: runner-web
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
OMNIROUTE_USE_TURBOPACK=0
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
@@ -237,8 +208,6 @@ jobs:
|
||||
file: Dockerfile.bun
|
||||
target: runner-base
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
OMNIROUTE_USE_TURBOPACK=0
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
@@ -264,8 +233,6 @@ jobs:
|
||||
file: Dockerfile.bun
|
||||
target: runner-web
|
||||
platforms: ${{ matrix.platform }}
|
||||
build-args: |
|
||||
OMNIROUTE_USE_TURBOPACK=0
|
||||
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||
tags: |
|
||||
${{ env.IMAGE_NAME }}
|
||||
@@ -528,14 +495,11 @@ jobs:
|
||||
severity: CRITICAL
|
||||
ignore-unfixed: true
|
||||
exit-code: "1"
|
||||
# Explicit: the advisory scan above already points at it, and the blocking
|
||||
# gate must honour the same accepted-risk list (#12084).
|
||||
trivyignores: .trivyignore
|
||||
|
||||
- name: Upload Trivy SARIF to Security tab
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
continue-on-error: true
|
||||
uses: github/codeql-action/upload-sarif@v4.37.8
|
||||
uses: github/codeql-action/upload-sarif@v4.37.7
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-image
|
||||
|
||||
13
.github/workflows/electron-release.yml
vendored
13
.github/workflows/electron-release.yml
vendored
@@ -4,10 +4,6 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
# A dispatch builds the ref it is dispatched ON (`gh workflow run … --ref v3.8.50` rebuilds
|
||||
# that tag; `--ref main` builds the repaired line). The ref is deliberately NOT an input:
|
||||
# CodeQL flags an input-controlled checkout next to the npm cache on the default branch as
|
||||
# cache poisoning (actions/cache-poisoning/poisonable-step), and `github.ref` is trusted.
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -421,14 +417,7 @@ jobs:
|
||||
tag_name: ${{ needs.validate.outputs.version }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
# NEVER. Phase 3 of the release flow creates the GitHub Release with the curated
|
||||
# notes seconds after pushing the tag, so by the time this step runs (1-2 h of
|
||||
# builds later) the body already exists — and `true` APPENDS GitHub's
|
||||
# auto-generated "What's Changed" block to it (v3.8.48 shipped that way; the
|
||||
# v3.8.50 re-attach dispatch added +1,416 chars to a 121 KB body, run
|
||||
# 33238093090). A curated body sits ~3 KB under the 125,000-char cap, so the
|
||||
# append can also turn this step RED and leave the release with no assets.
|
||||
generate_release_notes: false
|
||||
generate_release_notes: true
|
||||
fail_on_unmatched_files: false
|
||||
files: |
|
||||
release-assets/*.dmg
|
||||
|
||||
115
.github/workflows/nightly-release-green.yml
vendored
115
.github/workflows/nightly-release-green.yml
vendored
@@ -196,26 +196,6 @@ jobs:
|
||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||||
fi
|
||||
|
||||
- name: Close tracking issue when the branch is green again
|
||||
if: steps.validate.outputs.exit == '0'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The open/update step above is the UPWARD half of the loop; without this
|
||||
# step a stale "not green" issue outlives the fix and every base-green check
|
||||
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||||
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 close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||||
--comment "✅ \`${TARGET}\` is release-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||||
echo "Closed issue #$EXISTING"
|
||||
fi
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -314,25 +294,6 @@ jobs:
|
||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label base-red --body-file issue-body.md
|
||||
fi
|
||||
|
||||
- name: Close tracking issue when the branch is green again
|
||||
if: steps.validate.outputs.exit == '0'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# The open/update step above is the UPWARD half of the loop; without this
|
||||
# step a stale "not green" issue outlives the fix and every base-green check
|
||||
# (`AGENTS.md` → "Base-green check") keeps stamping new PRs as base-red inherited.
|
||||
TITLE="🔴 main branch not green"
|
||||
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 close "$EXISTING" --repo "$GITHUB_REPOSITORY" --reason completed \
|
||||
--comment "✅ \`main\` is main-green again at \`${GITHUB_SHA:0:9}\` — ${RUN_URL}. Auto-closed by Release-Green (continuous)."
|
||||
echo "Closed issue #$EXISTING"
|
||||
fi
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -423,13 +384,6 @@ jobs:
|
||||
# on `improvements`, complexity-ratchets only when `.improved`), and both exit
|
||||
# non-zero while the branch is over baseline — which is exactly when there is
|
||||
# nothing to bank. Their exit code is not the signal; the verifier below is.
|
||||
# Velocity phase (quality-baseline.json `_policy`, relax-baselines.mjs): the caps
|
||||
# were raised on purpose, so banking the measured shrink would silently undo the
|
||||
# 20% headroom every night. Pause the downward ratchet until the phase closes.
|
||||
if node -e 'process.exit(require("./config/quality/quality-baseline.json")._policy?.phase === "velocity" ? 0 : 1)'; then
|
||||
echo "Velocity phase active — ratchet banking paused (see docs/architecture/QUALITY_GATES.md → Velocity phase)."
|
||||
exit 0
|
||||
fi
|
||||
set +e
|
||||
node scripts/check/check-file-size.mjs --update
|
||||
node scripts/check/check-complexity-ratchets.mjs --update
|
||||
@@ -491,72 +445,3 @@ jobs:
|
||||
gh pr create --repo "$GITHUB_REPOSITORY" --base "$TARGET" --head "$BANK_BRANCH" \
|
||||
--title "chore(quality): bank ratchet shrinks (${TARGET})" --body-file pr-body.md
|
||||
fi
|
||||
|
||||
# ── Baseline headroom (velocity phase, 2026-08-30 → v4.0) ──────────────────────
|
||||
# The ratchets only speak when a baseline is crossed. With every baseline loosened by
|
||||
# 20% (scripts/quality/relax-baselines.mjs) the question is how fast the budget is
|
||||
# being consumed — this job measures each gate the way CI does and posts the headroom
|
||||
# table to one living issue, so a budget that fills in a week is visible before the
|
||||
# first red PR. Advisory: never fails the workflow.
|
||||
baseline-headroom:
|
||||
name: Baseline headroom
|
||||
if: ${{ github.event_name != 'push' }}
|
||||
timeout-minutes: 60
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-light"]')) || 'ubuntu-latest' }}
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 1
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
|
||||
- name: Measure headroom on ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
node scripts/quality/baseline-headroom.mjs \
|
||||
--json reports/quality/headroom.json --md reports/quality/headroom.md
|
||||
cat reports/quality/headroom.md >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload headroom report
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: baseline-headroom-${{ github.run_id }}
|
||||
path: reports/quality/headroom.*
|
||||
retention-days: 90
|
||||
|
||||
- name: Post to the living issue
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="📈 Baseline headroom (velocity phase)"
|
||||
BAD=$(node -e 'const r=require("./reports/quality/headroom.json").rows;console.log(r.filter(x=>x.status==="critical"||x.status==="warn").length)')
|
||||
{
|
||||
echo "Branch: \`${GITHUB_REF_NAME}\` · run: ${RUN_URL}"
|
||||
echo ""
|
||||
cat reports/quality/headroom.md
|
||||
} > headroom-comment.md
|
||||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -z "$EXISTING" ]; then
|
||||
EXISTING=$(gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --label quality-gate-finding \
|
||||
--body "Living tracker for the velocity-phase baseline budget (docs/architecture/QUALITY_GATES.md → Velocity phase). One comment per nightly run; the newest comment is the current state." \
|
||||
| grep -oE '[0-9]+$')
|
||||
fi
|
||||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file headroom-comment.md
|
||||
if [ "$BAD" != "0" ]; then
|
||||
gh issue edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --add-label "headroom-alert" 2>/dev/null || true
|
||||
else
|
||||
gh issue edit "$EXISTING" --repo "$GITHUB_REPOSITORY" --remove-label "headroom-alert" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
15
.github/workflows/npm-publish.yml
vendored
15
.github/workflows/npm-publish.yml
vendored
@@ -273,20 +273,11 @@ jobs:
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Attach SBOM to GitHub Release
|
||||
# Not only on the `release` event: the v3.8.50 package shipped through a
|
||||
# workflow_dispatch (staged publish, 11 attempts) and this step was skipped, so the
|
||||
# GitHub Release carried no SBOM until it was attached by hand from the run's
|
||||
# `sbom-npm` artifact. Attach whenever a release for the published tag exists.
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name == 'release' || github.event_name == 'workflow_dispatch')
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'release'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAG: ${{ github.event_name == 'release' && github.ref_name || format('v{0}', inputs.version) }}
|
||||
run: |
|
||||
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
echo "::notice::no GitHub Release for $TAG yet — SBOM stays on the sbom-npm workflow artifact"
|
||||
exit 0
|
||||
fi
|
||||
gh release upload "$TAG" sbom-npm.cdx.json --repo "$GITHUB_REPOSITORY" --clobber
|
||||
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
|
||||
|
||||
25
.github/workflows/quality.yml
vendored
25
.github/workflows/quality.yml
vendored
@@ -70,16 +70,7 @@ jobs:
|
||||
# 2026-08-14: 72 of the last 100 PRs into release/** came from forks, so the fork case is
|
||||
# the majority of the traffic, not the exception — this job earns its place, it just should
|
||||
# not duplicate build.yml for the own-origin 28%.
|
||||
# Disabled 2026-08-29 (#11976 follow-up). `continue-on-error: true` still
|
||||
# reports a GitHub check FAILURE, so every fork PR into release/** was born
|
||||
# with a red "Build (advisory)" even when every required gate was green
|
||||
# (sweep-reds, 41 PRs). Hosted ubuntu-latest cannot finish `npm run build`
|
||||
# on this tree — VM shutdown ~6 min in, same class as build.yml going
|
||||
# workflow_dispatch-only in #11962. Pre-merge build signal for release/**
|
||||
# is nightly-release-green (omni-build); for main it is ci.yml `Build`.
|
||||
# Restore this job when a runner that actually fits the tree is wired here.
|
||||
# Bare `false` (not `${{ false }}`) — zizmor obfuscation flags the expression form.
|
||||
if: false
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
# PINNED to hosted — this was the last job in THIS workflow still on the USE_VPS_RUNNER
|
||||
# switch (ci.yml's Build, nightly-release-green and npm-publish keep it, so the variable
|
||||
# stays meaningful), and with USE_VPS_RUNNER=true it produced NO signal at all here.
|
||||
@@ -303,12 +294,7 @@ jobs:
|
||||
# #8522: file-size is base-relative on PR events (compare against
|
||||
# max(frozen, base)) so inherited drift doesn't red an innocent PR;
|
||||
# workflow_dispatch (no PR base) falls back to absolute comparison.
|
||||
# New-code mode (Clean-as-You-Code, 2026-08-30): complexity-ratchets and
|
||||
# dead-code compare the PR's files against the merge-base and block only on
|
||||
# what the PR added; the global totals are advisory on PRs and re-frozen at
|
||||
# release. See scripts/check/newCodeMode.mjs.
|
||||
case "$g" in file-size|complexity-ratchets|dead-code) NEW_CODE=1 ;; *) NEW_CODE= ;; esac
|
||||
if [ -n "$NEW_CODE" ] && [ -n "${PR_BASE_SHA:-}" ]; then
|
||||
if [ "$g" = "file-size" ] && [ -n "${PR_BASE_SHA:-}" ]; then
|
||||
npm run "check:$g" -- --base-ref "$PR_BASE_SHA" || failed+=("$g")
|
||||
else
|
||||
npm run "check:$g" || failed+=("$g")
|
||||
@@ -526,10 +512,7 @@ jobs:
|
||||
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: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-light"]')) || 'ubuntu-latest' }}
|
||||
# 2026-08-30: a cold full lint with the eslint-plugin-react-hooks 7 compiler rules is
|
||||
# killed on the 7 GB hosted runner without a message (status null → exit 1, the
|
||||
# JSON never written); the box lints it in ~12 min with the heap below.
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
# G0 (trilho .50): security-events:read lets the CodeQL ratchet below read open
|
||||
# code-scanning alerts via `gh api .../code-scanning/alerts` (same as ci.yml's
|
||||
@@ -561,8 +544,6 @@ jobs:
|
||||
- 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
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=8192
|
||||
# ── G0 (trilho .50): motor de ratchet também no trilho B ─────────────────────
|
||||
# This job just wrote .artifacts/eslint-results.json — collect-metrics prefers
|
||||
# that file, so the ratchet engine lands here at ZERO extra ESLint cost (one
|
||||
|
||||
5
.github/workflows/scorecard.yml
vendored
5
.github/workflows/scorecard.yml
vendored
@@ -4,15 +4,12 @@ on:
|
||||
schedule:
|
||||
- cron: "27 7 * * 1"
|
||||
push:
|
||||
# Scorecard only accepts the DEFAULT branch — here the active release/vX.Y.Z,
|
||||
# not `main`. The job below guards on it so a push to any other branch skips.
|
||||
branches: ["main", "release/**"]
|
||||
branches: ["main"]
|
||||
|
||||
permissions: read-all
|
||||
|
||||
jobs:
|
||||
analysis:
|
||||
if: ${{ github.event_name != 'push' || github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
|
||||
name: Scorecard analysis
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
|
||||
10
.trivyignore
10
.trivyignore
@@ -19,12 +19,4 @@
|
||||
# Keep this list SHORT and reviewed every release. Prefer fixing (rebuild on a
|
||||
# patched base / bump the dep) over suppressing. Stale entries are debt.
|
||||
#
|
||||
# CVE-2025-68121 — Go stdlib crypto/tls (session-resumption certificate validation)
|
||||
# inside the PREBUILT bogdanfinn/tls-client v1.15.1 .so that tls-client-node's
|
||||
# postinstall downloads (built with go 1.24.1; fixed in 1.24.13). No upstream
|
||||
# rebuild exists (v1.15.1 is still the latest release) and nothing in this repo
|
||||
# can bump it. The binary is only loaded by the browser-TLS web-provider
|
||||
# executors (claude-web / grok-web / lmarena / perplexity-web / notion-web),
|
||||
# whose handshakes go through utls. Tracking issue: #12084. Revisit at the next
|
||||
# tls-client release or base-image bump and BEFORE the v3.8.51 tag (2026-09-15).
|
||||
CVE-2025-68121
|
||||
# (No accepted-risk suppressions at present — ignore-unfixed covers the noise.)
|
||||
|
||||
@@ -3477,7 +3477,7 @@ export function createOmniRouteProviderHook(
|
||||
|
||||
// ── Combo LCD across nested combo-refs (T-NN) ───────────────────────
|
||||
// Combos can nest other combos via `kind: "combo-ref"` members
|
||||
// (e.g. MASTER-LIGHT contains LEGACY, KIRO, Opecode Zen FREE). The
|
||||
// (e.g. MASTER-LIGHT contains OldLLM, KIRO, Opecode Zen FREE). The
|
||||
// nested combo's own `limit.context` is computed below in this same
|
||||
// loop, so we need a fixpoint iteration: if a combo-ref points at a
|
||||
// combo not yet processed, defer this combo and try again after the
|
||||
@@ -4495,7 +4495,7 @@ export function buildStaticProviderEntry(
|
||||
// ── Combo LCD across nested combo-refs (T-NN mirror) ─────────────────
|
||||
// Mirror of the dynamic-catalog fixpoint iteration: combos can nest
|
||||
// other combos via `kind: "combo-ref"` members (e.g. MASTER-LIGHT
|
||||
// contains LEGACY, KIRO, Opecode Zen FREE). The nested combo's own
|
||||
// contains OldLLM, KIRO, Opecode Zen FREE). The nested combo's own
|
||||
// capabilities and limits are computed in this same loop, so we need
|
||||
// a fixpoint pass: if a combo-ref points at a combo not yet processed,
|
||||
// defer this combo and try again after the sibling combos catch up.
|
||||
|
||||
@@ -641,13 +641,13 @@ test("models(): combos fetcher receives the resolved baseURL + apiKey", async ()
|
||||
|
||||
test("models(): nested combo-ref context is the min of nested + raw members", async () => {
|
||||
// Top-level combo MASTER-LIGHT has 1 raw model (claude-primary, 200k)
|
||||
// and 2 combo-refs: LEGACY (8k member) and KIRO (32k member). The OLD
|
||||
// and 2 combo-refs: OldLLM (8k member) and KIRO (32k member). The OLD
|
||||
// plugin would advertise 200k (only the raw model); the fix should
|
||||
// make it advertise 8k (the bottleneck across the member graph).
|
||||
const modelsFetcher = stubModelsFetcher([
|
||||
MODEL_PRIMARY,
|
||||
{
|
||||
id: "legacy-member-1",
|
||||
id: "oldllm-member-1",
|
||||
context_length: 8_000,
|
||||
max_output_tokens: 4_000,
|
||||
capabilities: {
|
||||
@@ -677,9 +677,9 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
|
||||
]);
|
||||
const combosFetcher = stubCombosFetcher([
|
||||
{
|
||||
id: "legacy",
|
||||
name: "LEGACY",
|
||||
models: [{ id: "s1", kind: "model", model: "legacy-member-1", weight: 100 }],
|
||||
id: "oldllm",
|
||||
name: "OldLLM",
|
||||
models: [{ id: "s1", kind: "model", model: "oldllm-member-1", weight: 100 }],
|
||||
},
|
||||
{
|
||||
id: "kiro",
|
||||
@@ -691,7 +691,7 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
|
||||
name: "MASTER-LIGHT",
|
||||
models: [
|
||||
{ id: "r1", kind: "model", model: "claude-primary", weight: 50 },
|
||||
{ id: "r2", kind: "combo-ref", comboName: "LEGACY", weight: 25 },
|
||||
{ id: "r2", kind: "combo-ref", comboName: "OldLLM", weight: 25 },
|
||||
{ id: "r3", kind: "combo-ref", comboName: "KIRO", weight: 25 },
|
||||
],
|
||||
},
|
||||
@@ -706,6 +706,6 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
|
||||
assert.equal(
|
||||
masterLight.limit.context,
|
||||
8_000,
|
||||
`expected 8_000 (LEGACY bottleneck), got ${masterLight.limit.context}`
|
||||
`expected 8_000 (OldLLM bottleneck), got ${masterLight.limit.context}`
|
||||
);
|
||||
});
|
||||
|
||||
90
AGENTS.md
90
AGENTS.md
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 354 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 351 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -56,9 +56,9 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (167 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (166 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
@@ -83,7 +83,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`): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-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 16-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`): 19 public strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, cache-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 15-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
|
||||
---
|
||||
|
||||
@@ -110,36 +110,26 @@ upstream/service level, so one unhealthy provider does not slow down every reque
|
||||
- Shared wrappers: `open-sse/services/accountFallback.ts`
|
||||
- Persisted state table: `domain_circuit_breakers`
|
||||
|
||||
**States** (4 — `src/shared/utils/circuitBreaker.ts`):
|
||||
**States**:
|
||||
|
||||
- `CLOSED`: normal traffic is allowed.
|
||||
- `DEGRADED`: early-warning band — failures crossed the degradation threshold but not the
|
||||
breaker threshold yet; traffic still flows, dashboards show the warning.
|
||||
- `OPEN`: provider is temporarily blocked; callers get a provider-circuit-open response
|
||||
or combo routing skips to another target.
|
||||
- `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
|
||||
breaker, failure opens it again.
|
||||
|
||||
**Defaults** (`open-sse/config/constants.ts` → `PROVIDER_PROFILES`, consumed via
|
||||
`DEFAULT_RESILIENCE_SETTINGS.providerBreaker` in `src/lib/resilience/settings.ts` →
|
||||
`getCircuitBreaker(provider, …)` in `src/sse/handlers/chatHelpers.ts`). The whole-provider
|
||||
breaker runs on `circuitBreakerThreshold` / `circuitBreakerReset`:
|
||||
**Defaults** (`open-sse/config/constants.ts` → `PROVIDER_PROFILES`). Two thresholds live side by
|
||||
side — do not confuse them:
|
||||
|
||||
| Profile | degrades at | opens at (`circuitBreakerThreshold`) | reset (`circuitBreakerReset`) |
|
||||
| ------- | ----------: | -----------------------------------: | ----------------------------: |
|
||||
| OAuth | `5` | `8` | `60s` |
|
||||
| API key | `7` | `12` | `30s` |
|
||||
| Local | (derived) | `2` | `15s` |
|
||||
| Profile | `providerFailureThreshold` (whole provider) | `providerCooldownMs` | `circuitBreakerThreshold` (one connection) | `circuitBreakerReset` |
|
||||
| ------- | ------------------------------------------: | -------------------: | -----------------------------------------: | --------------------: |
|
||||
| OAuth | `10` | `5min` | `8` | `60s` |
|
||||
| API key | `15` | `10min` | `12` | `30s` |
|
||||
| Local | `2` | `1min` | `2` | `15s` |
|
||||
|
||||
`PROVIDER_PROFILES` also defines `providerFailureThreshold` (10/15/2),
|
||||
`providerFailureWindowMs` (15/30/5 min) and `providerCooldownMs` (5/10/1 min): these power the
|
||||
**window gate of the opt-in global Provider Cooldown** (`PROVIDER_COOLDOWN_ENABLED`, default
|
||||
off) — a provider-level entry in `open-sse/services/providerCooldownTracker.ts` only counts as
|
||||
cooling after `providerFailureThreshold` failures inside `providerFailureWindowMs`, and then
|
||||
cools for `providerCooldownMs`. They are NOT the live breaker's thresholds — do not tune them
|
||||
expecting breaker behavior. Every default is overridable through the
|
||||
`OMNIROUTE_PROVIDER_BREAKER_*` and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars; the
|
||||
runtime-accurate reference table lives in `docs/architecture/RESILIENCE_GUIDE.md`.
|
||||
The provider-level thresholds were scaled up for deployments with 500+ connections (OAuth was
|
||||
`3`, API key was `5`); every default is overridable through the `OMNIROUTE_PROVIDER_BREAKER_*`
|
||||
and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars.
|
||||
|
||||
Only provider-level failure statuses should trip the provider breaker:
|
||||
|
||||
@@ -252,7 +242,7 @@ Read the nearest `AGENTS.md` and the linked deep-dive before making a non-trivia
|
||||
| Streaming request handling | `open-sse/handlers/` | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
||||
| Provider execution and translation | `open-sse/executors/`, `open-sse/translator/` | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
|
||||
| Routing and resilience | `open-sse/services/` | [`open-sse/services/AGENTS.md`](open-sse/services/AGENTS.md), [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
|
||||
| Database and migrations | `src/lib/db/`, `src/lib/db/migrations/` | [`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md) |
|
||||
| Database and migrations | `src/lib/db/`, `db/migrations/` | [`src/lib/db/AGENTS.md`](src/lib/db/AGENTS.md) |
|
||||
| Domain policy | `src/domain/` | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
||||
| MCP and A2A | `open-sse/mcp-server/`, `src/lib/a2a/` | [`docs/frameworks/MCP-SERVER.md`](docs/frameworks/MCP-SERVER.md), [`docs/frameworks/A2A-SERVER.md`](docs/frameworks/A2A-SERVER.md) |
|
||||
| Agent features | `src/lib/{acp,memory,skills,cloudAgent}/` | [`docs/frameworks/AGENT_PROTOCOLS_GUIDE.md`](docs/frameworks/AGENT_PROTOCOLS_GUIDE.md), [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
|
||||
@@ -264,13 +254,13 @@ Read the nearest `AGENTS.md` and the linked deep-dive before making a non-trivia
|
||||
## File placement & repo-root hygiene
|
||||
|
||||
- **Test files**: ALL unit tests, integration tests, ecosystem tests, or Vitest files MUST strictly be placed within the `tests/` directory (e.g., `tests/unit/`, `tests/integration/`). NEVER create test files in the project root (`/`).
|
||||
- **Scripts and utilities**: ALL maintenance, debugging, generation, or experimental scripts (`.cjs`, `.mjs`, `.js`, `.ts`) MUST be placed strictly inside one of the `scripts/` subfolders (`build/`, `dev/`, `check/`, `docs/`, `i18n/`, `ad-hoc/`, `quality/`, `release/`, `ci/`, `ops/`, `perf/`, `research/`, `sre/`, `vps/`, `homolog/`, `packs/`, `skills/`, `test/`, `cli/`, `compression/`, `compression-eval/`, `devin-bridge/`, `docker/`, `features/`, `router-eval/`). One-shot or experimental code goes under `scripts/ad-hoc/`. NEVER dump loose scripts in the project root (`/`) or the top-level `scripts/` folder.
|
||||
- **Scripts and utilities**: ALL maintenance, debugging, generation, or experimental scripts (`.cjs`, `.mjs`, `.js`, `.ts`) MUST be placed strictly inside one of the `scripts/` subfolders (`build/`, `dev/`, `check/`, `docs/`, `i18n/`, `ad-hoc/`, `quality/`, `release/`, `ci/`, `ops/`, `perf/`, `research/`, `sre/`, `vps/`, `homolog/`, `raycast/`, `skills/`, `test/`, `cli/`, `compression/`, `compression-eval/`, `devin-bridge/`, `docker/`, `features/`, `router-eval/`). One-shot or experimental code goes under `scripts/ad-hoc/`. NEVER dump loose scripts in the project root (`/`) or the top-level `scripts/` folder.
|
||||
|
||||
**The project root MUST ONLY contain:**
|
||||
|
||||
- Configuration files (`vitest.config.ts`, `next.config.mjs`, `eslint.config.mjs`, `tsconfig*.json`, `playwright.config.ts`, `prettier.config.mjs`, `postcss.config.mjs`, `sonar-project.properties`, `fly.toml`, `docker-compose*.yml`, `Dockerfile`)
|
||||
- Dependency files (`package.json`, `package-lock.json`)
|
||||
- Documentation files (`README.md`, `CHANGELOG.md`, `ROADMAP.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `llm.txt`)
|
||||
- Documentation files (`README.md`, `CHANGELOG.md`, `ROADMAP.md`, `LICENSE`, `AGENTS.md`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`, `llm.txt`, `Tuto_Qdrant.md`)
|
||||
- CI/CD files and ignore definitions (`.gitignore`, `.dockerignore`, `.npmignore`, `.npmrc`, `.node-version`, `.nvmrc`, `.env.example`)
|
||||
|
||||
When creating _any_ validation tests or one-off logic scripts, default to `scripts/ad-hoc/` or `tests/unit/` according to your goals. Do not pollute the `/` root context.
|
||||
@@ -299,7 +289,8 @@ When creating _any_ validation tests or one-off logic scripts, default to `scrip
|
||||
### Database
|
||||
|
||||
- **Always** go through `src/lib/db/` domain modules — **never** write raw SQL in routes or handlers
|
||||
- **Never** barrel-import from `localDb.ts` — import specific `src/lib/db/*` modules
|
||||
- **Never** add logic to `src/lib/localDb.ts` (re-export layer only)
|
||||
- **Never** barrel-import from `localDb.ts` — import specific `db/` modules instead
|
||||
- DB singleton: `getDbInstance()` from `src/lib/db/core.ts` (WAL journaling)
|
||||
- Migrations: `src/lib/db/migrations/` — versioned SQL files, idempotent, run in transactions
|
||||
|
||||
@@ -364,18 +355,19 @@ Documentation must describe verified behavior, not plausible behavior.
|
||||
1. Create `src/lib/db/yourModule.ts` — import `getDbInstance` from `./core.ts`
|
||||
2. Export CRUD functions for your domain table(s)
|
||||
3. Add migration in `src/lib/db/migrations/` if new tables needed
|
||||
4. Write tests
|
||||
4. Re-export from `src/lib/localDb.ts` (add to the re-export list only)
|
||||
5. Write tests
|
||||
|
||||
### Adding a New MCP Tool
|
||||
|
||||
1. Add tool definition in `open-sse/mcp-server/tools/` with Zod input schema + async handler
|
||||
2. Register in tool set (wired by `createMcpServer()`)
|
||||
3. Assign to appropriate scope(s)
|
||||
4. Write tests (tool invocation logged to the `mcp_tool_audit` table)
|
||||
4. Write tests (tool invocation logged to `mcp_audit` table)
|
||||
|
||||
### Adding a New A2A Skill
|
||||
|
||||
1. Create skill in `src/lib/a2a/skills/` (6 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report, list-capabilities)
|
||||
1. Create skill in `src/lib/a2a/skills/` (5 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report)
|
||||
2. Skill receives task context (messages, metadata) → returns structured result
|
||||
3. Register in `A2A_SKILL_HANDLERS` in `src/lib/a2a/taskExecution.ts`
|
||||
4. Expose in `src/app/.well-known/agent.json/route.ts` (Agent Card)
|
||||
@@ -384,7 +376,7 @@ Documentation must describe verified behavior, not plausible behavior.
|
||||
|
||||
### Adding a New Cloud Agent
|
||||
|
||||
1. Create agent class in `src/lib/cloudAgent/agents/` extending `CloudAgentBase` (4 already exist: codex-cloud, devin, jules, cursor-cloud)
|
||||
1. Create agent class in `src/lib/cloudAgent/agents/` extending `CloudAgentBase` (3 already exist: codex-cloud, devin, jules)
|
||||
2. Implement `createTask`, `getStatus`, `approvePlan`, `sendMessage`, `listSources`
|
||||
3. Register in `src/lib/cloudAgent/registry.ts`
|
||||
4. Add OAuth/credentials handling if needed (`src/lib/oauth/providers/`)
|
||||
@@ -395,7 +387,7 @@ Documentation must describe verified behavior, not plausible behavior.
|
||||
1. Create installer in `src/lib/services/installers/{name}.ts` modeled on `ninerouter.ts` (use `runNpm` from `installers/utils.ts` — no shell interpolation, hard rule #13).
|
||||
2. Register the service in `src/lib/services/bootstrap.ts` (add to `SERVICES[]` array and extend `buildSpawnArgsFactory()`).
|
||||
3. Add a DB seed row for the new service in `src/lib/db/migrations/` (`version_manager` table, `status='not_installed'`, `auto_start=0`).
|
||||
4. Create 8 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`, `auto-restart-adopted`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
|
||||
4. Create 7 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
|
||||
5. Verify `/api/services/` is in `LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`; add a test asserting `isLocalOnlyPath()` returns `true` for the new prefix if you add one (hard rule #17).
|
||||
6. Add a UI tab in `src/app/(dashboard)/dashboard/providers/services/tabs/` reusing `ServiceStatusCard`, `ServiceLifecycleButtons`, `ServiceLogsPanel`.
|
||||
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/openapi.yaml`.
|
||||
@@ -407,9 +399,6 @@ Documentation must describe verified behavior, not plausible behavior.
|
||||
- Eval suite: `src/lib/evals/` → docs: `docs/frameworks/EVALS.md`
|
||||
- Skill (sandbox): `src/lib/skills/` → docs: `docs/frameworks/SKILLS.md`
|
||||
- Webhook event: `src/lib/webhookDispatcher.ts` → docs: `docs/frameworks/WEBHOOKS.md`
|
||||
- Log-export destination: add `src/lib/logExport/destinations/<name>.ts` + one line in
|
||||
`src/lib/logExport/registry.ts` → docs: `docs/frameworks/LOG-EXPORT.md`. The runner, REST layer
|
||||
and dashboard form all read the registry, so nothing else changes.
|
||||
|
||||
---
|
||||
|
||||
@@ -422,7 +411,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 (16-factor scoring, 19 strategies) | `docs/routing/AUTO-COMBO.md` |
|
||||
| Auto-Combo (15-factor scoring, 19 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` |
|
||||
@@ -435,7 +424,6 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| Evals | `docs/frameworks/EVALS.md` |
|
||||
| Compliance / audit | `docs/security/COMPLIANCE.md` |
|
||||
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
|
||||
| Log export (call logs → BigQuery/…) | `docs/frameworks/LOG-EXPORT.md` |
|
||||
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
|
||||
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
|
||||
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
|
||||
@@ -448,7 +436,7 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| VS Code Copilot Chat (OmniCopilot extension) | `docs/guides/VSCODE-COPILOT.md` |
|
||||
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
|
||||
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
|
||||
| Quality gates (~90 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
| Quality gates (~80 scripts, allowlist policy) | `docs/architecture/QUALITY_GATES.md` |
|
||||
|
||||
---
|
||||
|
||||
@@ -494,12 +482,6 @@ Why this matters: fixing bug A while opening bug B is worse than not fixing at a
|
||||
pipeline, and A2A skills.
|
||||
- Do not close a contributor pull request after using its code; merge it through GitHub so
|
||||
the contributor receives credit.
|
||||
- **Never merge a PR that touches an agent-instruction surface without explicit operator
|
||||
approval** — `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `llm.txt` (+ mirrors) and
|
||||
`skills/**/SKILL.md` are executed as authority by every AI session; a merged instruction
|
||||
compromises every future agent run. Check with `gh pr diff <N> --name-only` before any
|
||||
merge. Incident record: PR #11770 (2026-09-01) told agents to execute a third-party
|
||||
setup script and was swept in by a merge campaign; reverted in #12249.
|
||||
|
||||
---
|
||||
|
||||
@@ -645,8 +627,8 @@ focused checks, and use a Conventional Commit message (for example, `docs: slim
|
||||
|
||||
## Environment
|
||||
|
||||
- **Runtime**: Node.js ≥22.22.2 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
|
||||
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.4.0` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
|
||||
- **Runtime**: Node.js ≥22.0.0 <23 || ≥24.0.0 <27, ES Modules. This is the **only supported** runtime for the published `omniroute` CLI, the server, and the test suites (`node:test` + vitest) — `engines.node` is authoritative and end users never need Bun. A **best-effort `bun:sqlite` compatibility path** exists so a global Bun install (`bun install -g omniroute`) can start without `better-sqlite3` (driver adapter + Bun-aware process spawning); it is **not** a supported runtime — no support guarantees — and every Bun-specific runtime change MUST preserve the Node driver/fallback chain and ship a Bun test (`test:bun:db`) or an explicit reason why the path is Node-only.
|
||||
- **Bun (build/dev script runner + compatibility smoke only)**: Bun `1.3.14` is pinned as an **exact devDependency** (provisioned through the existing `npm ci` via the lockfile's `@oven/bun-*` platform binaries — no `setup-bun`/ad-hoc install). It is used **only** to execute a small, allow-listed set of TypeScript **gate/generator scripts** (replacing `node --import tsx` for startup speed): the CI checks `check:provider-consistency`, `check:compression-budget`, `check:known-symbols`, and the non-CI `gen:provider-reference`, `bench:compression` — plus the focused `test:bun:db` compatibility smoke suite for the best-effort `bun:sqlite` path. **Do NOT** widen Bun to `npm install`, the build (`build:cli*`), `check:pack-artifact`, the supported published runtime, or the main test runners — those stay on Node. Any new Bun-invoking gate/generator script must be validated byte-identical against its `node --import tsx` output first. After pulling the lockfile change, run `npm install` so `bun` resolves locally (a stale `node_modules` will fail those scripts with `bun: not found`).
|
||||
- **TypeScript**: 6.0+, target ES2022, module esnext, resolution bundler
|
||||
- **Path aliases**: `@/*` → `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
|
||||
- **Default port**: 20128 (API + dashboard on same port)
|
||||
@@ -658,12 +640,12 @@ focused checks, and use a Conventional Commit message (for example, `docs: slim
|
||||
|
||||
## Quality Gates & Ratchets
|
||||
|
||||
OmniRoute has **~90 quality-gate scripts** (`scripts/check/` + `scripts/quality/`) wired
|
||||
OmniRoute has **~80 quality-gate scripts** (`scripts/check/` + `scripts/quality/`) wired
|
||||
across **9 gate-running jobs** in `.github/workflows/ci.yml` (`lint`, `quality-gate`,
|
||||
`quality-extended`, `docs-sync-strict`, `i18n-ui-coverage`, `i18n`, `pr-test-policy`,
|
||||
`test-vitest`, `sonarqube`), plus the `quality.yml` fast-gates job (PR→`release/**`) and
|
||||
5 quality nightly workflows (`nightly-property`, `nightly-resilience`,
|
||||
`nightly-llm-security`, `nightly-mutation`, `nightly-schemathesis`). Full inventory, per-job breakdown, and operational
|
||||
3 nightly workflows (`nightly-property`, `nightly-resilience`, `nightly-llm-security`;
|
||||
`nightly-mutation` once merged). Full inventory, per-job breakdown, and operational
|
||||
procedures are in [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALITY_GATES.md).
|
||||
|
||||
**Quick reference:**
|
||||
@@ -675,10 +657,6 @@ procedures are in [`docs/architecture/QUALITY_GATES.md`](docs/architecture/QUALI
|
||||
`npm run quality:ratchet -- --update` when a metric genuinely improves.
|
||||
- Job `test-vitest` runs `npm run test:vitest` (MCP tools, autoCombo, cache) — blocking.
|
||||
`test:vitest:ui` has been blocking since PR #7127.
|
||||
- **Velocity phase (2026-08-30 → v4.0)**: every numeric baseline is loosened by 20% and
|
||||
`--require-tighten` is advisory (`quality-baseline.json` → `_policy`); the nightly
|
||||
`baseline-headroom` job tracks how much of the budget is left in the issue
|
||||
"📈 Baseline headroom". See `docs/architecture/QUALITY_GATES.md` → "Velocity phase".
|
||||
|
||||
**Allowlist policy (short form):** Fix the cause; use the allowlist only for pre-existing
|
||||
violations you cannot fix in the same PR. Add a comment with justification + issue number.
|
||||
@@ -690,7 +668,7 @@ the stale-enforcement added in Fase 6A.3.
|
||||
## Hard Rules
|
||||
|
||||
1. Never commit secrets or credentials
|
||||
2. Never barrel-import from `localDb.ts` — import specific `src/lib/db/*` modules
|
||||
2. Never add logic to `localDb.ts`
|
||||
3. Never use `eval()` / `new Function()` / implied eval
|
||||
4. Never commit directly to `main`
|
||||
5. Never write raw SQL in routes — use `src/lib/db/` modules
|
||||
|
||||
19
CHANGELOG.md
19
CHANGELOG.md
@@ -97,10 +97,6 @@ _Living section — cycle opened at the v3.8.50 freeze (parallel-cycle model). B
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **security(streaming):** sanitize generic mid-stream error messages before emitting OpenAI,
|
||||
Responses, or Claude SSE failure frames and before diagnostic logging, while preserving raw
|
||||
failures for internal classification and keeping client disconnects out of provider failure state.
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
---
|
||||
@@ -720,6 +716,7 @@ _By commits in `ed2db6cb19..v3.8.50`, author identities consolidated via `.mailm
|
||||
- **[TS7] fix(types): normalize DuckDuckGo request messages** ([#9847](https://github.com/diegosouzapw/OmniRoute/pull/9847), original [#9797](https://github.com/diegosouzapw/OmniRoute/pull/9797)) — thanks @backryun
|
||||
- **[TS7] fix(types): expose SQLite transaction state** ([#9848](https://github.com/diegosouzapw/OmniRoute/pull/9848), original [#9796](https://github.com/diegosouzapw/OmniRoute/pull/9796)) — thanks @backryun
|
||||
- **[TS7] fix(types): validate default executor pool config** ([#9849](https://github.com/diegosouzapw/OmniRoute/pull/9849), original [#9795](https://github.com/diegosouzapw/OmniRoute/pull/9795)) — thanks @backryun
|
||||
- **[TS7] fix(types): preserve The Old LLM proxy contracts** ([#9850](https://github.com/diegosouzapw/OmniRoute/pull/9850), original [#9793](https://github.com/diegosouzapw/OmniRoute/pull/9793)) — thanks @backryun
|
||||
- **[TS7] fix(types): normalize Gemini Business credentials** ([#9851](https://github.com/diegosouzapw/OmniRoute/pull/9851), original [#9792](https://github.com/diegosouzapw/OmniRoute/pull/9792)) — thanks @backryun
|
||||
- **[TS7] fix(types): preserve Claude thinking body contracts** ([#9852](https://github.com/diegosouzapw/OmniRoute/pull/9852), original [#9791](https://github.com/diegosouzapw/OmniRoute/pull/9791)) — thanks @backryun
|
||||
- **fix(response): strip internal reasoning placeholder from all reasoning fields** ([#9853](https://github.com/diegosouzapw/OmniRoute/pull/9853), original [#9790](https://github.com/diegosouzapw/OmniRoute/pull/9790)) — thanks @adevwithpurpose
|
||||
@@ -891,7 +888,7 @@ _By commits in `ed2db6cb19..v3.8.50`, author identities consolidated via `.mailm
|
||||
- **fix(combo):** defer the known-context-overflow hard rejection for compressible requests so compression runs before the final context gate, instead of a raw-body estimate 400'ing generic Responses clients targeting a large model before OmniRoute can shrink it ([#10225](https://github.com/diegosouzapw/OmniRoute/issues/10225))
|
||||
- **fix(api):** deleting a manually-added custom model no longer tombstones a provider-synced model that shares its id. `DELETE /api/provider-models` is addressed by `provider` + `model` alone, so when both a custom row and a synced row existed for one id it removed both and wrote `isDeleted:true`. `replaceSyncedAvailableModelsForConnection` then filtered that id out of every subsequent re-import, so the provider could never resync — model sync kept reporting `added: N` while the catalog stayed empty and `/v1/models` never listed the model again, even though routing to it still worked. The custom row is now removed first and its presence is treated as the operator's intent, leaving the synced sibling importable; a synced-only delete still tombstones as before (#3199, #3782 unaffected) ([#10228](https://github.com/diegosouzapw/OmniRoute/pull/10228)) — thanks @Neuron-Mr-White
|
||||
- **Audio Bridge:** fix production transcription self-loop uploads so real audio reaches the configured STT provider instead of falling back to an unavailable-provider stub ([#10229](https://github.com/diegosouzapw/OmniRoute/pull/10229)).
|
||||
- **fix(api):** DeepSeek V4's native `max` reasoning tier is now reachable. DeepSeek accepts `reasoning_effort` `low`/`high`/`max` and maps `medium`/`xhigh` down to `high`, while OmniRoute's canonical vocabulary collapses `max` onto `xhigh` — so `{"effort":"max"}` silently resolved to `high` and the catalog never advertised a `max` tier (or its `<model>-max` variant). Following the existing `extendCodexGpt56EffortValues` precedent, the native tier is now preserved for `deepseek`/`ds` V4 models only; the global effort vocabulary is unchanged, routed namespaces (`openrouter/deepseek/…`, `oc/deepseek-v4-flash-free`) keep the canonical behavior, and an explicit client `reasoning_effort` still wins ([#10230](https://github.com/diegosouzapw/OmniRoute/pull/10230)) — thanks @Neuron-Mr-White
|
||||
- **fix(api):** DeepSeek V4's native `max` reasoning tier is now reachable. DeepSeek accepts `reasoning_effort` `low`/`high`/`max` and maps `medium`/`xhigh` down to `high`, while OmniRoute's canonical vocabulary collapses `max` onto `xhigh` — so `{"effort":"max"}` silently resolved to `high` and the catalog never advertised a `max` tier (or its `<model>-max` variant). Following the existing `extendCodexGpt56EffortValues` precedent, the native tier is now preserved for `deepseek`/`ds` V4 models only; the global effort vocabulary is unchanged, routed namespaces (`openrouter/deepseek/…`, `tllm/deepseek_v4`, `oc/deepseek-v4-flash-free`) keep the canonical behavior, and an explicit client `reasoning_effort` still wins ([#10230](https://github.com/diegosouzapw/OmniRoute/pull/10230)) — thanks @Neuron-Mr-White
|
||||
- **fix(providers):** FreeAIAPIKey now targets `api.freeaiapikey.com`, the host upstream names in its `410 endpoint_moved` response — every request through the provider was failing — and its catalog is resynced to the 10 models the live `/v1/models` actually serves ([#10233](https://github.com/diegosouzapw/OmniRoute/pull/10233))
|
||||
- **fix(providers):** MonsterAPI's deprecation now actually applies — the flag was written as `isDeprecated`, a key no consumer or schema reads, so the provider kept rendering as healthy in the dashboard, the onboarding wizard and the generated provider reference ([#10234](https://github.com/diegosouzapw/OmniRoute/pull/10234))
|
||||
- **fix(cliproxy):** read platform/arch at runtime via `os.platform()`/`os.arch()` in `binaryManager` so the embedded installer selects the Windows/ARM assets even when the release bundle is built on a Linux runner (fixes #10244)
|
||||
@@ -3003,6 +3000,7 @@ _Living section — regenerated 2026-07-19 from all 306 cycle commits (bump 2c62
|
||||
- Stream model health probes for slow providers ([#7377](https://github.com/diegosouzapw/OmniRoute/pull/7377)) — thanks @JxnLexn
|
||||
- Refresh NVIDIA free metadata and detect catalog drift ([#7378](https://github.com/diegosouzapw/OmniRoute/pull/7378)) — thanks @JxnLexn
|
||||
- Reject invalid output token budgets ([#7379](https://github.com/diegosouzapw/OmniRoute/pull/7379)) — thanks @JxnLexn
|
||||
- Honor provider proxies for The Old LLM Vercel blocks ([#7380](https://github.com/diegosouzapw/OmniRoute/pull/7380)) — thanks @JxnLexn
|
||||
- Restore proxy navigation and sidebar accordion state ([#7381](https://github.com/diegosouzapw/OmniRoute/pull/7381)) — thanks @JxnLexn
|
||||
- Expose proxy controls for no-auth providers ([#7419](https://github.com/diegosouzapw/OmniRoute/pull/7419)) — thanks @JxnLexn
|
||||
- Add reasoning-based model and effort routing ([#7607](https://github.com/diegosouzapw/OmniRoute/pull/7607)) — thanks @JxnLexn
|
||||
@@ -3444,7 +3442,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
- **fix(cli):** `omniroute serve` no longer hangs silently on a readiness timeout ([#6321](https://github.com/diegosouzapw/OmniRoute/issues/6321)) — the child server's stdout was piped to `"ignore"` whenever `--log`/`OMNIROUTE_SHOW_LOG` wasn't set (the default), discarding any debug output, and `runWithSupervisor`'s `waitForServer(...).then((up) => { if (up) {...} })` had no `else` branch, so a boot that never became ready produced zero further output after "⏳ Starting server...". Stdout is now buffered alongside stderr (`ServerSupervisor.getRecentLog()`), and a timeout prints a clear diagnostic plus the buffered output instead of staying silent. Does not by itself explain why boot never completes on a given machine — see the issue for further reproduction. Regression guard: `tests/unit/cli-serve-readiness-timeout-6321.test.ts`.
|
||||
- **fix(pricing):** Pricing Sync dashboard no longer stuck on "Next Sync: Never" / "Synced Models: 0" ([#6325](https://github.com/diegosouzapw/OmniRoute/issues/6325)) — `pricingSync.ts` kept sync state (`lastSyncTime`, `lastSyncModelCount`) in module-level vars, but the background periodic sync (`instrumentation-node.ts`) and the dashboard status route (`/api/pricing/sync`) each import the module from separate Next.js standalone webpack chunks, giving each its own independent state; `getSyncStatus()` read the (empty) API-route instance's vars. Sync status is now additionally persisted to a new `pricing_sync_status` `key_value` namespace and `getSyncStatus()` falls back to it when the local module instance never ran a sync itself. Regression guard: `tests/unit/pricing-sync-cross-instance.test.ts`.
|
||||
- **fix(api):** stop spuriously 403-ing "Invalid request origin" on `POST /api/providers/health-autopilot/actions` for Docker/LAN dashboard requests ([#6277](https://github.com/diegosouzapw/OmniRoute/issues/6277)) — the route carried a duplicate per-route `validateBrowserMutationOrigin` check re-added by the v3.8.42 release squash after PR #5278 centralized origin enforcement in the authz pipeline; the pipeline strips `PEER_IP_HEADER` before forwarding, so the stale duplicate check could no longer resolve the LAN "direct-local-host" candidate and rejected legitimate same-origin LAN mutations (e.g. clicking "remove cooldown" when accessed via a LAN IP). Removed the duplicate check — origin validation is now solely enforced by the centralized pipeline check, which already handles this case correctly. Regression guard: `tests/unit/serial/provider-health-autopilot.test.ts`.
|
||||
- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`.
|
||||
- **fix(resilience):** a bare, unrecognized `403` from a no-credential (`authType:"none"`) provider like mimocode or theoldllm no longer permanently bans the connection ([#6315](https://github.com/diegosouzapw/OmniRoute/issues/6315), [#6345](https://github.com/diegosouzapw/OmniRoute/issues/6345)) — `classifyProviderError()`'s 403 branch only exempted `apikey` providers from the terminal `FORBIDDEN` classification, so these free/stateless proxies (no real account/credential to revoke) fell through to `FORBIDDEN` on the first unmatched 403 and got `isActive:false, testStatus:"banned"` with no cooldown or retry. The exemption now also covers `authType:"none"` providers, returning `null` (recoverable) so the existing connection-cooldown/retry layer handles it. Regression guard: `tests/unit/errorClassifier-noauth-403-6315.test.ts`.
|
||||
- **fix(providers):** the **Auggie (Augment CLI)** executor no longer fails on Windows with `spawn EINVAL` ([#6304](https://github.com/diegosouzapw/OmniRoute/issues/6304)) — the global-npm install exposes `auggie` as a `.cmd` shim, which Node's `child_process.spawn` cannot launch on win32 without `shell: true`. Both spawn sites (streaming + the `auggie --version` test) now go through a shared `buildAuggieSpawnOptions()` that sets `shell: process.platform === "win32"`; the argv (built by `buildAuggieArgs()` with a registry-validated `model` and a trailing `--` end-of-options marker) is unchanged, so the argument-injection surface stays closed on non-Windows. Regression guard: `tests/unit/auggie-win32-spawn-6304.test.ts`.
|
||||
- **fix(api):** the dashboard **"Test model"** action is now a clean connection test ([#6240](https://github.com/diegosouzapw/OmniRoute/issues/6240)) — `modelTestRunner` sent its probe request without an explicit compression override, so whenever the operator's global `compression.enabled` flag was on the test call inherited compression (and any Output-Styles system prompt), polluting the result. The internal test requests now send `X-OmniRoute-Compression: off`, and `chatCore` honors an explicit `off` header even when `compression.enabled` is globally true. Regression guards: `tests/unit/model-test-runner-compression-off-6240.test.ts`, `tests/integration/test-model-compression-off-6240.test.ts`.
|
||||
- **fix(startup):** an update/restart could crash the whole server at boot with `TypeError: Cannot create property 'message' on string 'Database closed'`, masking the real failure and 500-ing every request until manually restarted ([#6560](https://github.com/diegosouzapw/OmniRoute/issues/6560), plausibly the root cause of #6594's post-upgrade 500) — `driverFactory.ts::preInitSqlJs()` cached its sql.js WASM adapter per file path in a `globalThis`-backed map for idempotency, but never checked whether the cached adapter had since been closed (e.g. by `gracefulShutdown`/`resetDbInstance` racing a reload); reusing that dead handle made the very next query throw sql.js's own bare string `"Database closed"` (not an `Error`) straight out of `instrumentation-node.ts`'s previously-unguarded `ensureDbInitialized()` call, and Next.js's internal `registerInstrumentation()` wrapper unconditionally does `err.message = ...` on whatever `register()` rejects with — assigning `.message` on a primitive string throws in strict mode, so the secondary `TypeError` is what actually crashed the process. Fixed in two parts: `preInitSqlJs()` now evicts a closed cached adapter and creates a fresh one instead of returning it; a new `ensureDbReadyForBoot()` wraps the DB-init call, normalizes any non-Error throw via `normalizeBootError()`, and retries once specifically for a transient "database closed" message (now succeeding against the fresh adapter) before re-throwing anything else as a real `Error`. Regression guard: `tests/unit/instrumentation-database-closed-6560.test.ts`.
|
||||
@@ -4000,6 +3998,7 @@ Thanks to everyone whose work landed in v3.8.45:
|
||||
- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. ([#6001](https://github.com/diegosouzapw/OmniRoute/pull/6001)) (thanks @KooshaPari)
|
||||
- **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). ([#6007](https://github.com/diegosouzapw/OmniRoute/pull/6007)) (thanks @KooshaPari)
|
||||
- **feat(autoCombo):** add a latency/speed-optimized routing mode (shared `rankBySpeed` scoring core) plus the `omniroute_pick_fastest_model` MCP tool. ([#6011](https://github.com/diegosouzapw/OmniRoute/pull/6011)) (thanks @KooshaPari)
|
||||
- **feat(providers):** refresh The Old LLM (Free) model catalog ([#5181](https://github.com/diegosouzapw/OmniRoute/issues/5181)) — seed the current free `/api/chatgpt` tier (GPT-5/5.1/5.2/5.3/5.4, o3/o4-mini, Gemini 3 Pro / 2.5 Pro / 2.0 Flash / 1.5 Flash, Claude 4.6 Opus/Sonnet & 4.5 Haiku, GPT-4o, Grok 4, DeepSeek V3/R1, Sonar Pro) while keeping the legacy alias IDs for saved-preference compatibility. Also fixes a latent routing bug: `mapModel()` now passes known upstream IDs through unchanged, so Gemini/o-series/Grok/DeepSeek/Sonar models no longer silently collapse onto `GPT_5_4`. Regression guard: `tests/unit/theoldllm-model-refresh-5181.test.ts`. (thanks @WslzGmzs)
|
||||
- **feat(resilience):** surface Codex **banked reset credits** per connected account ([#5199](https://github.com/diegosouzapw/OmniRoute/issues/5199)) — the Codex quota parsers (`buildCodexUsageQuotas`, `parseCodexUsageResponse`) now additively read `rate_limit_reset_credits.available_count` (+ optional `rate_limit_reached_type`) from the `/wham/usage` payload OmniRoute already fetches, and the provider-limits dashboard renders a **"Banked Reset Credits"** row when a positive count is present. Display-only and **fail-open** — the field is eligibility-gated, so accounts without it are unaffected (parsers never throw on absent/garbage shapes); redemption (an unofficial mutating endpoint) is intentionally out of scope. Regression guard: `tests/unit/codex-banked-reset-credits-5199.test.ts` (8). (thanks @ofekbetzalel)
|
||||
- **feat(providers):** add sign-up geo-restriction notices for **SenseNova** and **StepFun** ([#5462](https://github.com/diegosouzapw/OmniRoute/issues/5462)) — the provider add-form now warns that SenseNova's console appears to require a Chinese (+86) phone number with no documented international path, and that StepFun's default endpoint is its China platform while a global StepFun Open Platform (`platform.stepfun.ai`, operated by Sparkling AI Pte. Ltd., Singapore) with email/Google/Discord login exists for international users. Informational `notice` only — neither provider is disabled. Regression guard: `tests/unit/regional-provider-cn-notices-5462.test.ts`. (thanks @chirag127)
|
||||
- **feat(usage):** add on-demand period-scoped usage-data reset (Settings → System Storage) with a purge API and time-window selector. ([#5831](https://github.com/diegosouzapw/OmniRoute/pull/5831))
|
||||
@@ -5645,6 +5644,7 @@ Thanks to everyone whose work landed in v3.8.43:
|
||||
- **fix(sse): map `reasoning_effort` to DeepSeek V4's native `{high, max}` vocabulary** — DeepSeek V4 only understands `high`/`max` reasoning levels, so other `reasoning_effort` values are mapped onto its native vocabulary instead of being rejected. ([#4219](https://github.com/diegosouzapw/OmniRoute/pull/4219))
|
||||
- **fix(glm): default `max_tokens` and an extended timeout for GLM-5.2+ thinking** — GLM-5.2+ thinking responses are slow and need headroom, so OmniRoute now sets a sensible default `max_tokens` and a longer timeout for them. ([#4255](https://github.com/diegosouzapw/OmniRoute/pull/4255) — thanks @dhaern)
|
||||
- **fix(antigravity): default `includeThoughts` for modern Gemini models** — modern Gemini models on the Antigravity path now default to including thoughts so reasoning isn't silently dropped. ([#4180](https://github.com/diegosouzapw/OmniRoute/pull/4180) — thanks @dhaern)
|
||||
- **fix(provider-registry): add correct `contextLength` to theoldllm models** — fills in accurate context-window sizes for theoldllm's models. ([#4184](https://github.com/diegosouzapw/OmniRoute/pull/4184) — thanks @herjarsa)
|
||||
- **fix(models): expose combo model token limits** — `/v1/models` now reports token limits for combo models. ([#4189](https://github.com/diegosouzapw/OmniRoute/pull/4189) — thanks @megamen32)
|
||||
- **fix(combo): keep the passthrough quota fallback scoped** — prevents the passthrough quota fallback from leaking across unrelated targets. ([#4194](https://github.com/diegosouzapw/OmniRoute/pull/4194) — thanks @Svetznaniy33)
|
||||
- **fix(combo): opt proactive-fallback compression into the TV1 bail-out (no silent target drop)** — proactive-fallback compression now participates in the TV1 bail-out so a target is never silently dropped. ([#4228](https://github.com/diegosouzapw/OmniRoute/pull/4228))
|
||||
@@ -6304,6 +6304,7 @@ Thanks to everyone whose work landed in v3.8.43:
|
||||
- **fix(catalog):** Codex CLI model-catalog refresh no longer errors — `GET /v1/models` now returns a top-level `models: []` array for Codex clients (detected via the `originator` / `user-agent` = `codex_*` headers it sends on `GET /v1/models?client_version=...`), so `codex_models_manager` stops failing to decode the OpenAI-standard response and no longer logs `failed to refresh available models` on every startup. The array is intentionally empty: Codex replaces its built-in per-model agent prompt (`base_instructions`, ~21k chars) with whatever a populated entry carries for the selected model, so emitting our catalog would break Codex's agent behaviour — an empty list keeps Codex on its built-in model info (same inference as before, minus the error). Non-Codex OpenAI clients receive the unchanged `{object,data}` response. ([#3481](https://github.com/diegosouzapw/OmniRoute/pull/3481) — thanks @diegosouzapw)
|
||||
- **fix(provider):** Cursor's Responses-API-shaped bodies on `/chat/completions` are detected and handled — a body with `input` but no `messages` is now classified as `openai-responses` (instead of forcing `openai` and building from undefined `messages` → upstream 400); standard OpenAI clients are unaffected by the `messages===undefined` guard. ([#3490](https://github.com/diegosouzapw/OmniRoute/pull/3490) — thanks @borodulin)
|
||||
- **fix(sse):** numeric provider IDs normalized to strings across 4 more surfaces — extends #3427 to the Responses-API SSE passthrough (`response_id`/`item_id`/`call_id`), the buffered/flush path in `stream.ts`, the dedup-key builders, and `sseParser.ts`, preventing `undefined` lookups when IDs arrive as numbers. ([#3451](https://github.com/diegosouzapw/OmniRoute/pull/3451) — thanks @disafronov)
|
||||
- **fix(theoldllm):** `X-Request-Token` generated server-side, dropping the Playwright dependency — replicates the site's client `rie()` token (djb2 hash + `oldllm-client-2026` seed + UA prefix + 8-hex `crypto.randomUUID` suffix) directly, so The Old LLM no longer needs a headless browser to mint tokens. ([#3491](https://github.com/diegosouzapw/OmniRoute/pull/3491) — thanks @borodulin / @diegosouzapw)
|
||||
- **fix(combo):** parallel pre-screen + circuit-breaker fast-exit for priority combos — provider profiles and model availability for all targets are pre-screened concurrently (max 5), and targets whose circuit breaker is OPEN are skipped immediately, reducing first-token latency on multi-target priority combos. ([#3169](https://github.com/diegosouzapw/OmniRoute/pull/3169) — thanks @pizzav-xyz)
|
||||
- **fix(authz):** URL-tokenized client endpoints (`/api/v1/vscode/<key>/...`) authenticate again when the caller sends its own non-OmniRoute `Authorization` header — a non-`Bearer <token>` header (e.g. VS Code Copilot's own, or an empty `Bearer `) no longer short-circuits auth; it falls through to the path-scoped URL token (still validated downstream), instead of 401'ing under `REQUIRE_API_KEY=true`. ([#3504](https://github.com/diegosouzapw/OmniRoute/pull/3504) — thanks @zhiru / @diegosouzapw)
|
||||
- **fix(playground):** the dashboard provider Test playground works under `REQUIRE_API_KEY=true` — it previously sent the **masked** key (`sk-xxxx****yyyy`) as a bearer (always invalid → 401). It now authenticates via the dashboard session and sends only the key **id** (`x-omniroute-playground-key-id`); the gateway resolves the secret server-side, honored **only** for an authenticated session and never putting the key secret on the wire. ([#3503](https://github.com/diegosouzapw/OmniRoute/pull/3503) — thanks @zhiru / @diegosouzapw)
|
||||
@@ -6336,7 +6337,7 @@ Thanks to everyone whose work landed in v3.8.43:
|
||||
|
||||
- **fix(translator):** Vertex AI tool calls no longer fail with `400 Unknown name "id"` — the OpenAI-style `id` field is stripped from `functionCall`/`functionResponse` parts for `vertex`/`vertex-partner`; the public Gemini API still receives `id` as required for Gemini 3+ signature matching. ([#3457](https://github.com/diegosouzapw/OmniRoute/pull/3457) — thanks @nullbytef0x / @diegosouzapw)
|
||||
- **fix(claude):** Claude Code `claude-opus-4-8` tool calls no longer break with `tool call could not be parsed` — OmniRoute no longer force-injects `interleaved-thinking` / `advanced-tool-use` / `effort` beta flags the client never negotiated; clients sending their own `anthropic-beta` header control those betas themselves. ([#3458](https://github.com/diegosouzapw/OmniRoute/pull/3458) — thanks @Forcerecon / @diegosouzapw)
|
||||
- **fix(catalog):** imported/custom models on no-auth providers now appear in `GET /api/v1/models` and the Playground model selector — the eligibility gate required a DB connection row which no-auth providers never have, silently dropping every imported model for them. ([#3463](https://github.com/diegosouzapw/OmniRoute/pull/3463) — thanks @tjengbudi / @diegosouzapw)
|
||||
- **fix(catalog):** imported/custom models on no-auth providers (e.g. The Old LLM) now appear in `GET /api/v1/models` and the Playground model selector — the eligibility gate required a DB connection row which no-auth providers never have, silently dropping every imported model for them. ([#3463](https://github.com/diegosouzapw/OmniRoute/pull/3463) — thanks @tjengbudi / @diegosouzapw)
|
||||
- **fix(browser):** optional `cloakbrowser` import no longer causes bundle errors when the package is absent — the import is now wrapped in a dynamic require so the build succeeds on environments that don't install the optional dep. ([#3460](https://github.com/diegosouzapw/OmniRoute/pull/3460) — thanks @rdself)
|
||||
- **fix(claude-web):** claude-web session handling cleanup — corrects an edge case where session cookies were not properly refreshed after a Turnstile challenge, and removes stale wrapper code left over from the provider split. ([#3449](https://github.com/diegosouzapw/OmniRoute/pull/3449) — thanks @androw)
|
||||
- **fix(analytics):** SQL named params are now scoped per query context — a shared params object was being mutated across concurrent analytics queries, causing `SQLITE_MISUSE: named parameter not found` errors under load. ([#3447](https://github.com/diegosouzapw/OmniRoute/pull/3447) — thanks @ReqX)
|
||||
@@ -6512,7 +6513,8 @@ Thanks to everyone whose work landed in v3.8.14:
|
||||
|
||||
- **fix(dashboard):** Agent Bridge page (`/dashboard/tools/agent-bridge`) no longer crashes with "Internal Server Error" — the page replaced its well-shaped state with the raw `/api/tools/agent-bridge/state` response (`{ server, agents }`), leaving `serverState` undefined and throwing `Cannot read properties of undefined (reading 'running')`. A shared `normalizeAgentBridgeState()` now maps the route shape into the page contract (incl. `server.certExists → certTrusted`) and always returns safe defaults, used by both the SSR loader and the polling hook. (#3318 — thanks @tycronk20)
|
||||
- **fix(codex):** strip client-only params (`prompt_cache_retention`, `safety_identifier`, `user`) on the native `codex/` `/v1/responses` passthrough — Codex upstream rejects them with `400 Unsupported parameter`, which broke Factory Droid and any client injecting those fields. The chat-completions path already stripped them; the responses→responses passthrough now does too. (#3317 — thanks @tycronk20)
|
||||
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
|
||||
- **fix(theoldllm):** stop the `[502]: Body is unusable: Body has already been read` error on the cached-token path — the executor read the same upstream `Response` body with `.text()` twice; it now reads it once and only re-reads after a token-rejection refetch. (#3296 — thanks @onizukashonan14-png)
|
||||
- **fix(dashboard):** keep no-auth providers (opencode, duckduckgo-web, theoldllm, veoaifree-web) visible under the "Show configured only" filter — they never create a connection row (`stats.total === 0`) but are always usable and already appear in `/v1/models`, so the filter now treats `displayAuthType === "no-auth"` as configured. (#3290 — thanks @uniQta)
|
||||
- **fix(dashboard):** refresh the connection list after a Codex/Claude/Gemini auth import — the import modals called `fetchData()` (which only reloads provider metadata), so a freshly-imported connection stayed invisible until a manual reload; they now call `fetchConnections()`. ([#3320](https://github.com/diegosouzapw/OmniRoute/pull/3320) — thanks @zhiru)
|
||||
- **fix(cli):** `omniroute update` no longer always fails on a global install — `getCurrentVersion()` and `createBackup()` now resolve `package.json`/`bin` relative to the script (`import.meta.url`) instead of `process.cwd()` (the user's working dir on a global npm/brew install → _"Could not determine current version"_), and the backup copies the `cli` directory with `cpSync({recursive:true})` instead of `copyFileSync`, which threw a swallowed `EISDIR` → _"Failed to create backup. Aborting"_. (#3295 — thanks @uniQta)
|
||||
- **fix(sse):** harden the passthrough stream against empty upstream responses — emit a synthetic retry chunk on an empty `choices: []` (fixes a Copilot Chat crash) and log empty post-`tool_calls` completions; also registers **MiniMax M3** (1M context) across 8 provider tiers. ([#3297](https://github.com/diegosouzapw/OmniRoute/pull/3297), #3110 — thanks @wilsonicdev)
|
||||
@@ -6604,6 +6606,7 @@ Thanks to everyone whose work landed in v3.8.12:
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **theoldllm:** add The Old LLM — a free, Playwright-backed provider with dual-mode operation (cached browser token + direct fetch) bridged through a Vercel relay (#3217 — thanks @oyi77)
|
||||
- **codex:** add Codex login via OpenAI's browser-driven device authorization flow, exposed as a shareable "Adicionar Externo" public link (`/connect/codex/{token}`) so a third party can complete the OpenAI device login without dashboard access (#3195 — thanks @zhiru)
|
||||
- **proxy:** per-connection proxy distribution — `proxy_enabled` DB schema + Zod-validated resolution backend, automatic proxy-fallback selection when provider validation hits a network error, and a dashboard UI with per-connection toggles and a tag-filtered "Distribute Proxies" button (#3170, #3171, #3172 — thanks @pizzav-xyz)
|
||||
- **api:** `/v1/images/generations` and `/v1/images/edits` now resolve a bare combo/alias model name (e.g. `image`) to its single image target, and `/v1/images/edits` forwards multipart edits to custom OpenAI-compatible providers' `{base_url}/images/edits` (also accepting JSON/data-URL edit input) instead of rejecting everything but chatgpt-web (#3214, #3215 — thanks @ngocquynh85)
|
||||
|
||||
@@ -73,9 +73,6 @@ npm run dev
|
||||
npm run build # next build → .build/next/ then assembleStandalone → dist/
|
||||
npm run start
|
||||
|
||||
# Fast backend/API-only compile for contributor changes
|
||||
npm run build:contributor
|
||||
|
||||
# Release build (clean rebuild + HEAD sentinel — required for deploy)
|
||||
npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA
|
||||
|
||||
@@ -83,10 +80,6 @@ npm run build:release # rm -rf .build dist && build + writes dist/BUILD_SHA
|
||||
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
|
||||
```
|
||||
|
||||
The contributor build performs compile-only validation: it does not assemble the standalone
|
||||
distribution or build optional native packaging assets. Use the regular production build when
|
||||
you need to validate the shippable bundle.
|
||||
|
||||
### Build Output Layout
|
||||
|
||||
| Directory | Contents | Tracked |
|
||||
@@ -107,11 +100,6 @@ npm run build
|
||||
`npm run build:release` additionally cleans both directories first and writes
|
||||
`dist/BUILD_SHA` (= `git rev-parse --short HEAD`) as a deploy integrity sentinel.
|
||||
|
||||
`npm run build:contributor` uses the backend-only build profile. It temporarily stubs
|
||||
dashboard UI files while building, keeps API route handlers, and restores the original files
|
||||
after the build. Use `npm run build` for changes that affect the dashboard UI or for full
|
||||
release validation; the contributor profile is not a replacement for the release build.
|
||||
|
||||
> **VPS deploy note:** the remote image directory `/usr/lib/node_modules/omniroute/app/`
|
||||
> is unchanged. The deploy skills rsync the contents of `dist/` into it.
|
||||
> Only the in-repo build output path moved (`app/` → `dist/`).
|
||||
@@ -177,13 +165,6 @@ npm run test:all
|
||||
# Single test file (Node.js native test runner — most tests use this)
|
||||
node --import tsx/esm --test tests/unit/your-file.test.ts
|
||||
|
||||
# Only the unit tests impacted by your change (same TIA selector as the CI gate, #8084)
|
||||
npm run test:scoped # changes in the last commit (or the working tree)
|
||||
npm run test:scoped:staged # staged changes only — pairs well with a pre-commit run
|
||||
npm run test:scoped:full # rebuild the import-graph map first (after adding/moving files)
|
||||
# Exit 1 + "run the full suite" means a hub file (tsconfig, package.json, …) or an
|
||||
# unmapped source changed — the selector fails safe, it never silently skips.
|
||||
|
||||
# Vitest (MCP server, autoCombo, cache)
|
||||
npm run test:vitest
|
||||
|
||||
@@ -320,7 +301,7 @@ src/ # TypeScript (.ts / .tsx)
|
||||
open-sse/ # @omniroute/open-sse workspace
|
||||
├── executors/ # 89 executor implementation modules
|
||||
├── handlers/ # 11 request handlers (chat, responses, embeddings, images, etc.)
|
||||
├── mcp-server/ # MCP server (110 unique tools, 3 transports, 33 scopes)
|
||||
├── mcp-server/ # MCP server (107 unique tools, 3 transports, 32 scopes)
|
||||
├── services/ # 178 top-level services (combo, autoCombo, rateLimitManager, etc.)
|
||||
├── translator/ # Format translators (OpenAI ↔ Claude ↔ Gemini ↔ Responses ↔ Ollama)
|
||||
├── transformer/ # Responses API transformer
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ── Multi-stage Dockerfile for Native Bun Runtime (web-latest-bun) ───────────
|
||||
FROM oven/bun:1.4.0-slim AS base
|
||||
FROM oven/bun:1.3.14-slim AS base
|
||||
WORKDIR /app
|
||||
|
||||
RUN apt-get update \
|
||||
@@ -58,7 +58,7 @@ ENV NODE_ENV=production
|
||||
RUN bun run --quiet build
|
||||
|
||||
# ── Runner Base stage (100% Bun Native Production Runtime) ──────────────────
|
||||
FROM oven/bun:1.4.0-slim AS runner-base
|
||||
FROM oven/bun:1.3.14-slim AS runner-base
|
||||
|
||||
LABEL org.opencontainers.image.title="omniroute" \
|
||||
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint (Bun Native)" \
|
||||
|
||||
64
README.md
64
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 354 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 354 AI providers · 150+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 351 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 351 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
</div>
|
||||
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **446 free-tier entries across 38 recurring pool keys** and computes the token headline from the **20 pools with a published positive monthly budget**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **445 free-tier entries across 39 recurring pool keys** and computes the token headline from the **20 pools with a published positive monthly budget**, deduplicated by shared pool. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.51B free tokens per month steady, up to ~2.13B in the first month with signup credits, from 38 documented recurring pool keys covering 446 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 20 recurring pools with a published positive monthly token budget; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, LLM7 150M, Nara 150M, Gemini 60M and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.51B free tokens per month steady, up to ~2.13B in the first month with signup credits, from 39 documented recurring pool keys covering 445 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 20 recurring pools with a published positive monthly token budget; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, LLM7 150M, Nara 150M, Gemini 60M and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
|
||||
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
|
||||
>
|
||||
@@ -49,11 +49,11 @@
|
||||
|
||||
[](https://discord.gg/U47eFqAXCn)
|
||||
[](https://t.me/omnirouteOficial)
|
||||
[](https://chat.whatsapp.com/FvuCbrpZmQ6I85n2vW5QIC?s=cl&p=a&mlu=4)
|
||||
[](https://chat.whatsapp.com/KWgatljAjmbELQory59Oti?s=cl&p=a&mlu=4)
|
||||
[](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
[](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz)
|
||||
[](https://omniroute.online)
|
||||
|
||||
**Questions, provider tips, roadmap & support → [Discord](https://discord.gg/U47eFqAXCn) · [Telegram](https://t.me/omnirouteOficial) · WhatsApp [🌍 Global](https://chat.whatsapp.com/FvuCbrpZmQ6I85n2vW5QIC?s=cl&p=a&mlu=4) / [🇧🇷 Brasil](https://chat.whatsapp.com/KWgatljAjmbELQory59Oti?s=cl&p=a&mlu=4) / [Portal](https://portal.sthub.com.br/communities/groups/st-hub/channels/Omniroute-World-8kRjmK)**
|
||||
**Questions, provider tips, roadmap & support → [Discord](https://discord.gg/U47eFqAXCn) · [Telegram](https://t.me/omnirouteOficial) · WhatsApp [🌍 Global](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) / [🇧🇷 Brasil](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz)**
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
|
||||
| ------------------------- | :-----: | :-----------------------: | :---------: |
|
||||
| 🌐 Providers | 290 | **352** | more queued |
|
||||
| 🌐 Providers | 290 | **350** | more queued |
|
||||
| 🧠 Unique chat model IDs | 1185 | **1312** | — |
|
||||
| 🖼️ Modality Bridge | — | 🆕 vision + audio + video | — |
|
||||
| 📡 Radar free catalog | — | 🆕 opt-in | — |
|
||||
@@ -101,7 +101,7 @@
|
||||
<tr>
|
||||
<td align="right"><b>⚙️ Features</b></td>
|
||||
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
|
||||
<td align="center"><a href="#-352-ai-providers--154-catalog-marked-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-351-ai-providers--154-catalog-marked-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI & MCP</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 354 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 354 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 351 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 351 providers · up to 95% token savings on eligible workloads · $0 to start with 90+ free tiers and 56 recurring/keyless free-forever providers · 35 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -332,8 +332,6 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
|
||||
<tr><td align="left" nowrap><code>auto/cheap</code></td><td align="left">💰 Cheapest per token first</td></tr>
|
||||
<tr><td align="left" nowrap><code>auto/offline</code></td><td align="left">🔋 Most quota / rate-limit headroom first</td></tr>
|
||||
<tr><td align="left" nowrap><code>auto/smart</code></td><td align="left">🔭 Quality-first + 10% exploration to discover better models</td></tr>
|
||||
<tr><td align="left" nowrap><code>auto/lkgp</code></td><td align="left">📌 Explicit last-known-good-provider stickiness</td></tr>
|
||||
<tr><td align="left" nowrap><code>auto/chaos</code></td><td align="left">🧪 Fault-injection weights for resilience testing (chaos engineering)</td></tr>
|
||||
</table>
|
||||
|
||||
##
|
||||
@@ -431,7 +429,7 @@ All **19** strategies — mix & match per combo step:
|
||||
<tr>
|
||||
<td align="center">17</td>
|
||||
<td nowrap><code>auto</code></td>
|
||||
<td>16-factor live scoring across every connection 🤖</td>
|
||||
<td>15-factor live scoring across every connection 🤖</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center">18</td>
|
||||
@@ -445,13 +443,13 @@ All **19** strategies — mix & match per combo step:
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<sub>The Auto-Combo engine scores every candidate on **16 factors** (health, quota, cost, latency, task fit, quality, session availability…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
|
||||
<sub>The Auto-Combo engine scores every candidate on **15 factors** (health, quota, cost, latency, task fit, quality, session availability…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
|
||||
|
||||
##
|
||||
|
||||
### 🧱 Resilience is built in (3 independent layers)
|
||||
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 8× / API-key 12× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 10× / API-key 15× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
|
||||
<sub>📖 [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)</sub>
|
||||
|
||||
@@ -463,7 +461,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 354 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 351 providers, 90+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 43 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -550,7 +548,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🗜️ Compression hardening** — default-on inflation guard, Caveman packs for DE / FR / JA + Chinese (wényán), RTK filters for Gradle & .NET. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
- **💸 Honest flat-rate cost** — subscription / coding-plan providers read **$0** in cost analytics; budget, quota & routing keep estimating. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **⚖️ Quota-Share routing** — split a shared account's quota fairly across pooled keys, work-conserving so idle slices are lent out. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
|
||||
- **🤖 One-command CLI/agent setup** — 13 registered `setup-*` commands; `omniroute run` launches 7 CLIs (Claude Code, Codex, Aider, Goose, OpenCode, Qwen Code, Gemini CLI); `omniroute configure` supports 10 targets with an interactive provider+model picker and per-context favorites. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
- **🤖 One-command CLI/agent setup** — 12 registered `setup-*` commands; `omniroute run` launches 7 CLIs (Claude Code, Codex, Aider, Goose, OpenCode, Qwen Code, Gemini CLI); `omniroute configure` supports 9 targets with an interactive provider+model picker and per-context favorites. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
- **🛰️ Remote mode** — drive a remote OmniRoute with scoped tokens (`connect` / `contexts` / `tokens`) + an `antigravity` OAuth helper for VPS installs. → [Remote Mode](docs/guides/REMOTE-MODE.md)
|
||||
- **🧭 Smarter auto-routing** — `auto/<category>:<tier>` combos, **Fusion** (model panel + judge), task-aware routing, per-request model / mode / USD-budget overrides. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **🗜️ Pluggable compression** — 12 composable engines + Compression Studios: LLMLingua-2, two-tier Ultra, omniglyph, per-step fidelity gate, GCF v3.2, drag-reorder editor. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
@@ -561,10 +559,9 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Magnific, Adobe Firefly, Segmind, and speech providers such as ElevenLabs. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — cloud agents (Codex Cloud, Cursor, Devin, Jules), Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **352-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **351-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
- **🧩 Also in the box** — plugin framework + marketplace, Omni/Agent/GitHub skills frameworks, Obsidian vault integration (22 MCP tools), OpenAI-compatible Batch & Files APIs, semantic response cache, gamification with leaderboards, ACP agent discovery (15 built-in agents), scheduled log export to BigQuery, `auto/chaos` fault injection, a Telegram bot bridge, an in-app version manager and LMArena-ELO free-provider rankings. → [Docs](docs/README.md)
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -615,7 +612,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
<b>+ also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
|
||||
</div>
|
||||
|
||||
<sub>📖 Per-tool setup for all 36 tools (26 CLI Code's + 10 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
<sub>📖 Per-tool setup for all 35 tools (26 CLI Code's + 9 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -634,7 +631,7 @@ omniroute run qwen --model glm/glm-5.2 -- -p "reply OK"
|
||||
omniroute run gemini --model glm/glm-5.2 -- --skip-trust -p "reply OK"
|
||||
|
||||
# Or pick provider+model interactively and write the tool's own config:
|
||||
omniroute configure codex # also: claude opencode qwen aider goose gemini cline continue kilo
|
||||
omniroute configure codex # also: claude opencode qwen aider goose cline continue kilo
|
||||
```
|
||||
|
||||
Every command honors the active remote context (`omniroute connect <host>`), `--dry-run`
|
||||
@@ -645,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 352 AI Providers — 152 Catalog-Marked Free
|
||||
## 🌐 351 AI Providers — 154 Catalog-Marked Free
|
||||
|
||||
</div>
|
||||
|
||||
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **446 per-model rows**, **38 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
> **351 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **154 carrying `hasFree: true` discovery metadata**. The chat model registry covers **268 providers / 2,566 distinct provider-model pairs / 1,312 raw model IDs**; the separate free-budget catalog has **455 per-model rows**, **40 recurring pools** and **56 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -813,7 +810,7 @@ Tokens are scoped `read` / `write` / `admin`; process-spawning routes stay loopb
|
||||
|
||||
<div align="left">
|
||||
|
||||
<img src="./docs/diagrams/cli-terminal.svg" width="50%" alt="Animated terminal demoing the OmniRoute CLI — omniroute providers list, omniroute combo list and omniroute health — cycling over the 86-command top-level surface: providers · oauth · keys · combo · nodes · models · cache · compression · cost · usage · quota · health · resilience · telemetry · logs · audit · mcp · a2a · cloud · memory · skills · eval · tunnel · backup · sync · webhooks · policy · pricing · translator · simulate …"/>
|
||||
<img src="./docs/diagrams/cli-terminal.svg" width="50%" alt="Animated terminal demoing the OmniRoute CLI — omniroute providers list, omniroute combo list and omniroute health — cycling over the 85-command top-level surface: providers · oauth · keys · combo · nodes · models · cache · compression · cost · usage · quota · health · resilience · telemetry · logs · audit · mcp · a2a · cloud · memory · skills · eval · tunnel · backup · sync · webhooks · policy · pricing · translator · simulate …"/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -824,11 +821,11 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
|
||||
<table>
|
||||
<tr><th align="left">Interface</th><th align="left">Endpoint / command</th><th align="left">Use it for</th></tr>
|
||||
<tr><td align="left" nowrap>🧰 <b>MCP (stdio)</b></td><td align="left" nowrap><code>omniroute --mcp</code></td><td align="left">Plug into Claude Desktop, Cursor, any MCP client</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>110 tools</b>, 33 scopes (enforcement opt-in), full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>110 tools</b>, 33 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>📡 <b>MCP (SSE)</b></td><td align="left" nowrap><code>/api/mcp/sse</code></td><td align="left">Streaming MCP transport</td></tr>
|
||||
<tr><td align="left" nowrap>🤝 <b>A2A</b></td><td align="left" nowrap><code>/.well-known/agent.json</code></td><td align="left">Agent-to-agent, <b>JSON-RPC 2.0</b> + SSE, 6 skills</td></tr>
|
||||
<tr><td align="left" nowrap>🌐 <b>REST API</b></td><td align="left" nowrap><code>/v1/*</code></td><td align="left">OpenAI-compatible — chat, embeddings, images, audio, OCR</td></tr>
|
||||
<tr><td align="left" nowrap>🔔 <b>Webhooks</b></td><td align="left" nowrap><code>/api/webhooks</code></td><td align="left">Push request / quota events to Slack, Discord, Telegram or any URL</td></tr>
|
||||
<tr><td align="left" nowrap>🔔 <b>Webhooks</b></td><td align="left" nowrap><code>/api/webhooks</code></td><td align="left">Push events (usage, quota, errors, routing) to your URL</td></tr>
|
||||
<tr><td align="left" nowrap>🛰️ <b>Remote CLI</b></td><td align="left" nowrap><code>omniroute connect <host></code></td><td align="left">Drive a remote instance with scoped access tokens</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -920,8 +917,6 @@ The 12 engines above shrink what goes **in**. Three more layers shape **how**, *
|
||||
- **🪄 Output Styles** _(output-axis steering)_ — inject deterministic, cache-safe response-shaping instructions; combinable, each at `lite` / `full` / `ultra` intensity. Adding a style is a one-line registry entry:
|
||||
- **Terse prose** — drop filler / articles / hedging; keep technical substance exact.
|
||||
- **Less code** — "lazy senior dev" YAGNI: smallest working change, no unrequested scaffolding.
|
||||
- **Ponytail (lazy senior dev)** — climb the YAGNI ladder, fix the root cause, smallest working diff.
|
||||
- **I have ADHD (action-first)** — next action leads, steps numbered, one concrete next step, no preamble.
|
||||
- **Terse CJK (文言)** — classical-Chinese ultra-terse style (locale-gated to `zh`).
|
||||
- **🎯 Adaptive context-budget** _(the dial)_ — instead of one on/off token threshold, escalate the cheapest, most-lossless engines only as far as needed to **fit the model's context window**. Policy: `reserve-output` (default, model-aware) · `percentage` · `absolute`. Mode: `floor` (guarantee fit) · `replace-autotrigger` (your explicit choice wins) · `off` (legacy threshold).
|
||||
- **🎛️ Where compression is decided** _(precedence, high → low)_ — per-request `x-omniroute-compression` header › routing-combo override › active named profile › adaptive / auto-trigger › panel default › off. The applied plan echoes back in the `X-OmniRoute-Compression: <mode>; source=<source>` response header.
|
||||
@@ -1020,7 +1015,7 @@ Full table: [Docker Guide — runtime RAM](docs/guides/DOCKER_GUIDE.md#runtime-r
|
||||
Standard `bun install` and global installation (`bun install -g omniroute`) are supported via Bun runtime detection:
|
||||
|
||||
- **Built-in `bun:sqlite`**: OmniRoute uses Bun's built-in `bun:sqlite` driver when running under Bun, falling back to `better-sqlite3` on Node.js or `sql.js`.
|
||||
- **Automatic Webpack bundler selection in dev**: Development (`bun run dev`) automatically detects Bun and disables Turbopack in favor of Webpack to prevent native V8 binding incompatibilities. Production builds (`bun run build`) follow `OMNIROUTE_USE_TURBOPACK` exactly as on Node: Turbopack by default, `OMNIROUTE_USE_TURBOPACK=0` to build with Webpack (`Dockerfile.bun` exposes it as a `--build-arg`).
|
||||
- **Automatic Webpack bundler selection**: Development (`bun run dev`) and production builds (`bun run build`) automatically detect Bun and disable Turbopack in favor of Webpack to prevent native V8 binding incompatibilities.
|
||||
- **Dedicated Bun Dockerfile**: Multi-stage `Dockerfile.bun` for native Bun production deployments (`docker build -f Dockerfile.bun -t omniroute:bun .`).
|
||||
|
||||
```bash
|
||||
@@ -1183,10 +1178,9 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
| 🐙 **GitHub** — follow for releases & tips | [@diegosouzapw](https://github.com/diegosouzapw) |
|
||||
| 💬 **Discord** | [discord.gg/U47eFqAXCn](https://discord.gg/U47eFqAXCn) |
|
||||
| ✈️ **Telegram** | [t.me/omnirouteOficial](https://t.me/omnirouteOficial) |
|
||||
| 🟢 **WhatsApp — 🌍 Global** | [join the group](https://chat.whatsapp.com/FvuCbrpZmQ6I85n2vW5QIC?s=cl&p=a&mlu=4) |
|
||||
| 🟢 **WhatsApp — 🇧🇷 Brasil** | [entrar no grupo](https://chat.whatsapp.com/KWgatljAjmbELQory59Oti?s=cl&p=a&mlu=4) |
|
||||
| 🟢 **WhatsApp — 🌍 Global** | [join the group](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t) |
|
||||
| 🟢 **WhatsApp — 🇧🇷 Brasil** | [entrar no grupo](https://chat.whatsapp.com/LTSpdFhXTxjH4R6CCNiKWz) |
|
||||
| 🌍 **Website** | [omniroute.online](https://omniroute.online) |
|
||||
| 🌍 **🌍StHub OmniRoute Community (free)** | [portal sthub](https://portal.sthub.com.br/communities/groups/st-hub/channels/Omniroute-World-8kRjmK) |
|
||||
| 📦 **Source code** | [github.com/diegosouzapw/OmniRoute](https://github.com/diegosouzapw/OmniRoute) |
|
||||
| 🐛 **Report a bug** | [open an issue](https://github.com/diegosouzapw/OmniRoute/issues) — attach `npm run system-info` output |
|
||||
| 🤝 **Contribute** | [CONTRIBUTING.md](CONTRIBUTING.md) · [Branching & Release Model](docs/ops/BRANCHING_MODEL.md) · pick a `good first issue` |
|
||||
@@ -1208,7 +1202,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 167 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 166 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
@@ -1269,9 +1263,9 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b><a href="docs/compression/COMPRESSION_RULES_FORMAT.md">Compression Rules Format</a></b></td><td>JSON rule-pack schemas for Caveman and RTK filters</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/compression/COMPRESSION_LANGUAGE_PACKS.md">Compression Language Packs</a></b></td><td>Language detection and Caveman rule-pack authoring</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>16-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>15-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 38 documented recurring pools / 446 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 39 documented recurring pools / 445 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
|
||||
</table>
|
||||
@@ -1672,7 +1666,7 @@ MIT License - see [LICENSE](LICENSE) for details.
|
||||
|
||||
**[⬆ Back to top](#-omniroute)** · Built with ❤️ for the open-source AI community.
|
||||
|
||||
<sub>OmniRoute v3.8.51 · Node ≥22.22.2 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
|
||||
<sub>OmniRoute v3.8.50 · Node ≥22.22.2 · MIT License · <a href="https://omniroute.online">omniroute.online</a></sub>
|
||||
|
||||
</div>
|
||||
<!-- GitHub Discussions enabled for community Q&A -->
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
## codex-chatgpt-web
|
||||
|
||||
Parts of `open-sse/vendor/codex-chatgpt-web/` are adapted from
|
||||
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), v4.0.7 commit
|
||||
`b59d7dc51b84fb1f465ff1d00f5207f3b2b4a494`.
|
||||
[`miuuyy/codex-chatgpt-web`](https://github.com/miuuyy/codex-chatgpt-web), commit
|
||||
`55592fca0ba19a27f1b769cec8fff61ff340a785`.
|
||||
|
||||
MIT License
|
||||
|
||||
|
||||
@@ -176,14 +176,13 @@ function isWithinRoot(ancestor, candidate) {
|
||||
* Register the ESM resolve hook for the current process. Safe to call multiple
|
||||
* times — subsequent calls are no-ops once the hook is installed.
|
||||
*
|
||||
* Modern runtimes import the hook module in-thread, initialize its root with a
|
||||
* plain function call, and register its synchronous resolver through
|
||||
* `module.registerHooks()`. Runtimes without that API (notably Bun) retain the
|
||||
* `module.register()` worker-thread loader lifecycle path.
|
||||
* Uses Node's stable `module.register()` API (available since Node 20.6,
|
||||
* required Node 22+ here). The hook runs in a worker thread but only reads the
|
||||
* captured `root`, so no shared-state hazards.
|
||||
*
|
||||
* @param {string} root Absolute path to the package root.
|
||||
* @returns {Promise<boolean>} Resolves `true` once registered (or if already
|
||||
* registered), `false` when neither registration API is usable.
|
||||
* registered), `false` on environments where `module.register` is unavailable.
|
||||
*/
|
||||
let _registered = false;
|
||||
export async function registerAliasResolver(root) {
|
||||
@@ -202,7 +201,7 @@ export async function registerAliasResolver(root) {
|
||||
}
|
||||
|
||||
try {
|
||||
const mod = await import("node:module");
|
||||
const { register } = await import("node:module");
|
||||
// #7808: load the hook from a real file on disk via pathToFileURL() instead
|
||||
// of building a `data:text/javascript,...` URL dynamically. CodeQL's
|
||||
// `js/incomplete-url-substring-sanitization` flagged the interpolated
|
||||
@@ -212,21 +211,14 @@ export async function registerAliasResolver(root) {
|
||||
// package.json "files": ["bin/"].
|
||||
const hookPath = join(__dirname, "aliasResolverHook.mjs");
|
||||
const hookUrl = pathToFileURL(hookPath);
|
||||
if (typeof mod.registerHooks === "function") {
|
||||
const hook = await import(hookUrl.href);
|
||||
hook.initialize({ root });
|
||||
mod.registerHooks({ resolve: hook.resolve });
|
||||
_registered = true;
|
||||
return true;
|
||||
}
|
||||
mod.register(hookUrl, { data: { root } });
|
||||
register(hookUrl, { data: { root } });
|
||||
_registered = true;
|
||||
return true;
|
||||
} catch {
|
||||
// Runtime or sandboxed env without a usable module hook API — fall back to
|
||||
// the default resolver. The bug will resurface only in the exact
|
||||
// global-install scenario, which is what we explicitly patched; other entry
|
||||
// points still work because they import via relative paths.
|
||||
// Older Node or sandboxed env without module.register — fall back to the
|
||||
// default resolver. The bug will resurface only in the exact global-install
|
||||
// scenario, which is what we explicitly patched; other entry points still
|
||||
// work because they import via relative paths.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,20 +32,17 @@ export function resolveChatGptWebCodexMcpEntry(rootDir = root, exists = existsSy
|
||||
return candidates.find((candidate) => exists(candidate)) ?? null;
|
||||
}
|
||||
|
||||
export async function loadChatGptWebCodexMcpModule(entry) {
|
||||
if (entry.endsWith(".ts")) {
|
||||
await import("tsx/esm");
|
||||
}
|
||||
return import(pathToFileURL(entry).href);
|
||||
}
|
||||
|
||||
export async function startChatGptWebCodexMcp(args = process.argv.slice(2), rootDir = root) {
|
||||
const socketIndex = args.indexOf("--broker-socket");
|
||||
const brokerSocketPath = socketIndex >= 0 ? args[socketIndex + 1] : undefined;
|
||||
if (!brokerSocketPath) throw new Error("--broker-socket is required");
|
||||
const entry = resolveChatGptWebCodexMcpEntry(rootDir);
|
||||
if (!entry) throw new Error("ChatGPT Web (Codex) MCP entrypoint was not found");
|
||||
const module = await loadChatGptWebCodexMcpModule(entry);
|
||||
if (entry.endsWith(".ts")) {
|
||||
const { register } = await import("node:module");
|
||||
register("tsx/esm", pathToFileURL(`${rootDir}/`));
|
||||
}
|
||||
const module = await import(pathToFileURL(entry).href);
|
||||
await module.runChatGptMcpServer({ brokerSocketPath });
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ export function register_combos(parent) {
|
||||
});
|
||||
tag.command("post-api-combos")
|
||||
.description("Create routing combo")
|
||||
.requiredOption("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos";
|
||||
@@ -44,7 +44,7 @@ export function register_combos(parent) {
|
||||
tag.command("put-api-combos-id-")
|
||||
.description("Update combo")
|
||||
.requiredOption("--id <id>", "")
|
||||
.requiredOption("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
@@ -62,7 +62,7 @@ export function register_combos(parent) {
|
||||
tag.command("patch-api-combos-id-")
|
||||
.description("Update combo")
|
||||
.requiredOption("--id <id>", "")
|
||||
.requiredOption("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.option("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/{id}";
|
||||
@@ -99,17 +99,10 @@ export function register_combos(parent) {
|
||||
});
|
||||
tag.command("post-api-combos-test")
|
||||
.description("Test a combo configuration")
|
||||
.requiredOption("--body <jsonOrPath>", "JSON body or @path/to/file.json")
|
||||
.action(async (opts, cmd) => {
|
||||
const gOpts = cmd.optsWithGlobals();
|
||||
let url = "/api/combos/test";
|
||||
let body;
|
||||
if (opts.body) {
|
||||
body = opts.body.startsWith("@")
|
||||
? JSON.parse(readFileSync(opts.body.slice(1), "utf8"))
|
||||
: JSON.parse(opts.body);
|
||||
}
|
||||
const res = await apiFetch(url, { method: "POST", body, baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const res = await apiFetch(url, { method: "POST", baseUrl: gOpts.baseUrl, apiKey: gOpts.apiKey });
|
||||
const data = res.ok ? await res.json() : await res.text();
|
||||
emit(data, gOpts);
|
||||
});
|
||||
|
||||
@@ -93,16 +93,6 @@ export const CLI_TARGET_MANIFEST = Object.freeze({
|
||||
configure: true,
|
||||
runModel: null,
|
||||
}),
|
||||
"5dive": Object.freeze({
|
||||
// 5dive is a fleet manager, not a coding CLI: it points its own `claude`
|
||||
// agents at an endpoint. `omniroute run 5dive` would have nothing to
|
||||
// launch, so this is configure-only.
|
||||
description: "5dive (agent fleet)",
|
||||
aliases: Object.freeze(["fivedive", "5dive-cli"]),
|
||||
run: false,
|
||||
configure: true,
|
||||
runModel: null, // travels as the profile's ANTHROPIC_DEFAULT_*_MODEL
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
|
||||
@@ -39,7 +39,6 @@ export const SETUP_MODULES = {
|
||||
cline: { module: "./setup-cline.mjs", exportName: "runSetupClineCommand" },
|
||||
continue: { module: "./setup-continue.mjs", exportName: "runSetupContinueCommand" },
|
||||
kilo: { module: "./setup-kilo.mjs", exportName: "runSetupKiloCommand" },
|
||||
"5dive": { module: "./setup-5dive.mjs", exportName: "runSetup5diveCommand" },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,80 +24,6 @@ function parseHeader(kv) {
|
||||
return { name: kv.slice(0, eq), value: kv.slice(eq + 1) };
|
||||
}
|
||||
|
||||
function getRootCommand(cmd) {
|
||||
let curr = cmd;
|
||||
while (curr.parent) curr = curr.parent;
|
||||
return curr;
|
||||
}
|
||||
|
||||
function resolveNodeEndpoint(opts, cmd) {
|
||||
if (opts.endpoint) {
|
||||
return { endpoint: opts.endpoint, apiFetchOpts: cmd.optsWithGlobals() };
|
||||
}
|
||||
if (opts.nodeUrl) {
|
||||
return { endpoint: opts.nodeUrl, apiFetchOpts: cmd.optsWithGlobals() };
|
||||
}
|
||||
|
||||
// Check if --base-url, --endpoint, or --node-url was explicitly passed after the subcommand
|
||||
const root = getRootCommand(cmd);
|
||||
const rawArgs = root.rawArgs || process.argv;
|
||||
const cmdName = cmd.name();
|
||||
|
||||
let subArgsStart = -1;
|
||||
for (let i = 0; i < rawArgs.length - 1; i++) {
|
||||
if (rawArgs[i] === "nodes" || rawArgs[i] === "provider-nodes") {
|
||||
if (rawArgs[i + 1] === cmdName) {
|
||||
subArgsStart = i + 2;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let explicitSubcommandBaseUrl = undefined;
|
||||
let serverBaseUrl = undefined;
|
||||
|
||||
if (subArgsStart !== -1) {
|
||||
const preArgs = rawArgs.slice(0, subArgsStart);
|
||||
for (let i = 0; i < preArgs.length; i++) {
|
||||
if (preArgs[i] === "--base-url" && i + 1 < preArgs.length) {
|
||||
serverBaseUrl = preArgs[i + 1];
|
||||
} else if (preArgs[i].startsWith("--base-url=")) {
|
||||
serverBaseUrl = preArgs[i].slice("--base-url=".length);
|
||||
}
|
||||
}
|
||||
|
||||
const subArgs = rawArgs.slice(subArgsStart);
|
||||
for (let i = 0; i < subArgs.length; i++) {
|
||||
const arg = subArgs[i];
|
||||
if (
|
||||
(arg === "--base-url" || arg === "--endpoint" || arg === "--node-url") &&
|
||||
i + 1 < subArgs.length
|
||||
) {
|
||||
explicitSubcommandBaseUrl = subArgs[i + 1];
|
||||
} else if (
|
||||
arg.startsWith("--base-url=") ||
|
||||
arg.startsWith("--endpoint=") ||
|
||||
arg.startsWith("--node-url=")
|
||||
) {
|
||||
explicitSubcommandBaseUrl = arg.slice(arg.indexOf("=") + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (explicitSubcommandBaseUrl !== undefined) {
|
||||
const globals = cmd.optsWithGlobals?.() ?? {};
|
||||
const apiFetchOpts = { ...globals };
|
||||
if (serverBaseUrl) {
|
||||
apiFetchOpts.baseUrl = serverBaseUrl;
|
||||
} else {
|
||||
delete apiFetchOpts.baseUrl;
|
||||
}
|
||||
return { endpoint: explicitSubcommandBaseUrl, apiFetchOpts };
|
||||
}
|
||||
|
||||
return { endpoint: undefined, apiFetchOpts: cmd.optsWithGlobals() };
|
||||
}
|
||||
|
||||
const nodeSchema = [
|
||||
{ key: "id", header: "Node ID", width: 22 },
|
||||
{ key: "provider", header: "Provider", width: 16 },
|
||||
@@ -144,8 +70,7 @@ export function registerNodes(program) {
|
||||
nodes
|
||||
.command("add")
|
||||
.requiredOption("--provider <p>", t("nodes.add.provider"))
|
||||
.option("--endpoint <url>", t("nodes.add.baseUrl"))
|
||||
.option("--base-url <url>", t("nodes.add.baseUrl"))
|
||||
.requiredOption("--endpoint <url>", t("nodes.add.baseUrl"))
|
||||
.option("--name <n>", t("nodes.add.name"))
|
||||
.option("--weight <w>", t("nodes.add.weight"), parseInt, 100)
|
||||
.option("--region <r>", t("nodes.add.region"))
|
||||
@@ -156,14 +81,9 @@ export function registerNodes(program) {
|
||||
[]
|
||||
)
|
||||
.action(async (opts, cmd) => {
|
||||
const { endpoint, apiFetchOpts } = resolveNodeEndpoint(opts, cmd);
|
||||
if (!endpoint) {
|
||||
process.stderr.write(`error: required option '--endpoint <url>' or '--base-url <url>' not specified\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const body = {
|
||||
provider: opts.provider,
|
||||
baseUrl: endpoint,
|
||||
baseUrl: opts.endpoint,
|
||||
name: opts.name,
|
||||
weight: opts.weight,
|
||||
region: opts.region,
|
||||
@@ -171,7 +91,7 @@ export function registerNodes(program) {
|
||||
headers: opts.authHeader?.length ? opts.authHeader : undefined,
|
||||
};
|
||||
const res = await apiFetch("/api/provider-nodes", {
|
||||
...apiFetchOpts,
|
||||
...cmd.optsWithGlobals(),
|
||||
method: "POST",
|
||||
body,
|
||||
});
|
||||
@@ -179,26 +99,24 @@ export function registerNodes(program) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), apiFetchOpts);
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
nodes
|
||||
.command("update <nodeId>")
|
||||
.option("--endpoint <url>", t("nodes.update.baseUrl"))
|
||||
.option("--base-url <url>", t("nodes.update.baseUrl"))
|
||||
.option("--name <n>", t("nodes.update.name"))
|
||||
.option("--weight <w>", t("nodes.update.weight"), parseInt)
|
||||
.option("--region <r>", t("nodes.update.region"))
|
||||
.option("--enabled <b>", t("nodes.update.enabled"), (v) => v === "true")
|
||||
.action(async (id, opts, cmd) => {
|
||||
const { endpoint, apiFetchOpts } = resolveNodeEndpoint(opts, cmd);
|
||||
const body = {};
|
||||
if (endpoint !== undefined) body.baseUrl = endpoint;
|
||||
if (opts.endpoint !== undefined) body.baseUrl = opts.endpoint;
|
||||
for (const k of ["name", "weight", "region", "enabled"]) {
|
||||
if (opts[k] !== undefined) body[k] = opts[k];
|
||||
}
|
||||
const res = await apiFetch(`/api/provider-nodes/${id}`, {
|
||||
...apiFetchOpts,
|
||||
...cmd.optsWithGlobals(),
|
||||
method: "PUT",
|
||||
body,
|
||||
});
|
||||
@@ -206,7 +124,7 @@ export function registerNodes(program) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), apiFetchOpts);
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
nodes
|
||||
@@ -227,25 +145,19 @@ export function registerNodes(program) {
|
||||
|
||||
nodes
|
||||
.command("validate")
|
||||
.option("--endpoint <url>", t("nodes.validate.baseUrl"))
|
||||
.option("--base-url <url>", t("nodes.validate.baseUrl"))
|
||||
.requiredOption("--endpoint <url>", t("nodes.validate.baseUrl"))
|
||||
.requiredOption("--provider <p>", t("nodes.validate.provider"))
|
||||
.action(async (opts, cmd) => {
|
||||
const { endpoint, apiFetchOpts } = resolveNodeEndpoint(opts, cmd);
|
||||
if (!endpoint) {
|
||||
process.stderr.write(`error: required option '--endpoint <url>' or '--base-url <url>' not specified\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
const res = await apiFetch("/api/provider-nodes/validate", {
|
||||
...apiFetchOpts,
|
||||
...cmd.optsWithGlobals(),
|
||||
method: "POST",
|
||||
body: { baseUrl: endpoint, provider: opts.provider },
|
||||
body: { baseUrl: opts.endpoint, provider: opts.provider },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
process.exit(1);
|
||||
}
|
||||
emit(await res.json(), apiFetchOpts);
|
||||
emit(await res.json(), cmd.optsWithGlobals());
|
||||
});
|
||||
|
||||
nodes
|
||||
|
||||
@@ -66,7 +66,6 @@ import { registerSetupClaude } from "./setup-claude.mjs";
|
||||
import { registerSetupOpencode } from "./setup-opencode.mjs";
|
||||
import { registerSetupCline } from "./setup-cline.mjs";
|
||||
import { registerSetupKilo } from "./setup-kilo.mjs";
|
||||
import { registerSetup5dive } from "./setup-5dive.mjs";
|
||||
import { registerSetupContinue } from "./setup-continue.mjs";
|
||||
import { registerSetupCursor } from "./setup-cursor.mjs";
|
||||
import { registerSetupRoo } from "./setup-roo.mjs";
|
||||
@@ -153,7 +152,6 @@ export function registerCommands(program) {
|
||||
registerSetupOpencode(program);
|
||||
registerSetupCline(program);
|
||||
registerSetupKilo(program);
|
||||
registerSetup5dive(program);
|
||||
registerSetupContinue(program);
|
||||
registerSetupCursor(program);
|
||||
registerSetupRoo(program);
|
||||
|
||||
@@ -5,11 +5,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
import {
|
||||
ServerSupervisor,
|
||||
detectMitmCrash,
|
||||
BUN_PRELOAD_PATH,
|
||||
} from "../runtime/processSupervisor.mjs";
|
||||
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
|
||||
import { isTermux } from "../../../scripts/build/postinstallSupport.mjs";
|
||||
import {
|
||||
ensureAndroidCacheDir,
|
||||
@@ -310,7 +306,7 @@ function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[
|
||||
...(process.versions.bun
|
||||
? ["--preload", BUN_PRELOAD_PATH]
|
||||
? ["--preload", join(APP_DIR, "open-sse/utils/setupPolyfill.ts")]
|
||||
: buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
],
|
||||
@@ -335,7 +331,7 @@ function runWithoutRecovery(serverJs, env, memoryLimit, dashboardPort, apiPort,
|
||||
process.versions.bun ? process.execPath : "node",
|
||||
[
|
||||
...(process.versions.bun
|
||||
? ["--preload", BUN_PRELOAD_PATH]
|
||||
? ["--preload", join(APP_DIR, "open-sse/utils/setupPolyfill.ts")]
|
||||
: buildNodeHeapArgs(process.env, memoryLimit)),
|
||||
serverJs,
|
||||
],
|
||||
@@ -427,9 +423,7 @@ async function runWithSupervisor(
|
||||
if (detectMitmCrash(crashLog)) {
|
||||
try {
|
||||
const PROJECT_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
const { updateSettings } = await import(
|
||||
pathToFileURL(join(PROJECT_ROOT, "src/lib/db/settings.ts")).href
|
||||
);
|
||||
const { updateSettings } = await import(pathToFileURL(join(PROJECT_ROOT, "src/lib/db/settings.ts")).href);
|
||||
updateSettings({ mitmEnabled: false });
|
||||
} catch {}
|
||||
return "disable-mitm-and-retry";
|
||||
|
||||
@@ -1,315 +0,0 @@
|
||||
/**
|
||||
* omniroute setup-5dive — point a 5dive agent fleet at OmniRoute.
|
||||
*
|
||||
* 5dive (https://5dive.com) manages a fleet of long-running coding agents, each
|
||||
* one a systemd unit under its own Unix user. It is not itself a coding CLI, so
|
||||
* there is nothing for `omniroute run` to launch — this is a configure-only
|
||||
* target.
|
||||
*
|
||||
* Unlike the other recipes, 5dive does not read a config file out of $HOME. Its
|
||||
* credentials live in AUTH PROFILES under /var/lib/5dive/auth-profiles/<name>/,
|
||||
* and the supported way to write one is the CLI itself:
|
||||
*
|
||||
* 5dive agent auth set claude --provider=<id> --base-url=<url> \
|
||||
* --api-key=- --auth-profile=<name> --model=<slug>
|
||||
*
|
||||
* Four value flags, all four load-bearing (verified against 5dive-cli main,
|
||||
* 2026-08-27):
|
||||
* --provider `--base-url` is refused without it, rather than accepted
|
||||
* and silently dropped. `openai` here is 5dive's BYO id for
|
||||
* "a custom Anthropic-compatible endpoint", not a vendor
|
||||
* choice — override with --byo-provider.
|
||||
* --base-url OmniRoute's Anthropic surface, ROOT url with no /v1.
|
||||
* --auth-profile BYO credentials are profile-scoped; required for claude.
|
||||
* --model `openai` has no row in 5dive's built-in endpoint catalog,
|
||||
* so there are no per-tier model ids to inherit.
|
||||
*
|
||||
* The key is handed over on stdin (`--api-key=-`) so it never reaches argv.
|
||||
*
|
||||
* Two things this recipe cannot do for you, and says so instead of failing
|
||||
* obscurely:
|
||||
* 1. Writing an auth profile is root-only on the 5dive host. We re-exec
|
||||
* through sudo when we are not root (disable with --no-sudo).
|
||||
* 2. `agent auth set` writes the profile and restarts the agents bound to it,
|
||||
* but each seat also carries its OWN runtime model pin, and that pin wins
|
||||
* over the profile's ANTHROPIC_DEFAULT_*_MODEL. Pass --agent <name> (repeatable)
|
||||
* to pin the seats too; otherwise we print the command for them.
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
|
||||
const DEFAULT_PROFILE = "omniroute";
|
||||
|
||||
/** 5dive's `claude` BYO endpoint is the Anthropic surface ROOT — strip a trailing /v1. */
|
||||
function stripToRoot(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
return s.endsWith("/v1") ? s.slice(0, -3) : s;
|
||||
}
|
||||
|
||||
/** Resolve baseUrl (ROOT, no /v1) + apiKey from flags -> active context -> localhost. */
|
||||
export function resolveFivediveTarget(opts = {}) {
|
||||
let baseUrl;
|
||||
if (opts.remote) baseUrl = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
baseUrl = stripToRoot(
|
||||
resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl
|
||||
);
|
||||
} catch {
|
||||
/* no context configured */
|
||||
}
|
||||
if (!baseUrl)
|
||||
baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
|
||||
}
|
||||
let apiKey = opts.apiKey ?? opts["api-key"];
|
||||
if (!apiKey) {
|
||||
try {
|
||||
const c = resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT);
|
||||
apiKey = c?.accessToken || c?.apiKey;
|
||||
} catch {
|
||||
/* no context configured */
|
||||
}
|
||||
}
|
||||
if (!apiKey) apiKey = process.env.OMNIROUTE_API_KEY || "";
|
||||
return { baseUrl, apiKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* 5dive refuses a base URL before storing it, and the rule is not the obvious
|
||||
* one: the agent's key rides this URL on every request, so https:// is required
|
||||
* unless the host is loopback. Reproduce the check here so the operator gets the
|
||||
* reason at the point of choosing, not a validation error three commands later.
|
||||
*/
|
||||
export function validateFivediveBaseUrl(rawUrl) {
|
||||
const url = String(rawUrl || "");
|
||||
if (!url) return { ok: false, reason: "A base URL is required." };
|
||||
if (url.startsWith("https://")) return { ok: true };
|
||||
if (!url.startsWith("http://")) {
|
||||
return { ok: false, reason: `Unsupported scheme in '${url}' (expected http:// or https://).` };
|
||||
}
|
||||
let host = url.slice("http://".length);
|
||||
host = host.split("/")[0].split("?")[0];
|
||||
host = host.startsWith("[") ? `${host.slice(0, host.indexOf("]"))}]` : host.split(":")[0];
|
||||
if (host === "127.0.0.1" || host === "localhost" || host === "[::1]") return { ok: true };
|
||||
return {
|
||||
ok: false,
|
||||
reason:
|
||||
`5dive accepts http:// only for a loopback host; '${host}' is off-box, so the agent's ` +
|
||||
`API key would travel in plaintext. Serve OmniRoute over https:// and pass ` +
|
||||
`--remote https://${host}...`,
|
||||
};
|
||||
}
|
||||
|
||||
/** Argv for the profile write. The key is NOT here — it goes in on stdin. */
|
||||
export function buildFivediveAuthArgs({ baseUrl, profile, model, provider = "openai" }) {
|
||||
return [
|
||||
"agent",
|
||||
"auth",
|
||||
"set",
|
||||
"claude",
|
||||
`--provider=${provider}`,
|
||||
`--base-url=${baseUrl}`,
|
||||
"--api-key=-",
|
||||
`--auth-profile=${profile}`,
|
||||
`--model=${model}`,
|
||||
];
|
||||
}
|
||||
|
||||
/** Argv for one seat's runtime model pin, which outranks the profile's env defaults. */
|
||||
export function buildFivedivePinArgs(agent, model) {
|
||||
return ["agent", "config", agent, "set", `model=${model}`];
|
||||
}
|
||||
|
||||
/** Prepend sudo when the profile write needs root and we do not have it. */
|
||||
export function withPrivilege(bin, args, { isRoot, useSudo }) {
|
||||
if (isRoot || !useSudo) return [bin, args];
|
||||
return ["sudo", [bin, ...args]];
|
||||
}
|
||||
|
||||
function quote(arg) {
|
||||
return /^[A-Za-z0-9_@%+=:,./-]+$/.test(arg) ? arg : `'${String(arg).replace(/'/g, "'\\''")}'`;
|
||||
}
|
||||
|
||||
/** Render argv the way an operator would type it. */
|
||||
export function renderCommand(bin, args) {
|
||||
return [bin, ...args].map(quote).join(" ");
|
||||
}
|
||||
|
||||
function run(bin, args, stdinPayload) {
|
||||
return new Promise((resolve) => {
|
||||
const child = spawn(bin, args, {
|
||||
// sudo reads its password straight from the tty, so stdin stays free for
|
||||
// the API key.
|
||||
stdio: [stdinPayload === undefined ? "inherit" : "pipe", "inherit", "inherit"],
|
||||
});
|
||||
child.on("error", (e) => resolve({ code: 1, error: e }));
|
||||
child.on("close", (code) => resolve({ code: code ?? 1 }));
|
||||
if (stdinPayload !== undefined && child.stdin) {
|
||||
child.stdin.end(stdinPayload);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchModelIds(baseUrl, apiKey) {
|
||||
try {
|
||||
const headers = { "Content-Type": "application/json" };
|
||||
if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
|
||||
const res = await fetch(`${baseUrl}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function agentList(opts) {
|
||||
const raw = opts.agent ?? opts.agents ?? [];
|
||||
return (Array.isArray(raw) ? raw : [raw]).map((a) => String(a).trim()).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function runSetup5diveCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveFivediveTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const bin = opts.fivediveBin ?? opts["fivedive-bin"] ?? process.env.CLI_5DIVE_BIN ?? "5dive";
|
||||
const profile = String(opts.authProfile ?? opts["auth-profile"] ?? opts.name ?? DEFAULT_PROFILE);
|
||||
// NOT `opts.provider`: the `configure` picker uses that flag for the
|
||||
// OmniRoute model provider to filter on, and it reaches setup recipes
|
||||
// verbatim. The 5dive BYO id is its own flag.
|
||||
const provider = String(opts.byoProvider ?? opts["byo-provider"] ?? "openai");
|
||||
const agents = agentList(opts);
|
||||
|
||||
printHeading("OmniRoute -> 5dive (claude BYO endpoint)");
|
||||
printInfo(`Server: ${baseUrl}`);
|
||||
printInfo(`Profile: ${profile}`);
|
||||
|
||||
const urlCheck = validateFivediveBaseUrl(baseUrl);
|
||||
if (!urlCheck.ok) {
|
||||
printError(urlCheck.reason);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 5dive needs one explicit model id: `openai` has no catalog row, so there
|
||||
// are no per-tier defaults to fall back to.
|
||||
let model = opts.model;
|
||||
if (!model) {
|
||||
const ids = await fetchModelIds(baseUrl, apiKey);
|
||||
if (ids.length && !opts.yes) {
|
||||
printInfo(`Examples: ${ids.slice(0, 20).join(", ")}${ids.length > 20 ? " …" : ""}`);
|
||||
printInfo("A combo id works here too — that is how you get failover across providers.");
|
||||
const prompt = createPrompt();
|
||||
try {
|
||||
model = await prompt.ask("Model or combo id for the 5dive agents");
|
||||
} finally {
|
||||
prompt.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
printError("A model is required. Pass --model <id> (5dive has no model auto-discovery here).");
|
||||
return 2;
|
||||
}
|
||||
if (!apiKey) {
|
||||
printError("An OmniRoute API key is required. Pass --api-key, or set OMNIROUTE_API_KEY.");
|
||||
return 2;
|
||||
}
|
||||
|
||||
const isRoot = typeof process.getuid === "function" ? process.getuid() === 0 : false;
|
||||
const useSudo = (opts.sudo ?? true) !== false;
|
||||
const authArgs = buildFivediveAuthArgs({ baseUrl, profile, model, provider });
|
||||
const [authBin, authArgv] = withPrivilege(bin, authArgs, { isRoot, useSudo });
|
||||
|
||||
if (dryRun) {
|
||||
printInfo("\n[dry-run] would run:");
|
||||
printInfo(` ${renderCommand(authBin, authArgv)}`);
|
||||
printInfo(" (the API key is written to that command's stdin, never to argv)");
|
||||
for (const agent of agents) {
|
||||
const [pinBin, pinArgv] = withPrivilege(bin, buildFivedivePinArgs(agent, model), {
|
||||
isRoot,
|
||||
useSudo,
|
||||
});
|
||||
printInfo(` ${renderCommand(pinBin, pinArgv)}`);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!isRoot && !useSudo) {
|
||||
printError(
|
||||
"Writing a 5dive auth profile needs root on the 5dive host. Re-run as root, drop --no-sudo, " +
|
||||
"or run this by hand:"
|
||||
);
|
||||
printInfo(` ${renderCommand(bin, authArgs)}`);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const authResult = await run(authBin, authArgv, apiKey);
|
||||
if (authResult.error?.code === "ENOENT") {
|
||||
printError(
|
||||
`Could not find the '${bin}' CLI on this machine. 5dive's verbs run ON the fleet host — ` +
|
||||
"run this there, or point at the binary with --fivedive-bin."
|
||||
);
|
||||
return 1;
|
||||
}
|
||||
if (authResult.code !== 0) {
|
||||
printError(`'${bin} agent auth set' exited ${authResult.code}.`);
|
||||
return authResult.code;
|
||||
}
|
||||
printSuccess(`Auth profile '${profile}' now points at ${baseUrl}`);
|
||||
|
||||
// The profile carries ANTHROPIC_DEFAULT_{OPUS,SONNET,HAIKU}_MODEL, but each
|
||||
// seat's own runtime pin outranks it — a seat still pinned to a stock model id
|
||||
// fails its first turn with "There's an issue with the selected model".
|
||||
for (const agent of agents) {
|
||||
const [pinBin, pinArgv] = withPrivilege(bin, buildFivedivePinArgs(agent, model), {
|
||||
isRoot,
|
||||
useSudo,
|
||||
});
|
||||
const pinResult = await run(pinBin, pinArgv);
|
||||
if (pinResult.code !== 0) {
|
||||
printError(`Could not pin agent '${agent}' to '${model}' (exit ${pinResult.code}).`);
|
||||
return pinResult.code;
|
||||
}
|
||||
printSuccess(`Agent '${agent}' pinned to ${model}`);
|
||||
}
|
||||
|
||||
if (!agents.length) {
|
||||
printInfo("\nEach seat also carries its own runtime model pin, and it beats the profile:");
|
||||
printInfo(` ${renderCommand(bin, buildFivedivePinArgs("<agent>", model))}`);
|
||||
printInfo("Re-run with --agent <name> to have this command apply it for you.");
|
||||
}
|
||||
printInfo("\nBind a seat to the profile at creation time with:");
|
||||
printInfo(` ${renderCommand(bin, ["agent", "create", "<name>", `--auth-profile=${profile}`])}`);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerSetup5dive(program) {
|
||||
program
|
||||
.command("setup-5dive")
|
||||
.description(
|
||||
"Point a 5dive agent fleet's claude seats at OmniRoute (writes a 5dive auth profile)"
|
||||
)
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. https://omniroute.example.com")
|
||||
.option("--context <name>", "Named local/remote context")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to the active context/env)")
|
||||
.option("--model <id>", "OmniRoute model or combo id the agents should use")
|
||||
.option("--byo-provider <id>", "5dive BYO provider id (default: openai)", "openai")
|
||||
.option("--auth-profile <name>", "5dive auth profile to write", DEFAULT_PROFILE)
|
||||
.option(
|
||||
"--agent <name>",
|
||||
"Also pin this agent's runtime model (repeatable)",
|
||||
(value, previous) => [...(previous || []), value],
|
||||
[]
|
||||
)
|
||||
.option("--fivedive-bin <path>", "Path to the 5dive binary (default: 5dive on PATH)")
|
||||
.option("--no-sudo", "Do not re-exec through sudo when not running as root")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print the commands without running them")
|
||||
.action(async (opts) => {
|
||||
const code = await runSetup5diveCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
});
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { printHeading, printInfo, printSuccess, printError, printWarning } from "../io.mjs";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { homedir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
@@ -6,7 +6,6 @@ import { execFile } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { npmBin, npmExecOptions } from "../npm-exec.mjs";
|
||||
import { readPidFile, isPidRunning } from "../utils/pid.mjs";
|
||||
|
||||
const execFileAsync = promisify(execFile);
|
||||
|
||||
@@ -80,39 +79,6 @@ export async function createBackup() {
|
||||
}
|
||||
}
|
||||
|
||||
// #11885: `--apply` installs the new files (npm install -g) and re-reads
|
||||
// package.json from disk to confirm it, but a long-lived server process keeps
|
||||
// serving whatever it loaded at its last start — Node caches a `require()`d
|
||||
// package.json per resolved path for the life of the process. A later
|
||||
// `omniroute update` then correctly reports "already up to date" (the files
|
||||
// ARE current) while the running server is still stale, matching the reported
|
||||
// symptom. `--apply` never restarted anything and its success message ("Run
|
||||
// `omniroute --version` to verify.") implied the update was already live.
|
||||
//
|
||||
// `restart.mjs`'s `runRestartCommand()` stops then re-spawns the server in the
|
||||
// foreground (via `serve.mjs::runServe`), which can block the calling terminal
|
||||
// and is a materially bigger behavior change than this fix warrants to invoke
|
||||
// unconditionally and unattended from `--apply`. Instead, detect whether a
|
||||
// CLI-managed server is currently running (the same PID file `stop.mjs`/
|
||||
// `restart.mjs` already trust) and print an explicit, prominent instruction —
|
||||
// honest about what did and didn't happen — rather than silently assuming.
|
||||
export async function isServerProcessRunning(deps = { readPidFile, isPidRunning }) {
|
||||
const pid = deps.readPidFile("server");
|
||||
return Boolean(pid && deps.isPidRunning(pid));
|
||||
}
|
||||
|
||||
export async function printPostApplyGuidance(latest, deps = { readPidFile, isPidRunning }) {
|
||||
const running = await isServerProcessRunning(deps);
|
||||
if (running) {
|
||||
printWarning(`Files updated to ${latest}, but the running server is still on the old version.`);
|
||||
printInfo(" Run `omniroute restart` now to apply this update.");
|
||||
} else {
|
||||
printInfo(`No running OmniRoute server was detected via the CLI's PID file.`);
|
||||
printInfo(` Start it with \`omniroute serve\` (or restart your existing process) to run ${latest}.`);
|
||||
}
|
||||
printInfo("`omniroute --version` will keep reporting the old version until the process restarts.");
|
||||
}
|
||||
|
||||
export function registerUpdate(program) {
|
||||
program
|
||||
.command("update")
|
||||
@@ -244,8 +210,8 @@ export async function runUpdateCommand(opts = {}) {
|
||||
console.log(" or reorder PATH so the global bin comes first.");
|
||||
return 1;
|
||||
}
|
||||
printSuccess(`Installed omniroute@${latest} to disk.`);
|
||||
await printPostApplyGuidance(latest);
|
||||
printSuccess(`Updated to version ${latest}`);
|
||||
printInfo("Run `omniroute --version` to verify.");
|
||||
return 0;
|
||||
} catch (err) {
|
||||
printError(`Update failed: ${err.message}`);
|
||||
|
||||
@@ -81,7 +81,3 @@ export function printInfo(message) {
|
||||
export function printError(message) {
|
||||
console.log(`\x1b[31m✖ ${message}\x1b[0m`);
|
||||
}
|
||||
|
||||
export function printWarning(message) {
|
||||
console.log(`\x1b[33m⚠ ${message}\x1b[0m`);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writePidFile, cleanupPidFile, killAllSubprocesses, isPidRunning } from "../utils/pid.mjs";
|
||||
import {
|
||||
RESTART_RESET_MS,
|
||||
@@ -18,24 +17,6 @@ import {
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
|
||||
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
// Bun needs the Node-compat polyfill preloaded (#9761). The file ships at the
|
||||
// package root via package.json "files" (see scripts/build/pack-artifact-policy.ts)
|
||||
// and is never copied into dist/, so the path must resolve against the package
|
||||
// root — resolving it next to the server bundle fails with "preload not found" (#11980).
|
||||
export const BUN_PRELOAD_PATH = join(PACKAGE_ROOT, "open-sse", "utils", "setupPolyfill.ts");
|
||||
|
||||
/**
|
||||
* Argument vector for the server child. Kept pure so tests can assert on it
|
||||
* directly: the bare `import { spawn }` above cannot be intercepted without
|
||||
* --experimental-test-module-mocks (same seam as #8131).
|
||||
*/
|
||||
export function buildServerSpawnArgs(serverPath, memoryLimit, env = process.env) {
|
||||
return process.versions.bun
|
||||
? ["--preload", BUN_PRELOAD_PATH, serverPath]
|
||||
: buildNodeRuntimeArgs(env, memoryLimit, serverPath);
|
||||
}
|
||||
|
||||
export class ServerSupervisor {
|
||||
constructor({
|
||||
serverPath,
|
||||
@@ -74,11 +55,21 @@ export class ServerSupervisor {
|
||||
// Node args come from buildNodeRuntimeArgs (#9209 IPv4-first DNS + #5238
|
||||
// heap flag handling); the Bun branch keeps #9761's polyfill preload —
|
||||
// Bun does not accept the Node-only flags.
|
||||
this.child = spawn(process.execPath, buildServerSpawnArgs(this.serverPath, this.memoryLimit), {
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
this.child = spawn(
|
||||
process.execPath,
|
||||
process.versions.bun
|
||||
? [
|
||||
"--preload",
|
||||
join(dirname(this.serverPath), "open-sse/utils/setupPolyfill.ts"),
|
||||
this.serverPath,
|
||||
]
|
||||
: buildNodeRuntimeArgs(process.env, this.memoryLimit, this.serverPath),
|
||||
{
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
}
|
||||
);
|
||||
|
||||
writePidFile("server", this.child.pid);
|
||||
|
||||
|
||||
@@ -100,66 +100,31 @@ export async function waitForServer(port, timeout = 60000) {
|
||||
// - "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.
|
||||
// #11766: probe both IPv4 and IPv6 loopback to handle servers listening on
|
||||
// either family (or both).
|
||||
async function pollHealthOnce(port) {
|
||||
const hosts = ["127.0.0.1", "::1"];
|
||||
const outcomes = [];
|
||||
|
||||
// Probe both loopback families concurrently
|
||||
const results = await Promise.all(
|
||||
hosts.map(async (host) => {
|
||||
try {
|
||||
const res = await fetch(`http://${host}:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return { host, outcome: res.ok ? "ready" : "fast-reject" };
|
||||
} catch (err) {
|
||||
const outcome = err?.name === "TimeoutError" ? "hanging" : "error";
|
||||
return { host, outcome };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
outcomes.push(...results.map((r) => r.outcome));
|
||||
|
||||
// If either family is ready, the server is ready
|
||||
if (outcomes.includes("ready")) return "ready";
|
||||
|
||||
// If either family is fast-reject, treat as fast-reject
|
||||
// (TCP is listening and rejecting, just route not ready yet)
|
||||
if (outcomes.includes("fast-reject")) return "fast-reject";
|
||||
|
||||
// If either family is hanging, server accepted TCP but not answering
|
||||
// (still booting, must not report as ready per #6800)
|
||||
if (outcomes.includes("hanging")) return "hanging";
|
||||
|
||||
// Both families failed — check if either port is actually listening
|
||||
// If listening, then errors above are route-level (fast-reject case)
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
return listening ? "fast-reject" : "not-listening";
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${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");
|
||||
// #11766: check both IPv4 and IPv6 loopback. Return true if either is listening.
|
||||
const hosts = ["127.0.0.1", "::1"];
|
||||
const results = await Promise.all(
|
||||
hosts.map(
|
||||
(host) =>
|
||||
new Promise((resolve) => {
|
||||
const socket = net.connect({ host, port, timeout: 1000 });
|
||||
const finish = (ok) => {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
resolve(ok);
|
||||
};
|
||||
socket.once("connect", () => finish(true));
|
||||
socket.once("error", () => finish(false));
|
||||
socket.once("timeout", () => finish(false));
|
||||
})
|
||||
)
|
||||
);
|
||||
return results.some((ok) => ok);
|
||||
return new Promise((resolve) => {
|
||||
const socket = net.connect({ host: "127.0.0.1", port, timeout: 1000 });
|
||||
const finish = (ok) => {
|
||||
try {
|
||||
socket.destroy();
|
||||
} catch {}
|
||||
resolve(ok);
|
||||
};
|
||||
socket.once("connect", () => finish(true));
|
||||
socket.once("error", () => finish(false));
|
||||
socket.once("timeout", () => finish(false));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- feat(api): add an opt-in `modelVisibilityAllowlist`/`modelVisibilityDenylist` settings pair to curate exactly which models `/v1/models` advertises, mirrored into every `auto/*` combo candidate pool so a denied model cannot be routed to via combo selection either (#11481)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(guardrails):** enforce a bounded, deterministic contract for Video Bridge transcripts — 256 cues, 4096 input code units and 4 KiB UTF-8 per cue, 64 KiB total text, malformed-Unicode rejection, focus-window scoping, cross-source reconciliation with contributing-source metadata, and a structural provenance trust boundary so caller JSON can never self-assert `embedded`/`audio-bridge` provenance ([#11652](https://github.com/diegosouzapw/OmniRoute/issues/11652))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(video):** orchestrate optional Video Bridge audio extraction and Audio Bridge STT behind a dual opt-in (operator setting AND per-request signal) — a new loopback-only broker `mode=audio` operation shares the frame path's exact process queue, deadline, AbortSignal, and byte budgets to extract a bounded mono 16 kHz PCM WAV from the same already-downloaded video, then reuses the existing Audio Bridge transcription boundary; provider segment timing is preserved when available and marked coarse otherwise, and every failure degrades to a visual-only-safe partial instead of throwing (#11654).
|
||||
@@ -1 +0,0 @@
|
||||
- **test(video):** Add the Video Bridge FU-07/FU-09 promotion-evidence harness (#11656) — a frozen Zod manifest schema covering the 8 required scenario kinds (static scenes, rapid cuts, late facts, fades, blur, small text, close events, visual prompt injection) with a minimum of 3 repetitions per case, deterministic declarative fixture recipes (`videoBridgePromotionFixtures.ts`), a pure medians/p95 metrics aggregator, a pure FU-07/FU-09 promotion-verdict evaluator applying the ticket's exact thresholds (missing token usage always holds), a digest-only persistence layer that never retains raw media or raw model responses, and a versioned per-model promotion allowlist shipped empty with every model defaulting to `hold`. The FU-07/FU-09 promotion verdicts themselves remain HOLD — they require a real evidence run against real models on VPS 192.168.0.15.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(video bridge):** "embedded" transcript provenance can now be legitimately earned instead of merely asserted — a bounded, allowlisted (`mov_text`/`subrip`/`webvtt`) subtitle probe runs through the loopback-only Video Bridge broker (at most 2 streams, 10s subdeadline bounded by the request deadline, 256 KiB output, 4096-code-unit lines), normalized through a bounded, ReDoS-safe WebVTT parser and Zod-validated end to end. The adapter always resolves to an explicit `success`/`absent`/`transient_failure` outcome — a subtitle failure never breaks the visual description path, and only a fingerprint-verified broker response (never a caller-declared label) can produce embedded cues (#11659).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(ui):** enable React Compiler (`reactCompiler: true` + `babel-plugin-react-compiler`) for automatic memoization at build time ([#11783](https://github.com/diegosouzapw/OmniRoute/pull/11783)) — thanks @jonlwheat2-gif
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(zai):** add GLM-5.3-Flash Coding Plan support (1M context, 128K output, vision, `low|high|max` reasoning) and route `zai` GLM-5.3-family API-key traffic through the OpenAI-compatible Coding Plan endpoint with native thinking defaults ([#11801](https://github.com/diegosouzapw/OmniRoute/pull/11801)) — thanks @Neuron-Mr-White
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(nodejs):** add `5dive` as a `configure` target — `omniroute configure 5dive` / `omniroute setup-5dive` write a 5dive auth profile that points an agent fleet's `claude` seats at OmniRoute, with the root-only write, the loopback-vs-`https` endpoint rule and the per-seat model pin handled explicitly ([#11852](https://github.com/diegosouzapw/OmniRoute/pull/11852))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** treat `max` as a first-class reasoning-effort tier and clamp per model family (GLM 5.1+/DeepSeek V4+/Kimi K3+ keep native `max`; o1/MiniMax/Grok/Muse Spark clamp to their upstream ceiling) ([#11875](https://github.com/diegosouzapw/OmniRoute/pull/11875)) — thanks @Chewji9875
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** the provider plugin manifest now advertises a `usage-fetch` capability for the 40 providers that have a wired usage/quota fetcher, so external dashboards can read it from `GET /api/v1/provider-plugin-manifest` instead of parsing `open-sse/services/usage.ts` after every release. Discovery only — no new fetcher, no quota change, and the Dashboard quota widget stays gated by `USAGE_SUPPORTED_PROVIDERS`. `USAGE_FETCHER_PROVIDERS` moved to a zero-dependency leaf (`open-sse/services/usage/fetcherProviders.ts`) and is re-exported from `services/usage.ts`, keeping the manifest module a light leaf instead of pulling the ~490-module usage dispatcher into the manifest route. ([#11903](https://github.com/diegosouzapw/OmniRoute/pull/11903)) — thanks @maxmad64bis
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(plugins):** `OMNIROUTE_PLUGINS_DIR` sets the directory the runtime plugin scanner reads — and the root the plugin manager installs into — overriding the `HOME`-derived default, so a Docker/K8s deployment can point straight at its bind-mounted plugin tree instead of moving `HOME` just to relocate the scan path. An image that exports no home no longer scans `/tmp/.omniroute/plugins` in silence: the resolved directory is logged once at startup as `scanner.dir_resolved`, naming the input that won. Unset, behaviour is unchanged. Distinct from the CLI-only `OMNIROUTE_PLUGIN_PATH`, which finds `omniroute-cmd-*` command packages and never reached this scanner ([#11906](https://github.com/diegosouzapw/OmniRoute/pull/11906)) — thanks @amaleta
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(leases):** add an explicit owner-authenticated status action that returns only the active lease's privacy-safe configured connection and provider labels, with generation fencing and no credential or internal-id disclosure ([#11910](https://github.com/diegosouzapw/OmniRoute/pull/11910)) — thanks @KaspaPulse
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(routing):** add a `score` Auto router strategy that selects the highest configured weighted score and reuses `explorationRate`.
|
||||
@@ -1 +0,0 @@
|
||||
- Add a runtime feature flag to disable universal context handoffs globally without changing the default behavior.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** the provider plugin manifest now also advertises a `usage-supported` capability for the 46 providers whose usage API is accepted by the server and Dashboard routes, so integrators can distinguish "the server will serve quota for this provider" from "a fetcher is wired" without reading TypeScript. Discovery only — no fetcher or quota change. `usage-fetch` resolves on id or alias (the usage dispatcher accepts both); `usage-supported` resolves on id alone, matching the runtime guard `USAGE_SUPPORTED_PROVIDERS.includes(providerId)`. `USAGE_SUPPORTED_PROVIDERS` moved to a zero-dependency leaf (`open-sse/services/usage/supportedProviders.ts`) and is re-exported from `providers.ts`, mirroring the `fetcherProviders` leaf from #11903 and keeping the manifest a light module. ([#12214](https://github.com/diegosouzapw/OmniRoute/pull/12214)) — thanks @maxmad64bis
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(rankings):** order Free Provider Rankings by what each provider actually served — `GET /api/free-provider-rankings?sortBy=reliability` and a "Most reliable first" toggle on the page. Providers with too few calls to state a success rate keep their score order below the measured ones; the default order is unchanged ([#12218](https://github.com/diegosouzapw/OmniRoute/pull/12218)).
|
||||
@@ -1 +0,0 @@
|
||||
- **perf(sse):** defer `cloneLogPayload()` in the structured SSE collector until after the `maxEvents`/`maxBytes` cap check, eliminating ~9,800 wasted `structuredClone` calls per streaming response (65–71% faster `push()`). Reducer snapshot isolation restored for OpenAI and Responses summaries ([#12241](https://github.com/diegosouzapw/OmniRoute/pull/12241)) — thanks @PauloHSOliveira
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** Devin CLI agentic quota (Codeium seat-management GetUserStatus) and OpenRouter key limits plus account credits now surface in Provider Limits ([#12256](https://github.com/diegosouzapw/OmniRoute/pull/12256) — thanks @Neuron-Mr-White)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(auto-combo):** Auto-Combo scoring can now weigh how often a provider/model has actually succeeded. The engine already carried that number on every candidate — 24 hours of usage history behind a ten-sample floor, real-time metrics otherwise — and the scoring function never read it, while the weight table described `stability` as if it did. `reliability` (`1 - failureRate`, with the same field precedence and the same rate-bounding the speed ranking already uses, so a corrupt reading means "nothing observed" rather than "fails every call") is now a declared factor shipping at weight `0`, so routing is unchanged until an operator gives it one, and the `stability` description now matches what that factor computes ([#12317](https://github.com/diegosouzapw/OmniRoute/pull/12317))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(routing):** With `freeAccessPolicy: "strict"`, the read-only candidate listing (`GET /v1/auto-combo/{channel}/candidates`) no longer hides the candidates the zero-cost guard excludes — the same read-only transparency the resilience filter already honours (#9133). Each candidate now carries `freeAccessExclusion` saying why it would be kept out, and it tells an exhausted allowance apart from a quota reading that never arrived or went stale, which used to look identical from the outside. Routing is unchanged: the listing reports, it never enforces. The separate `excludeTosAvoid` guard still drops its candidates without a reason; that gap is now documented rather than closed ([#12319](https://github.com/diegosouzapw/OmniRoute/pull/12319))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** The Radar catalog table now shows two facts it was already receiving from the feed and dropping on the floor: the per-model rate limits (requests and tokens, per minute and per day) in a new column, and a badge when a provider's terms state it may train on the prompts you send. A limit of zero renders as zero rather than "rate-only" — for a ceiling those are opposite facts — and a model with no training statement gets no badge, because an absent statement is not a guarantee ([#12320](https://github.com/diegosouzapw/OmniRoute/pull/12320))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** explain Community, single-use, contributor, supporter, recovery, abuse, offers, and privacy rules before either Radar activation action, and remove the superseded fixed-PR grant promise from every UI locale ([#12342](https://github.com/diegosouzapw/OmniRoute/pull/12342))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(gamification):** the dashboard Profile page now shows the real daily streak — `/api/gamification/level` returns `streak: { current, longest }` (per key with `apiKeyId`, operator-wide maximum otherwise) and the streak card reads it instead of a hard-coded 0 (#2403)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(gamification):** the dashboard leaderboard now shows each API key's display name under the Name column instead of a truncated key id; `GET /api/gamification/leaderboard` attaches `name` per entry (name only — no key material), while the shared ranking helper and the federation leaderboard stay id-only — thanks @pacocartones
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(gamification):** enforce the documented 1000 XP/min per-API-key anti-cheat rate limit on the XP award path; over-limit awards are logged and skipped instead of persisted, and the sliding window now matches the timestamp format stored in `xp_audit_log` ([#2403](https://github.com/diegosouzapw/OmniRoute/issues/2403))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(admin):** localize the gamification anomalies page — the loading state, the Status column and the Suspicious badge now come from the `common` catalog (new `common.suspicious` key propagated to every locale) — add it to the Gamification sidebar group as `gamification-admin` (`/dashboard/gamification/admin`), and expose the loading and empty states as polite `role="status"` live regions (#12401 — thanks @pacocartones)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(dashboard):** display clamped `[0, 100]%` cached input token ratio in request logs table ([#PR_NUMBER](https://github.com/diegosouzapw/OmniRoute/pull/PR_NUMBER))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(catalog):** add `OMNIROUTE_DISABLE_THINKING_LEVEL_VARIANTS` feature flag to optionally filter out thinking level variants from model catalog ([#PR_NUMBER](https://github.com/diegosouzapw/OmniRoute/pull/PR_NUMBER))
|
||||
@@ -1,11 +0,0 @@
|
||||
- **feat(dashboard):** continuously export call logs to external analytics stores. A pluggable
|
||||
destination registry ships the full Logs-tab record set on an hourly `JobRegistry` cron, with
|
||||
a persisted per-destination cursor, batched inserts, a config UI rendered from each
|
||||
destination's own field descriptors, and a REST layer (`/api/log-export/*`) for CRUD, a
|
||||
connection test, and an on-demand run. A destination can opt into `includeBodies` to also ship
|
||||
the request and response payloads shown in the Logs detail pane, including the client and
|
||||
provider views of each call; this is off by default, and payloads inherit the dashboard's PII
|
||||
sanitisation, secret redaction and `noLog` handling. Google BigQuery is the first destination,
|
||||
using a service-account key stored encrypted at rest and streaming inserts keyed by call-log id,
|
||||
into a table that is day-partitioned on `timestamp` and clustered on `api_key_name`, `provider`,
|
||||
`model` and `status`.
|
||||
@@ -1,6 +0,0 @@
|
||||
- **feat(providers):** add MaxAI as a signed, OpenAI-compatible provider serving its 13 paid chat models (GPT-5.6 / Luna / Thinking, Claude 5 Sonnet, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite, Grok 4.1-fast / 4.5, DeepSeek V3.2 / R1, Llama 3.3 70B) through OmniRoute's `/v1` endpoint, with per-request HMAC-SHA1→SM3→AES request signing, live model + context-window discovery from `/models/get_config`, and prompted tool-calling translated to OpenAI `tool_calls`
|
||||
- **feat(providers):** MaxAI vision input — image_url content parts are forwarded inline in `message_content` to the 6 vision-capable models (GPT-5.6 / Luna / Thinking, Claude Haiku 4.5, Gemini 3.1 Pro / Flash-Lite)
|
||||
- **feat(providers):** MaxAI image generation — 6 image models (gpt-image-1, dall-e-3, flux-1-schnell/dev/pro, sd3-medium) exposed through `POST /v1/images/generations`
|
||||
- **feat(providers):** MaxAI document RAG — inline base64 file/document attachments are uploaded to MaxAI (content-addressed `doc_id`) and attached to the chat via `doc_list`
|
||||
- **feat(providers):** browserless MaxAI onboarding — email device-pair login (`/api/providers/[id]/login`) and signed access-token refresh, so a connection can be created and kept fresh without a real browser or Google OAuth
|
||||
- **feat(providers):** per-provider TLS impersonation profile (MaxAI presents a Windows Firefox-150 client fingerprint) so its bot-sensitive endpoints accept OmniRoute traffic
|
||||
@@ -1,5 +0,0 @@
|
||||
- **feat(dashboard):** the `/dashboard/orchestration` snapshot hook now subscribes to the
|
||||
`agents` WebSocket channel (`agent.task.updated`) instead of `requests` as its refetch
|
||||
trigger, and relaxes its background poll from 5s to 30s while that WS connection is up —
|
||||
falling back to the tighter 5s cadence, reprogrammed live on any connect/disconnect
|
||||
transition, whenever the socket is down.
|
||||
@@ -1,11 +0,0 @@
|
||||
- **feat(dashboard):** Orchestration canvas quick wins — search box plus state/source/provider
|
||||
filter chips with a one-click clear, and per-source collapse/expand, all reflected in the URL
|
||||
so a filtered/collapsed view is shareable and survives a refresh; the detail drawer gained a
|
||||
"copy trace JSON" action and hardened error/empty-state and accessibility handling; the
|
||||
Agents-tab edges now animate traveling particles along active (running) connections; and the
|
||||
canvas node/edge status colors moved off fixed hex values onto theme-aware `--orch-status-*`
|
||||
CSS custom properties, so they adapt correctly to light/dark mode.
|
||||
- **chore(dashboard):** Orchestration UI hardening pass and the missing component/model test
|
||||
coverage it called for — `OrchestratorNode`/`ActivityNode`/`OverflowNode` rendering, the
|
||||
`?node=`/overflow-click page routing, the Agents-tab orchestrator-click no-op and
|
||||
`showCompleted` toggle, and the overview kanban's done-column sort order (#12270, #12271).
|
||||
@@ -1 +0,0 @@
|
||||
- New `/dashboard/orchestration` page: live unified view of everything running — Cloud Agent, A2A and Conductor as a real-time graph (Agents tab), the combo cascade (Routing tab, reusing the Combo Live Studio) and a state kanban (Overview tab), with a detail drawer (trace, cost, approve/cancel). Read-only over existing APIs — no new backend. Canvas concept credit: PR #11815 design
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add a Perplexity Agent API provider (`perplexity-agent` / `pplx-agent`) for Perplexity `/v1/responses`, including the documented Anthropic, OpenAI, Google, xAI, DeepSeek, Z.AI, Moonshot/Kimi, NVIDIA, and Perplexity model IDs plus Anthropic-model `max_output_tokens` compatibility.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** add RPD (Requests Per Day) limit to provider rate limit overrides across UI, schemas, DB, and i18n ([#PR_NUMBER](https://github.com/diegosouzapw/OmniRoute/pull/PR_NUMBER))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers): add UC Direct (uncensored.com Developer API), the metered OpenAI-compatible surface.** A standard OpenAI-compatible passthrough (default executor) for uncensored.com's official REST API at `https://api.uncensored.com/api/v1`: `X-api-key` auth (never-expiring `uai_sk_live_` key), `POST /chat/completions` with streaming SSE and native tool-calling, and the full live metered catalog (82 models across 15 providers, discovered from the public `GET /v1/models`). Registered as provider `uc-direct` (alias `ucd`). Complements the un-metered `uc` persona provider — same models, metered credits and a plain API key instead of a subscription session.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers): add UC (uncensored.com), the un-metered subscription "persona" chat as an OpenAI-compatible provider.** A WebSocket web-app port: a durable Clerk credential mints a short-lived session token per connect (browserless — no API key), driving UC's persona socket. Ships the browserless email-code login (request → verify → harvest), the 19 verified persona models (Claude Opus, Gemini, Grok, GLM, Kimi, DeepSeek, MiniMax, incl. the uncensored variants), prompted `<tool>` tool-calling with a per-model code-style dialect + auto-cure retry for guardrailed models, live `<think>`/reasoning split, streaming + non-streaming OpenAI responses, and full quota/auth error surfacing (paywall / message-limit / rate-limit → 429, invalid session → 401 re-login). Full multimodal parity via the persona blob-upload layer: **vision** (image input, 15 vision-capable models), **document RAG** (PDF/doc upload, server-side extraction), **image generation** (22 models), **video generation** (14 models, async signed-url → poll), and **TTS** (streaming MP3). Registered as provider `uc` (alias `ucn`). The metered OpenAI-compatible Developer API is a separate `uc-direct` provider.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** Keep local and theme-aware provider SVG icons at a definite layout size so Chromium does not collapse them to 0×0 after the v3.8.50 image-rendering change ([#12054](https://github.com/diegosouzapw/OmniRoute/pull/12054)) — thanks @ponkcore
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(kie):** reroute `flux/kontext` off the KIE Market `createTask` flow — it is catalogued with `isMarket: true` but has no Market catalog page, so KIE rejected it with "model name not supported"; it now hits the dedicated `POST /api/v1/flux/kontext/generate` / `GET /api/v1/flux/kontext/record-info` endpoints instead (#11296).
|
||||
@@ -1,3 +0,0 @@
|
||||
- Fixed the v3.8.50 Costs and Analytics dashboards so flat-rate Claude Code usage can be shown as an explicitly requested token-price estimate without changing default billed-cost semantics.
|
||||
- Fixed archived usage retention so each request is priced individually instead of pricing a day's summed tokens once, which understated archived cost whenever a day mixed cache-heavy and ordinary requests.
|
||||
- Fixed the Costs dashboard so it discloses when displayed figures include flat-rate token-price estimates instead of labelling them as billed spend, using the flag the analytics API already returns; the month-end projection and the CSV/JSON exports carry the same marker, and billed-cost mode is unchanged.
|
||||
@@ -1 +0,0 @@
|
||||
- fix(db): rate-limit repeated Arena ELO leaderboard fetch-failure warnings instead of logging one per sync attempt (#11500)
|
||||
@@ -1,5 +0,0 @@
|
||||
- Dropped the stale-`.eslintcache` `restore-keys` fallback from both "Restore ESLint file
|
||||
cache" steps in `ci.yml`, so the blocking `Lint` job can no longer be served per-file
|
||||
verdicts computed under a different lint config, suppressions file or lockfile. `quality.yml`
|
||||
had already dropped it in #11963; `ci.yml` — the workflow that actually gates PRs — had not
|
||||
(#11600).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(ci): pass `--pass-on-unpruned-suppressions` in `run-eslint-json.mjs` so the CI Lint job no longer fails when a suppression is merely orphaned by a genuine fix, mirroring the identical fix already in `validate-release-green.mjs` (#11600)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(build):** `prepublish.ts` bundles the ChatGPT Web (Codex) MCP bridge through `runBuildTool()` instead of spawning `npx.cmd` raw, fixing the build crash on Node ≥ 20/Windows where `.cmd` shims cannot be spawned without a shell (EINVAL) ([#11704](https://github.com/diegosouzapw/OmniRoute/issues/11704))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(codex): keep `parallel_tool_calls:false` on the translated Codex Responses Lite path (#11707)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(packages/browser-pool):** regenerate `package-lock.json` so the `packages/browser-pool` workspace's locked `playwright`/`@types/node` (and transitives) match its `package.json` specs, fixing cache-only/offline installs (`npm ci --offline`, Nix `buildNpmPackage`) that previously failed with `ENOTCACHED` ([#11747](https://github.com/diegosouzapw/OmniRoute/issues/11747)) — thanks @benjaminkitt
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(usage):** quota and usage refresh no longer 409 when an exclusive lease reserves the connection ([#11758](https://github.com/diegosouzapw/OmniRoute/pull/11758)) — thanks @TheDemonTuan
|
||||
@@ -1,5 +0,0 @@
|
||||
- Keep the embedding registry's vector width and `embedding` type on models when a synced model exists
|
||||
for the same id, so `/v1/models` no longer reports registry-described embedding models widthless or
|
||||
untyped (#11761)
|
||||
- Correct `google/gemini-embedding-001` on the OpenRouter route to 3072 dimensions, the width it
|
||||
returns when `dimensions` is not sent (#11761)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): stop prepublish from re-rebuilding the already-built ESM-only opencode-plugin dist (#11787)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): set X-OmniRoute-Selected-Connection-Id on successful combo dispatches so downstream consumers stop falling back to an empty connection id (#11810)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** Antigravity's dynamic mitmAlias table no longer routes `gemini-3.7-flash-{high,medium,low}` to a literal tier-suffixed upstream id just because one connected account's own discovery listed it directly — those display ids always resolve through the safe `gemini-3.7-flash-tiered` static alias, so one account's Google-provisioned access no longer 404s every sibling account of the provider ([#11824](https://github.com/diegosouzapw/OmniRoute/issues/11824), [#11651](https://github.com/diegosouzapw/OmniRoute/issues/11651))
|
||||
@@ -1 +0,0 @@
|
||||
- **perf(compression):** OOM mitigations for large payload hashing, memoization, and token estimation ([#11844](https://github.com/diegosouzapw/OmniRoute/pull/11844) — thanks @AndrianBalanescu)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(config):** Nous Research's `Hermes-4-405B` model now displays as "Hermes 4 405B (Nous Research)" in both the provider registry and the free-model catalog, instead of the mislabelled "Hermes 4 7B" ([#11861](https://github.com/diegosouzapw/OmniRoute/issues/11861)) — thanks @Karan825
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(provider/nous):** inject required user= tag into Nous Research inference requests to resolve upstream 400 "missing tags" error ([#11861](https://github.com/diegosouzapw/OmniRoute/issues/11861)) — thanks @Karan825
|
||||
@@ -1,10 +0,0 @@
|
||||
- **fix(build):** `npm run build` now fails in one second with a named package and a
|
||||
copy-pasteable fix when npm silently drops an externalised optional native
|
||||
dependency, instead of dying four minutes in with `Module not found: Can't resolve
|
||||
'better-sqlite3'` ([#11863](https://github.com/diegosouzapw/OmniRoute/pull/11863)) —
|
||||
thanks @ujjawalkaushik1110
|
||||
- **fix(install):** `postinstall` no longer throws `ReferenceError: isAndroid is not
|
||||
defined` — failing the whole `npm install` — when the `better-sqlite3` rebuild
|
||||
fallback times out; the manual-fix guidance is reachable again
|
||||
([#11863](https://github.com/diegosouzapw/OmniRoute/pull/11863)) — thanks
|
||||
@ujjawalkaushik1110
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** `omniroute update --apply` now tells you explicitly whether a running server was detected and, if so, that you must run `omniroute restart` to apply the update — it never restarted anything and previously implied the update was already live once files were installed. The dashboard's npm-mode Update flow (`/api/system/version`) now tries OmniRoute's own PID-file-managed supervisor before falling back to pm2, and reports an honest "restart required" step instead of a silent pm2-only "skipped" that read like a completed update. The server-side latest-version lookup backing the dashboard's update banner also gained `--prefer-online`, closing the same stale-npm-cache class already fixed in the CLI's own copy for #4376 ([#11885](https://github.com/diegosouzapw/OmniRoute/issues/11885)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** clear persisted LKGP pins when a target suffers connection/provider exhaustion or is skipped before dispatch due to cooldown/exhaustion/unavailability, preventing subsequent requests from repeatedly prioritizing known-dead providers ([#11911](https://github.com/diegosouzapw/OmniRoute/issues/11911)).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(ollama): preserve multi-byte UTF-8 content split across stream chunks in the Ollama NDJSON transform, which previously corrupted CJK/emoji into U+FFFD (#11921)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** OrcaRouter chat requests now target `/v1/chat/completions` instead of the bare `/v1` API root, fixing the upstream `404 Invalid URL (POST /v1)` ([#11923](https://github.com/diegosouzapw/OmniRoute/pull/11923)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(plugins):** deliver the `onStreamComplete` event to disk-installed plugins. The event shipped in v3.8.50 (#9669) was emitted internally but had no plugin-facing wiring, so no plugin could ever subscribe: the manifest schema silently dropped `hooks.onStreamComplete`, and the loader/manager only knew the seven legacy hooks. `onStreamComplete` is now a declarable manifest hook, wired through the loader and registered by the manager like the other hooks, and its payload carries a `requestId` so consumers can correlate the stream-completion event with the originating request ([#11934](https://github.com/diegosouzapw/OmniRoute/pull/11934)) — thanks @amaleta
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** the Qdrant embedding-model dropdown now lists local/self-hosted providers (Ollama, LM Studio, vLLM, etc.) — an active connection is treated as "configured" when the provider allows an optional API key, not only when it has a real key or OAuth, so a running local embedding provider is no longer hidden from the picker ([#11949](https://github.com/diegosouzapw/OmniRoute/issues/11949))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** Vertex AI Anthropic partner-model discovery now calls the Model Garden `v1beta1` publisher list (`/v1beta1/publishers/anthropic/models`, global) and parses its `publisherModels` envelope, so Claude models auto-synced from Vertex populate the active live catalog and route at request time instead of returning `Model '<id>' is not available in the active live catalog` ([#11991](https://github.com/diegosouzapw/OmniRoute/issues/11991)) — thanks @fabioluissilva
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** support `--base-url` alongside `--endpoint` in `omniroute nodes add`, `update`, and `validate` subcommands to prevent global `--base-url` shadowing issues ([#11999](https://github.com/diegosouzapw/OmniRoute/issues/11999)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** `cloudflare-ai` no longer refuses image content parts for every Workers AI model ([#12002](https://github.com/diegosouzapw/OmniRoute/pull/12002)) — the plain-string `content` requirement behind #2539 is carried by the _model_ schema, not by the `/ai/v1/chat/completions` endpoint (measured: an all-text part array returns 200 on `@cf/mistralai/mistral-small-3.1-24b-instruct`, `@cf/meta/llama-4-scout-17b-16e-instruct` and `@cf/meta/llama-3.3-70b-instruct-fp8-fast`, and 400 on the text-only `@cf/qwen/qwen2.5-coder-32b-instruct`). `transformRequest()` flattened every array and threw on the first non-text part (#6390), so image input was refused for vision-capable Cloudflare models that accept it. All-text arrays are still flattened — the one shape every model accepts — while an array carrying a non-text part is passed through untouched, so the attachment is still never silently dropped. Regression guards: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** decouple the limiter-managed execution backstop from the queue-wait budget — new `requestQueue.executionMaxWaitMs` (env `RATE_LIMIT_EXECUTION_MAX_WAIT_MS`, default 600000 = 10 min) now feeds Bottleneck's post-dispatch `expiration`, while `requestQueue.maxWaitMs` keeps its documented queue-wait semantics. Previously the queue-wait budget doubled as the execution expiration, so legitimate long-running calls on non-incremental gateways (whole generation buffered before the first upstream byte, e.g. Console Go / Command Code tiers serving GLM models) were killed mid-flight at the queue budget with a false 504 `RATE_LIMIT_EXECUTION_TIMEOUT` — the local limiter undercut the provider-aware upstream fetch-start timeouts. The surfaced 504 message now names `requestQueue.executionMaxWaitMs`; the error keeps the #4165 guarantees (disclaims an upstream timeout, preserves the Bottleneck error as `cause`, branded code + trusted provenance, classified request-scoped so combo falls back). A real queue-wait bound (the `Promise.race` around `limiter.schedule()` sketched in #9533) remains future work. (#12025)
|
||||
@@ -1 +0,0 @@
|
||||
- **Call logs:** keep the `error` field when an artifact exceeds the storage cap, instead of replacing it with the omission marker. The error is the only field that says *why* a request failed and is typically ~90 bytes next to the multi-hundred-KB bodies that trip the cap, so dropping it left a size-limited row undiagnosable — a provider outage, a local timeout and an upstream 400 all rendered identically. It is now preserved at every fallback stage, truncated to 4KB if it is itself large ([#12026](https://github.com/diegosouzapw/OmniRoute/issues/12026)).
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user