mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-27 18:32:16 +03:00
Compare commits
169 Commits
main
...
release/v3
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6f1aa2bb3f | ||
|
|
c096464a33 | ||
|
|
f777f252ba | ||
|
|
5d0cf47e29 | ||
|
|
a1a5df0970 | ||
|
|
68c56b699e | ||
|
|
b1a0b93371 | ||
|
|
8fb3a2c379 | ||
|
|
ceff7054b1 | ||
|
|
a533a312df | ||
|
|
d776a42324 | ||
|
|
4f294c0cca | ||
|
|
0d3d31c21f | ||
|
|
526048da5e | ||
|
|
9a33cdac1d | ||
|
|
c3cef782ac | ||
|
|
958260a5c9 | ||
|
|
f6b4926138 | ||
|
|
e45e6c3e34 | ||
|
|
69d2b31930 | ||
|
|
95708051a2 | ||
|
|
b681308259 | ||
|
|
17da3b6e09 | ||
|
|
27867e5af3 | ||
|
|
62a74faa4b | ||
|
|
a03c22e5ed | ||
|
|
04335944ea | ||
|
|
3cc48edb35 | ||
|
|
58f53e3a35 | ||
|
|
18fa0b2651 | ||
|
|
f5147d00f9 | ||
|
|
62b1bc9d05 | ||
|
|
8db5a665d6 | ||
|
|
ecf3d3aa06 | ||
|
|
c7c7d476a3 | ||
|
|
b7ac5261a5 | ||
|
|
c6a80071f1 | ||
|
|
437ca488b0 | ||
|
|
b67f2c58da | ||
|
|
2ecaae7c40 | ||
|
|
259d9b0f38 | ||
|
|
bd65eeb045 | ||
|
|
d415cc0216 | ||
|
|
cfbc2c27c2 | ||
|
|
58bb2cd8c7 | ||
|
|
4b1c1859f8 | ||
|
|
f0b085ebca | ||
|
|
1636ace600 | ||
|
|
7a7a72c6f4 | ||
|
|
465f0a0e83 | ||
|
|
a73c6ca5fb | ||
|
|
a7e0dddac4 | ||
|
|
d12ac37d4d | ||
|
|
fa3a09cb43 | ||
|
|
d11bf528af | ||
|
|
ac96c0dd02 | ||
|
|
6ea7c68e6e | ||
|
|
c8e94d7a14 | ||
|
|
a2fabdde8f | ||
|
|
2f4b793c3d | ||
|
|
cd4a720b7e | ||
|
|
4a2172e569 | ||
|
|
53aa6d9716 | ||
|
|
d98a31587e | ||
|
|
f1a02f602b | ||
|
|
c50a83a94d | ||
|
|
76b1b04495 | ||
|
|
e3d29d1419 | ||
|
|
db7a6c2437 | ||
|
|
6fff4d6df1 | ||
|
|
f60090b278 | ||
|
|
0086f1772b | ||
|
|
49795c24ef | ||
|
|
049bad494b | ||
|
|
b796f2b457 | ||
|
|
fb3da7ce98 | ||
|
|
8853b257aa | ||
|
|
0785cd2e55 | ||
|
|
a0ab693c4b | ||
|
|
a69547a7f5 | ||
|
|
0776f83fd0 | ||
|
|
f3d285ba90 | ||
|
|
c0430342c6 | ||
|
|
5d07bdd76f | ||
|
|
ab8b41bdaa | ||
|
|
1f419cf253 | ||
|
|
192f38d58a | ||
|
|
264dda77be | ||
|
|
5ecca12aa5 | ||
|
|
b74c63a39e | ||
|
|
6c1d597d42 | ||
|
|
6f41775a1c | ||
|
|
ffe825b6b8 | ||
|
|
dd12539a2c | ||
|
|
5c953d1f51 | ||
|
|
1ad8b3b637 | ||
|
|
efc92c6955 | ||
|
|
aabefc8026 | ||
|
|
8e33393668 | ||
|
|
234956ddf5 | ||
|
|
01ce92a3c0 | ||
|
|
ddd546483f | ||
|
|
b6ffe8ce20 | ||
|
|
f680aacff0 | ||
|
|
dc5ae96905 | ||
|
|
265d93ffd8 | ||
|
|
bf1481f11f | ||
|
|
fecf888fd9 | ||
|
|
509fd5425c | ||
|
|
faf68a222f | ||
|
|
8a2b522a53 | ||
|
|
bfd8a6533f | ||
|
|
c26984e9bd | ||
|
|
046093b9cb | ||
|
|
f35839fe01 | ||
|
|
143b7b13c4 | ||
|
|
fc16dcd6ee | ||
|
|
f237c07def | ||
|
|
9899a6dacc | ||
|
|
4d330dafa9 | ||
|
|
9285dc1b89 | ||
|
|
149b086d0f | ||
|
|
1044821bda | ||
|
|
cefbcfb278 | ||
|
|
776a7a3a58 | ||
|
|
5531fc7f05 | ||
|
|
826a66f287 | ||
|
|
58abebd106 | ||
|
|
c347abb774 | ||
|
|
f26aa16da3 | ||
|
|
0e0ca7e26b | ||
|
|
7e2b839935 | ||
|
|
9ebb53e432 | ||
|
|
9827ae6137 | ||
|
|
9b986fa220 | ||
|
|
05857018f4 | ||
|
|
5acfbe882f | ||
|
|
b834c740d1 | ||
|
|
44e85a7d40 | ||
|
|
74546ed854 | ||
|
|
00c74ea6c4 | ||
|
|
e755c5ac68 | ||
|
|
b1e27258c0 | ||
|
|
dc7eeba717 | ||
|
|
b074c6d75e | ||
|
|
5e3a95be4f | ||
|
|
0ed6780798 | ||
|
|
6a12ba07b1 | ||
|
|
6816bcdaf3 | ||
|
|
286fdf8794 | ||
|
|
b3a2cfe0ea | ||
|
|
f2ad9b23bd | ||
|
|
8a7b62ec85 | ||
|
|
e44f125992 | ||
|
|
cf6c2798b4 | ||
|
|
1473261c4c | ||
|
|
adde9e4bae | ||
|
|
cbc16af286 | ||
|
|
670d502bd9 | ||
|
|
201908df5e | ||
|
|
8cb7f00821 | ||
|
|
c04ce386f1 | ||
|
|
2d5bd41261 | ||
|
|
1e59d14143 | ||
|
|
059dbe9f13 | ||
|
|
5a4bde1879 | ||
|
|
0757503eae | ||
|
|
bb62c2a618 | ||
|
|
7a098a0d85 |
105
.env.example
105
.env.example
@@ -84,14 +84,10 @@ PORT=20128
|
||||
# API_HOST=0.0.0.0
|
||||
# DASHBOARD_PORT=20128
|
||||
|
||||
# Connection backpressure: cap concurrent in-flight chat connections (503 + Retry-After when full).
|
||||
# Used by: src/sse/utils/backpressure.ts — disabled when unset/0.
|
||||
# OMNI_MAX_CONCURRENT_CONNECTIONS=0
|
||||
|
||||
# Port for the real-time WebSocket live monitoring server.
|
||||
# Used by: src/server/ws/liveServer.ts, src/app/api/v1/ws/route.ts
|
||||
# Default: 20132
|
||||
# LIVE_WS_PORT=20132
|
||||
# Default: 20129
|
||||
# LIVE_WS_PORT=20129
|
||||
|
||||
# Bind address for the live WebSocket server.
|
||||
# Default: 127.0.0.1 (loopback only). Set to 0.0.0.0 to expose on LAN —
|
||||
@@ -116,14 +112,16 @@ PORT=20128
|
||||
|
||||
# Public URL for the live dashboard WebSocket (client-side, browser only).
|
||||
# Set this when fronting the WS server with a reverse proxy or Cloudflare Tunnel.
|
||||
# The browser will connect to this URL instead of ws://hostname:20132.
|
||||
# The path portion of this URL (e.g. ws://localhost:20132/live-ws -> /live-ws) is also used by the dev proxy
|
||||
# (scripts/dev/standalone-server-ws.mjs) and the handshake response to route
|
||||
# WebSocket upgrades. Default path: /live-ws.
|
||||
# Used by: src/hooks/useLiveDashboard.ts, src/app/api/v1/ws/route.ts,
|
||||
# scripts/dev/standalone-server-ws.mjs, and scripts/start-ws-server.mjs.
|
||||
# The browser will connect to this URL instead of ws://hostname:20129.
|
||||
# The /live-ws path is already proxied from the main app (port 20128) to the
|
||||
# live WS server (port 20129) by scripts/dev/standalone-server-ws.mjs.
|
||||
# Used by: src/hooks/useLiveDashboard.ts
|
||||
# Example: NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=wss://ws.my-ai.com/live-ws
|
||||
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=ws://localhost:20132/live-ws
|
||||
# NEXT_PUBLIC_LIVE_WS_PUBLIC_URL=
|
||||
|
||||
# Disable the standalone live WebSocket helper used by scripts/start-ws-server.mjs.
|
||||
# Used by: scripts/start-ws-server.mjs (CI/embedded harness toggle).
|
||||
# OMNIROUTE_DISABLE_LIVE_WS=0
|
||||
|
||||
# Enable the real-time dashboard WebSocket server.
|
||||
# Used by: src/server/ws/liveServer.ts, scripts/start-ws-server.mjs
|
||||
@@ -176,12 +174,6 @@ OMNIROUTE_USE_TURBOPACK=1
|
||||
# hints in production logs.
|
||||
# OMNIROUTE_PROXY_FETCH_DEBUG=true
|
||||
|
||||
# Set to any non-empty value to emit `[omniroute completion]` diagnostics from
|
||||
# the CLI shell-completion cache paths (read/refresh/write) in
|
||||
# bin/cli/commands/completion.mjs. Off by default — these caches fail silently
|
||||
# so a missing/corrupt cache never breaks tab-completion.
|
||||
# OMNIROUTE_DEBUG_COMPLETION=1
|
||||
|
||||
# Docker production port mappings (docker-compose.prod.yml only).
|
||||
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
|
||||
# PROD_DASHBOARD_PORT=20130
|
||||
@@ -199,9 +191,9 @@ OMNIROUTE_USE_TURBOPACK=1
|
||||
# the machine name by bash/zsh. The .env loader cannot override it (first-wins
|
||||
# semantics). Use OMNIROUTE_SERVER_HOST instead for `omniroute serve`.
|
||||
# See: https://github.com/diegosouzapw/OmniRoute/issues/6194
|
||||
# HOST=0.0.0.0
|
||||
# HOSTNAME=127.0.0.1
|
||||
# OMNIROUTE_SERVER_HOST=0.0.0.0
|
||||
#HOST=0.0.0.0
|
||||
#HOSTNAME=127.0.0.1
|
||||
#OMNIROUTE_SERVER_HOST=0.0.0.0
|
||||
|
||||
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
|
||||
# Values: production | development | Default: production
|
||||
@@ -213,17 +205,6 @@ NODE_ENV=production
|
||||
# gives the correct fix instructions (podman unshare chown vs sudo chown).
|
||||
CONTAINER_HOST=docker
|
||||
|
||||
# Container runtime override for skill sandboxing.
|
||||
# Used by: src/lib/skills/sandbox.ts + src/lib/skills/containerProvider.ts
|
||||
# Values: auto | docker | apple | wsl | orbstack | podman
|
||||
# - auto: OS-aware auto-detect (apple/orbstack on macOS, wsl on Windows, podman on Linux)
|
||||
# - apple: Apple Container (native OCI on macOS 26+)
|
||||
# - wsl: WSL Container CLI (wslc.exe on Windows)
|
||||
# - orbstack: OrbStack (high-perf Linux VM + docker shim on macOS)
|
||||
# - podman: Podman (rootless, daemonless)
|
||||
# - docker: Docker (default fallback)
|
||||
SKILLS_SANDBOX_RUNTIME=auto
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 4. SECURITY & AUTHENTICATION
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -505,9 +486,7 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
|
||||
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
|
||||
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
|
||||
# OpenCode Go has no public quota API — this has no default and stays
|
||||
# unset unless you explicitly opt in to a self-hosted/mirrored endpoint:
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=
|
||||
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
|
||||
#OMNIROUTE_OPENCODE_GO_DASHBOARD_URL=https://opencode.ai/workspace
|
||||
#OMNIROUTE_OLLAMA_CLOUD_USAGE_URL=https://ollama.com/settings
|
||||
|
||||
@@ -987,7 +966,7 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
|
||||
# Update these when providers release new CLI versions to avoid blocks.
|
||||
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
|
||||
CLAUDE_USER_AGENT="claude-cli/2.1.195 (external, cli)"
|
||||
|
||||
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
|
||||
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) —
|
||||
@@ -996,7 +975,7 @@ CLAUDE_USER_AGENT="claude-cli/2.1.207 (external, cli)"
|
||||
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
|
||||
# forward the original names verbatim (debugging only).
|
||||
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
|
||||
CODEX_USER_AGENT="codex-cli/0.144.1 (Windows 10.0.26200; x64)"
|
||||
CODEX_USER_AGENT="codex-cli/0.142.0 (Windows 10.0.26200; x64)"
|
||||
GITHUB_USER_AGENT="GitHubCopilotChat/0.54.0"
|
||||
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
|
||||
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
|
||||
@@ -1017,7 +996,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
|
||||
# Override Codex client version sent in headers independently of the
|
||||
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
|
||||
# CODEX_CLIENT_VERSION=0.144.1
|
||||
# CODEX_CLIENT_VERSION=0.142.0
|
||||
|
||||
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
|
||||
# from the Codex Responses stream. These frames break the OpenAI SDK's
|
||||
@@ -1533,11 +1512,6 @@ APP_LOG_TO_FILE=true
|
||||
# Timeout for fast-fail health checks (ms). Default: 2000
|
||||
# PROXY_FAST_FAIL_TIMEOUT_MS=2000
|
||||
|
||||
# Time window (hours) for calculating the average latency of candidate proxies
|
||||
# in the latency-optimized pool strategy. Default: 3
|
||||
# Used by: src/lib/db/proxies.ts
|
||||
# PROXY_LATENCY_WINDOW_HOURS=3
|
||||
|
||||
# Health check result cache TTL (ms). Default: 30000 (30s)
|
||||
# PROXY_HEALTH_CACHE_TTL_MS=30000
|
||||
|
||||
@@ -1656,19 +1630,10 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/utils/cursorImages.ts.
|
||||
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
|
||||
|
||||
# Cursor state DB path override (for IDE cursor version detection).
|
||||
# Cursor state DB path override (for cursor version detection).
|
||||
# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically.
|
||||
# CURSOR_STATE_DB_PATH=
|
||||
|
||||
# Cursor Agent CLI build id for AgentService/Run impersonation (YYYY.MM.DD-<hash>).
|
||||
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: detect local install, else pin.
|
||||
# CURSOR_AGENT_CLI_VERSION=2026.07.08-0c04a8a
|
||||
|
||||
# Cursor Agent CLI data directory override (versions live under <dir>/versions/).
|
||||
# Used by: open-sse/utils/cursorAgentCliVersion.ts. Default: ~/.local/share/cursor-agent (unix)
|
||||
# or %LOCALAPPDATA%\cursor-agent (win32). Official agent CLI also honors this var.
|
||||
# CURSOR_DATA_DIR=
|
||||
|
||||
# Direct Cursor bearer token used by scripts/ad-hoc/cursor-tap.cjs (developer tooling).
|
||||
# CURSOR_TOKEN=
|
||||
|
||||
@@ -1850,15 +1815,6 @@ APP_LOG_TO_FILE=true
|
||||
# SKILLS_SANDBOX_NETWORK_ENABLED=0
|
||||
# SKILLS_ALLOWED_SANDBOX_IMAGES=
|
||||
|
||||
# Container runtime used by the skill sandbox. Accepted values:
|
||||
# auto — pick the best installed runtime per host OS (default)
|
||||
# docker — Docker Engine / Docker Desktop
|
||||
# apple — Apple Container (macOS native, micro-VM)
|
||||
# wsl — WSL Container (Windows native via wslc.exe)
|
||||
# orbstack — OrbStack (high-perf Linux VM + docker shim on macOS)
|
||||
# podman — Podman (rootless, daemonless)
|
||||
# SKILLS_SANDBOX_RUNTIME=auto
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 25. TEST & E2E
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1904,7 +1860,7 @@ APP_LOG_TO_FILE=true
|
||||
# OMNIROUTE_TRANSLATION_API_URL=
|
||||
# Bearer token for the translation backend (NEVER commit a real key here).
|
||||
# OMNIROUTE_TRANSLATION_API_KEY=
|
||||
# Model id, e.g. gpt-4o-mini or cx/gpt-5.6-sol.
|
||||
# Model id, e.g. gpt-4o-mini or cx/gpt-5.4-mini.
|
||||
# OMNIROUTE_TRANSLATION_MODEL=gpt-4o-mini
|
||||
# Per-request timeout in milliseconds (default 60000).
|
||||
# OMNIROUTE_TRANSLATION_TIMEOUT_MS=60000
|
||||
@@ -2112,24 +2068,3 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# BIFROST_API_KEY=
|
||||
# BIFROST_STREAMING_ENABLED=true
|
||||
# BIFROST_TIMEOUT_MS=30000
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Account rotation config (operator-managed; consumed by open-sse/services/rotationConfig.ts)
|
||||
# Lets a supervising front-end mirror its rotation rules onto the backend's account-fallback
|
||||
# engine. All optional; defaults preserve the historical behavior.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# OMNIROUTE_ROTATION_ENABLED=true
|
||||
# OMNIROUTE_ROTATION_RATE_LIMIT_RESET_SECONDS=0
|
||||
# OMNIROUTE_ROTATION_DISABLE_TAG_WITHOUT_RESET=true
|
||||
# OMNIROUTE_ROTATE_ON_429=true
|
||||
# OMNIROUTE_ROTATE_429_THRESHOLD=1
|
||||
# OMNIROUTE_ROTATE_429_WINDOW_SECONDS=120
|
||||
# OMNIROUTE_ROTATE_ON_500=true
|
||||
# OMNIROUTE_ROTATE_500_THRESHOLD=1
|
||||
# OMNIROUTE_ROTATE_500_WINDOW_SECONDS=120
|
||||
# OMNIROUTE_ROTATE_ON_502=true
|
||||
# OMNIROUTE_ROTATE_502_THRESHOLD=1
|
||||
# OMNIROUTE_ROTATE_502_WINDOW_SECONDS=120
|
||||
# OMNIROUTE_ROTATE_ON_400=false
|
||||
# OMNIROUTE_ROTATE_400_THRESHOLD=1
|
||||
# OMNIROUTE_ROTATE_400_WINDOW_SECONDS=120
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
# Homologação E2E real — copie para .env.homolog (NUNCA commitar o real)
|
||||
HOMOLOG_BASE_URL=http://192.168.0.15:20128
|
||||
# Senha de management do dashboard da VPS (a mesma do /login)
|
||||
HOMOLOG_ADMIN_PASSWORD=
|
||||
# Deixe vazio: a suíte cria uma API key efêmera via admin e revoga no fim.
|
||||
# Só preencha para depurar uma camada isolada com uma key fixa.
|
||||
HOMOLOG_API_KEY=
|
||||
# Tier crítico (chat real, max_tokens=5). Demais providers: só validação de catálogo.
|
||||
HOMOLOG_CRITICAL_PROVIDERS=openai,anthropic,gemini,codex,grok,glm,deepseek,openrouter
|
||||
9
.github/dependabot.yml
vendored
9
.github/dependabot.yml
vendored
@@ -24,15 +24,6 @@ updates:
|
||||
update-types: ["version-update:semver-major"]
|
||||
- dependency-name: "eslint-config-next"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# typescript majors are peer-blocked by typescript-eslint, which pins a hard
|
||||
# upper bound (8.64.0 → peerDependencies.typescript ">=4.8.4 <6.1.0"). A TS 7
|
||||
# bump therefore violates the peer and takes down the whole toolchain at once —
|
||||
# #7068 grouped it with 6 harmless bumps and turned Build + Lint + Quality Ratchet
|
||||
# + Unit (6/8, 8/8) + Integration (1/2, 2/2) + dast-smoke red in one shot, blocking
|
||||
# the innocuous updates riding along with it. Un-ignore once typescript-eslint
|
||||
# widens the peer, and migrate TS majors intentionally (own PR, own CI run).
|
||||
- dependency-name: "typescript"
|
||||
update-types: ["version-update:semver-major"]
|
||||
# jscpd v5 is a Rust rewrite (native binary, no Node.js programmatic API).
|
||||
# scripts/check/check-duplication.mjs is deliberately pinned to jscpd@4 (it
|
||||
# parses jscpd-report.json against a frozen baseline). A v5 major would break
|
||||
|
||||
271
.github/workflows/ci.yml
vendored
271
.github/workflows/ci.yml
vendored
@@ -33,23 +33,17 @@ jobs:
|
||||
docs: ${{ steps.classify.outputs.docs }}
|
||||
i18n: ${{ steps.classify.outputs.i18n }}
|
||||
workflow: ${{ steps.classify.outputs.workflow }}
|
||||
testsOnly: ${{ steps.classify.outputs.testsOnly }}
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- id: classify
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
# Single source of truth: scripts/quality/classify-pr-changes.mjs
|
||||
# (unit-tested). Push/dispatch always enable every lane.
|
||||
if [ "$EVENT_NAME" != "pull_request" ]; then
|
||||
{
|
||||
echo "code=true"
|
||||
@@ -60,18 +54,51 @@ jobs:
|
||||
exit 0
|
||||
fi
|
||||
|
||||
code=false
|
||||
docs=false
|
||||
i18n=false
|
||||
workflow=false
|
||||
|
||||
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
|
||||
|
||||
while IFS= read -r file; do
|
||||
case "$file" in
|
||||
.github/workflows/*|.zizmor.yml)
|
||||
workflow=true
|
||||
code=true
|
||||
;;
|
||||
docs/*|*.md)
|
||||
docs=true
|
||||
;;
|
||||
src/i18n/*|src/i18n/messages/*|scripts/i18n/*|config/i18n.json)
|
||||
i18n=true
|
||||
code=true
|
||||
;;
|
||||
src/*|open-sse/*|bin/*|electron/*|tests/*|scripts/*|package.json|package-lock.json|tsconfig*.json|next.config.*|vitest*.config.*|playwright.config.*)
|
||||
code=true
|
||||
;;
|
||||
db/*|config/*)
|
||||
code=true
|
||||
;;
|
||||
*)
|
||||
code=true
|
||||
;;
|
||||
esac
|
||||
done < changed-files.txt
|
||||
|
||||
{
|
||||
echo "code=$code"
|
||||
echo "docs=$docs"
|
||||
echo "i18n=$i18n"
|
||||
echo "workflow=$workflow"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Path filter: pure docs / pure message-catalog PRs skip the code lint bag + typecheck.
|
||||
# Existence reason of this job is code regression; docs/i18n have dedicated jobs.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
env:
|
||||
# tsx gates below (known-symbols, route-guard-membership) import modules that
|
||||
# open SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
||||
@@ -89,27 +116,7 @@ jobs:
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: npm run audit:deps
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-
|
||||
# Single ESLint inventory (JSON) — quality-gate reuses the artifact instead of
|
||||
# a second cold full-tree pass for eslintWarnings ratchet counts.
|
||||
- name: ESLint (JSON report)
|
||||
run: npm run lint:json
|
||||
- name: Upload ESLint results
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: eslint-results
|
||||
path: .artifacts/eslint-results.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 7
|
||||
- run: npm run lint
|
||||
- run: npm run check:cycles
|
||||
- run: npm run check:route-validation:t06
|
||||
- run: npm run check:any-budget:t11
|
||||
@@ -125,33 +132,21 @@ jobs:
|
||||
- run: npm run check:route-guard-membership
|
||||
- run: npm run check:test-discovery
|
||||
- run: npm run check:tracked-artifacts
|
||||
# WS1.7 (v3.8.49 plan): Dockerfile lint (hadolint, pinned by digest).
|
||||
# failure-threshold=error keeps the 5 pre-existing warnings (DL3008/DL3003/
|
||||
# DL3016 version pinning / WORKDIR) visible without blocking; any ERROR fails.
|
||||
- name: hadolint (Dockerfile)
|
||||
run: docker run --rm -i hadolint/hadolint@sha256:27086352fd5e1907ea2b934eb1023f217c5ae087992eb59fde121dce9c9ff21e hadolint --failure-threshold error - < Dockerfile
|
||||
- run: npm run check:lockfile
|
||||
- run: npm run check:licenses
|
||||
# check:docs-sync is run by the docs-sync-strict job (via check:docs-all) and the
|
||||
# husky pre-commit hook; the standalone copy here was redundant (ROI dedup).
|
||||
- run: npm run typecheck:core
|
||||
# #7033: typecheck:core's curated file allowlist does not cover
|
||||
# src/app/(dashboard) TSX (and next.config.mjs sets ignoreBuildErrors:
|
||||
# true, so `next build` never type-checks it either) — orphaned
|
||||
# identifiers there (see #6625/#6909) were invisible to CI. This gate
|
||||
# runs tsc scoped to the dashboard tree against a frozen baseline of
|
||||
# pre-existing errors; only NEW errors fail it.
|
||||
- run: npm run check:dashboard-typecheck
|
||||
# typecheck:noimplicit:core dropped from this job (2026-07 optimize):
|
||||
# it was advisory (continue-on-error) and largely subsumed by the blocking
|
||||
# check:type-coverage ratchet in quality-gate. Local: npm run typecheck:noimplicit:core.
|
||||
# typecheck:noimplicit:core is a forward-looking gate (noImplicitAny).
|
||||
# Run informationally for now — many pre-existing call sites still need
|
||||
# explicit annotations; track in a dedicated follow-up.
|
||||
- run: npm run typecheck:noimplicit:core
|
||||
continue-on-error: true
|
||||
|
||||
quality-gate:
|
||||
name: Quality Ratchet
|
||||
runs-on: ubuntu-latest
|
||||
# needs lint so eslint-results artifact is available (same inventory as the
|
||||
# blocking lint step). Allow lint failure so other ratchets still run.
|
||||
needs: [changes, test-coverage, lint]
|
||||
needs: test-coverage
|
||||
# Run even when test-coverage was SKIPPED/FAILED (e.g. a single flaky Coverage
|
||||
# Shard breaks the shard→coverage→ratchet chain). The DETERMINISTIC ratchets
|
||||
# (eslint / complexity / cognitive-complexity / duplication / codeql) do NOT need
|
||||
@@ -160,8 +155,7 @@ jobs:
|
||||
# release PR #4854, where the drift cascade only surfaced post-merge in #5029).
|
||||
# The coverage.* metrics degrade gracefully: the download is continue-on-error and
|
||||
# the ratchet runs with --allow-missing, so absent coverage is skipped, not failed.
|
||||
# Path filter: code-only — pure docs/i18n PRs have nothing for these ratchets to guard.
|
||||
if: ${{ !cancelled() && !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true' && (needs.lint.result == 'success' || needs.lint.result == 'failure'))) }}
|
||||
if: ${{ !cancelled() && (github.event_name != 'pull_request' || github.event.pull_request.draft == false) }}
|
||||
# security-events: read lets the CodeQL ratchet read open code-scanning alerts
|
||||
# via `gh api .../code-scanning/alerts`. contents: read keeps checkout working.
|
||||
permissions:
|
||||
@@ -176,15 +170,6 @@ jobs:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-
|
||||
# Coverage mergeada (coverage-summary.json) p/ o ratchet de cobertura.
|
||||
# continue-on-error: o artifact pode não existir se a job test-coverage foi
|
||||
# SKIPPED (shard flaky). Nesse caso collect-metrics pula coverage.* (ausente sem
|
||||
@@ -195,13 +180,6 @@ jobs:
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
# Prefer lint job's ESLint JSON (one inventory, two consumers).
|
||||
- name: Download ESLint results
|
||||
continue-on-error: true
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: eslint-results
|
||||
path: .artifacts/
|
||||
- run: npm run quality:collect
|
||||
# Catraca: falha se qualquer métrica regredir vs quality-baseline.json (commitado).
|
||||
# Hoje: contagem de warnings do ESLint. Fase 4 estende com cobertura (lida do
|
||||
@@ -223,13 +201,15 @@ jobs:
|
||||
# para não pesar no caminho crítico do lint.
|
||||
- name: Duplication ratchet
|
||||
run: npm run check:duplication
|
||||
# Complexity + cognitive: one ESLint walk, two independent baselines (by ruleId).
|
||||
- name: Complexity + cognitive ratchets
|
||||
run: npm run check:complexity-ratchets
|
||||
# Fase 7 INT: dead-code, type-coverage promovidos de advisory (quality-extended)
|
||||
# para BLOQUEANTES aqui. cognitive-complexity is folded into the step above.
|
||||
- name: Complexity ratchet
|
||||
run: npm run check:complexity
|
||||
# Fase 7 INT: dead-code, cognitive-complexity, type-coverage promovidos de
|
||||
# advisory (quality-extended) para BLOQUEANTES aqui. Os 3 leem seus baseline
|
||||
# de quality-baseline.json e saem 1 em regressão.
|
||||
- name: Dead-code ratchet (knip)
|
||||
run: npm run check:dead-code
|
||||
- name: Cognitive complexity ratchet (sonarjs)
|
||||
run: npm run check:cognitive-complexity
|
||||
- name: Type coverage ratchet
|
||||
run: npm run check:type-coverage
|
||||
- name: Compression budget ratchet (F2.4 / N4)
|
||||
@@ -267,11 +247,9 @@ jobs:
|
||||
quality-extended:
|
||||
name: Quality Gates (Extended)
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Path filter: code-only (scanners/ratchets target production surface).
|
||||
if: ${{ !contains(github.event.pull_request.labels.*.name, 'hotfix') && (github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.code == 'true')) }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
steps:
|
||||
# fetch-depth: 0 — the OpenAPI breaking-change gate (oasdiff) reads the base
|
||||
# spec via `git show <base_ref>:docs/openapi.yaml`; a shallow clone
|
||||
@@ -379,11 +357,9 @@ jobs:
|
||||
docs-sync-strict:
|
||||
name: Docs Sync (Strict)
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Run when docs OR code change: API/route code can break doc/OpenAPI contract gates.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -403,20 +379,19 @@ jobs:
|
||||
run: npm run check:openapi-coverage
|
||||
- name: OpenAPI security-tier consistency (advisory)
|
||||
run: npm run check:openapi-security-tiers
|
||||
# One FS inventory of src/app/api for both anti-hallucination directions.
|
||||
- name: API docs refs (openapi + prose → routes)
|
||||
run: npm run check:api-docs-refs
|
||||
- name: OpenAPI spec paths resolve to real routes (anti-hallucination)
|
||||
run: npm run check:openapi-routes
|
||||
- name: Doc /api refs resolve to real routes (anti-hallucination)
|
||||
run: npm run check:docs-symbols
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
|
||||
docs-lint:
|
||||
name: Docs Lint (prose — advisory)
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Prose/markdown only — skip when the PR has no doc surface.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.docs == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
# Advisory (warning-first): prose/markdown style must not block merges while the
|
||||
# existing doc corpus is brought up to style. Promote to blocking once it converges.
|
||||
continue-on-error: true
|
||||
@@ -443,11 +418,9 @@ jobs:
|
||||
i18n-ui-coverage:
|
||||
name: i18n UI Coverage
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# UI keys move with dashboard code OR message catalogs.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && (needs.changes.outputs.i18n == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
@@ -467,11 +440,9 @@ jobs:
|
||||
i18n:
|
||||
name: i18n Validation (all languages)
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
# P3 (plano mestre): a release-PR viva fica DRAFT o ciclo inteiro — jobs pesados pulam
|
||||
# drafts (ciclo v3.8.44: 123 runs pesados re-disparados por merges na release, 88 cancelados).
|
||||
# Message-catalog / i18n-tooling only — pure code without i18n surface skips this lane.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false && needs.changes.outputs.i18n == 'true') }}
|
||||
if: ${{ github.event_name != 'pull_request' || github.event.pull_request.draft == false }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -519,7 +490,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- name: Fetch base branch
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}"
|
||||
run: git fetch --no-tags origin "${GITHUB_BASE_REF}" --depth=1
|
||||
- name: Validate source changes include tests
|
||||
run: node scripts/check/check-pr-test-policy.mjs --summary-file .artifacts/pr-test-policy.md
|
||||
# Anti test-masking: flag net assert removal / new assert.ok(true) in changed tests.
|
||||
@@ -622,26 +593,12 @@ jobs:
|
||||
- name: Assert dist/server.js exists
|
||||
run: test -f dist/server.js || (echo "dist/server.js missing — build:cli did not assemble correctly" && exit 1)
|
||||
- run: npm run check:pack-artifact
|
||||
# WS1.2 (#7065 class): pack the real tarball, install it into a clean prefix and
|
||||
# BOOT it to a healthy /api/monitoring/health — the gate that structure checks
|
||||
# cannot provide (3 releases shipped boot-crashing tarballs with green lists).
|
||||
- name: Boot-smoke the packed tarball
|
||||
run: npm run check:pack-boot
|
||||
|
||||
electron-package-smoke:
|
||||
name: Electron Package Smoke (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
name: Electron Package Smoke
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 25
|
||||
needs: build
|
||||
# WS1.5 (v3.8.49 plan): the Electron rebuild/spawn path previously executed for
|
||||
# the FIRST time on the release tag — the v3.8.48 Windows bug (npx.cmd spawned
|
||||
# without shell, CVE-2024-27980 behavior change) could only surface at release.
|
||||
# windows-latest runs prepare:bundle (the ABI rebuild + spawn plan) per release
|
||||
# PR; ubuntu keeps the full pack + headless smoke.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, windows-latest]
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
CSC_IDENTITY_AUTO_DISCOVERY: "false"
|
||||
@@ -667,21 +624,9 @@ jobs:
|
||||
working-directory: electron
|
||||
run: npm install --no-audit --no-fund
|
||||
- name: Pack Electron app
|
||||
if: runner.os == 'Linux'
|
||||
working-directory: electron
|
||||
run: npm run pack
|
||||
# ADVISORY while the new Windows leg matures (repo convention, dast-smoke
|
||||
# precedent): its first-ever real run (2026-07-15, run 29457533565) died in
|
||||
# 0.7s with the error swallowed by pwsh — bash shell captures stderr and
|
||||
# continue-on-error keeps the heavy gate green while we harden it (#7336).
|
||||
- name: Prepare Electron standalone (Windows ABI rebuild + spawn path)
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: electron
|
||||
continue-on-error: true
|
||||
shell: bash
|
||||
run: npm run prepare:bundle 2>&1
|
||||
- name: Smoke packaged Electron app
|
||||
if: runner.os == 'Linux'
|
||||
env:
|
||||
ELECTRON_SMOKE_TIMEOUT_MS: 60000
|
||||
run: xvfb-run -a npm run electron:smoke:packaged
|
||||
@@ -764,23 +709,12 @@ jobs:
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
# The second test runner (CLAUDE.md: "Both test runners must pass") — was never
|
||||
# wired into CI until the 2026-06-09 quality audit (Fase 6A.2).
|
||||
# WS5.2/5.3 (v3.8.49 plan): JUnit output feeds Trunk Flaky Tests (advisory upload
|
||||
# below). node:test stays OUT of the first wave (fd1-sensitive reporter stream).
|
||||
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-mcp.xml
|
||||
# vitest:ui went back to 870/870 green in the v3.8.49 quality plan (WS6.1,
|
||||
# PR #7127 — 69 fails triaged: matchMedia polyfill, node:test→vitest migration,
|
||||
# CompareTab D22 cap). Promoted to BLOCKING per the plan's post-merge step.
|
||||
- run: npm run test:vitest:ui -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-ui.xml
|
||||
# Trunk Flaky Tests upload — advisory (never blocks), own-origin only (fork PRs
|
||||
# have no TRUNK_TOKEN). Pinned by SHA (tag v2.1.2).
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
- run: npm run test:vitest
|
||||
# vitest:ui is RED today (14 fails — UI component drift accumulated while the
|
||||
# suite never ran in CI). Informational until the Fase 6A triage (2026-06-16+)
|
||||
# fixes the components/tests; then drop continue-on-error to make it blocking.
|
||||
- run: npm run test:vitest:ui
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: trunk-junit/**/*.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
|
||||
# Node 24/26 compatibility matrices moved to .github/workflows/nightly-compat.yml
|
||||
# (plano mestre testes+CI, Eixo D2 — they cost ~28% of every heavy run to catch a
|
||||
@@ -789,12 +723,9 @@ jobs:
|
||||
test-coverage:
|
||||
name: Coverage
|
||||
runs-on: ubuntu-latest
|
||||
# 10min was sized before #7114 added the lcov reporter (Codecov/Sonar need it);
|
||||
# merging 8 shard JSONs + text+json+lcov now takes ~10-12min — three consecutive
|
||||
# release-tip runs died at exactly 10m as job-timeout "cancelled" (2026-07-15/16).
|
||||
timeout-minutes: 20
|
||||
timeout-minutes: 10
|
||||
needs: test-unit
|
||||
if: ${{ !cancelled() && needs.test-unit.result == 'success' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
||||
if: ${{ !cancelled() && needs.test-unit.result == 'success' }}
|
||||
env:
|
||||
JWT_SECRET: ci-test-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-test-api-key-secret-long
|
||||
@@ -842,7 +773,6 @@ jobs:
|
||||
--merge-async \
|
||||
--reporter=text-summary \
|
||||
--reporter=json-summary \
|
||||
--reporter=lcov \
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
--check-coverage \
|
||||
@@ -864,18 +794,6 @@ jobs:
|
||||
> coverage/coverage-report.md
|
||||
fi
|
||||
cat coverage/coverage-report.md >> "$GITHUB_STEP_SUMMARY"
|
||||
# WS5.6 (D7, v3.8.49 plan): patch coverage on the PR diff via Codecov —
|
||||
# informational during calibration (codecov.yml sets informational: true);
|
||||
# promote to blocking only after ~2 weeks without false blocks. The lcov
|
||||
# reporter above also fixes coverage/lcov.info being silently absent
|
||||
# (if-no-files-found: warn) — Sonar consumes the same file.
|
||||
- name: Upload coverage to Codecov (informational)
|
||||
if: always()
|
||||
uses: codecov/codecov-action@04b047e8bb82a0c002c8312c1c880fbc6a999d45 # v5
|
||||
with:
|
||||
files: coverage/lcov.info
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: false
|
||||
- name: Upload coverage artifacts
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -900,14 +818,10 @@ jobs:
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
# The upload strips the common `coverage/` prefix, so the artifact root holds
|
||||
# lcov.info directly — download into coverage/ so it lands at coverage/lcov.info,
|
||||
# where sonar.javascript.lcov.reportPaths expects it (path: . left the Sonar
|
||||
# new-code coverage at 0% every scan).
|
||||
- uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage/
|
||||
path: .
|
||||
- name: Explain SonarQube skip
|
||||
if: ${{ github.event_name != 'pull_request' || env.SONAR_TOKEN == '' || env.SONAR_HOST_URL == '' }}
|
||||
run: |
|
||||
@@ -1026,12 +940,7 @@ jobs:
|
||||
# ~33%. Playwright browser is cached across runs (~1.5min saved per shard).
|
||||
# Heavy shard target: ≤20min (was ~40min). Timeout 45min to cover slow runners.
|
||||
timeout-minutes: 45
|
||||
needs: [build, changes]
|
||||
# WS3.1 hotfix fast-lane: the 9-shard E2E matrix is the CI critical path (~25min).
|
||||
# It skips for (a) PRs labeled `hotfix` (entry policy in docs/ops/RELEASE_CHECKLIST.md:
|
||||
# production-broken only, full-suite evidence from the previous green run linked in the
|
||||
# PR) and (b) tests-only diffs outside tests/e2e/ (cannot change the served app).
|
||||
if: ${{ needs.changes.outputs.testsOnly != 'true' && !contains(github.event.pull_request.labels.*.name, 'hotfix') }}
|
||||
needs: build
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -1052,7 +961,7 @@ jobs:
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- name: Cache Playwright browsers
|
||||
uses: actions/cache@v6
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: ~/.cache/ms-playwright
|
||||
key: playwright-chromium-${{ runner.os }}-${{ hashFiles('package-lock.json') }}
|
||||
@@ -1066,33 +975,7 @@ jobs:
|
||||
- name: Extract Next.js build artifact
|
||||
run: |
|
||||
tar -xzf /tmp/e2e-build.tar.gz
|
||||
# WS4.1: duration-balanced shards (LPT over config/quality/e2e-timings.json).
|
||||
# Measured skew of plain --shard was 14× (24m47s vs 1m47s) — E2E was the CI
|
||||
# critical path. The balancer self-verifies completeness and exits non-zero on
|
||||
# any inconsistency, falling back to plain --shard (never fewer specs).
|
||||
- name: Run E2E tests (duration-balanced shard)
|
||||
env:
|
||||
SHARD: ${{ matrix.shard }}
|
||||
PLAYWRIGHT_JUNIT_OUTPUT_NAME: junit-e2e-results.xml
|
||||
run: |
|
||||
if FILES=$(node scripts/quality/balance-e2e-shards.mjs "$SHARD" 9); then
|
||||
if [ -z "$FILES" ]; then echo "[e2e-balance] shard $SHARD has no files"; exit 0; fi
|
||||
echo "[e2e-balance] shard $SHARD runs:"; echo "$FILES"
|
||||
# shellcheck disable=SC2086 — FILES is our own newline-separated path list
|
||||
npx playwright test $(echo "$FILES" | tr '\n' ' ') --reporter=line,junit
|
||||
else
|
||||
echo "[e2e-balance] balancer unavailable — plain --shard fallback"
|
||||
npx playwright test tests/e2e/*.spec.ts --shard="$SHARD"/9 --reporter=line,junit
|
||||
fi
|
||||
# WS5.2/5.3: Trunk Flaky Tests upload — advisory, own-origin only, SHA-pinned.
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: junit-e2e-results.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
- run: npx playwright test tests/e2e/*.spec.ts --shard=${{ matrix.shard }}/9
|
||||
|
||||
test-integration:
|
||||
name: Integration Tests (${{ matrix.shard }}/2)
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
- uses: github/codeql-action/init@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
queries: security-extended
|
||||
- uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
|
||||
- uses: github/codeql-action/analyze@54f647b7e1bb85c95cddabcd46b0c578ec92bc1a # v4.36.3
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
5
.github/workflows/dast-smoke.yml
vendored
5
.github/workflows/dast-smoke.yml
vendored
@@ -10,10 +10,7 @@ jobs:
|
||||
# ADVISORY while this new gate matures (repo convention: advisory -> blocking).
|
||||
# Flip to blocking (remove continue-on-error) once it's proven stable across a few PRs.
|
||||
continue-on-error: true
|
||||
# Build CLI bundle alone varies 6-11min on GitHub-hosted runners (3 consecutive
|
||||
# timeouts observed on 2026-07-14 with the old 12min cap killing schemathesis
|
||||
# mid-run) — 25min leaves real headroom for the actual DAST steps.
|
||||
timeout-minutes: 25
|
||||
timeout-minutes: 12
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-api-key-secret-with-sufficient-length-aaaa
|
||||
|
||||
7
.github/workflows/electron-release.yml
vendored
7
.github/workflows/electron-release.yml
vendored
@@ -201,12 +201,6 @@ jobs:
|
||||
[ -f "$file" ] && cp "$file" "../../release-assets/OmniRoute.exe" && break
|
||||
done
|
||||
fi
|
||||
# electron-updater manifests (latest.yml / latest-mac.yml / latest-linux.yml)
|
||||
# must be published alongside the installers, or autoUpdater fails with
|
||||
# "Cannot find latest.yml in the latest release artifacts" (#6766).
|
||||
for file in latest*.yml; do
|
||||
[ -f "$file" ] && cp "$file" ../../release-assets/
|
||||
done
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v7
|
||||
@@ -269,7 +263,6 @@ jobs:
|
||||
release-assets/*.AppImage
|
||||
release-assets/*.deb
|
||||
release-assets/*.blockmap
|
||||
release-assets/*.yml
|
||||
release-assets/*.source.tar.gz
|
||||
release-assets/*.source.zip
|
||||
env:
|
||||
|
||||
175
.github/workflows/nightly-release-green.yml
vendored
175
.github/workflows/nightly-release-green.yml
vendored
@@ -1,18 +1,11 @@
|
||||
name: Release-Green (continuous)
|
||||
name: Nightly Release-Green
|
||||
|
||||
# Solution D — continuous, NON-BLOCKING drift signal for the active release branch.
|
||||
#
|
||||
# WHY: the full gate (ci.yml) only runs on the release PR (PR → main), so reds
|
||||
# accrue silently on release/** and explode — in layers — at release time. This
|
||||
# workflow reproduces the release-equivalent validation on the release branch and,
|
||||
# when there are HARD failures, opens/updates a single tracking issue.
|
||||
#
|
||||
# WS5.1 (v3.8.49 quality plan) — two modes:
|
||||
# push to release/v* (code paths) → --quick (fast HARD gates, ~5-8min). Catches the
|
||||
# captain's direct pushes (sync-back — the one ungated write path) AND the merged
|
||||
# COMBINATION right after every PR merge, attributing the offending push range in
|
||||
# the issue. Base-red MTTD drops from ≤24h to ≤~15min after the offending push.
|
||||
# schedule (3×/day) → full --with-build --full-ci (the deep sweep incl. build+suites).
|
||||
# nightly reproduces the release-equivalent validation on the active release branch
|
||||
# HEAD and, when there are HARD failures, opens/updates a single tracking issue.
|
||||
#
|
||||
# It is NOT a required status check and never touches a contributor PR — it only
|
||||
# reports. Ratchet drift (eslint warnings / cognitive-complexity / file-size) is
|
||||
@@ -21,23 +14,8 @@ name: Release-Green (continuous)
|
||||
# package-artifact) flip the issue open.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/v*", "main"]
|
||||
paths:
|
||||
- "src/**"
|
||||
- "open-sse/**"
|
||||
- "bin/**"
|
||||
- "electron/**"
|
||||
- "scripts/**"
|
||||
- "tests/**"
|
||||
- "config/**"
|
||||
- "package.json"
|
||||
- "package-lock.json"
|
||||
- "tsconfig*.json"
|
||||
schedule:
|
||||
- cron: "23 5 * * *" # full sweep — off-peak, distinct from other nightlies
|
||||
- cron: "23 12 * * *" # full sweep — midday (WS5.1: 3×/day instead of 1×)
|
||||
- cron: "23 18 * * *" # full sweep — evening
|
||||
- cron: "23 5 * * *" # 05:23 UTC daily — off-peak, distinct from other nightlies
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
@@ -50,9 +28,7 @@ permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
# push storms during merge campaigns collapse to the newest commit per branch;
|
||||
# scheduled full sweeps keep their own single lane.
|
||||
group: release-green-${{ github.event_name }}-${{ github.ref }}
|
||||
group: nightly-release-green
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
@@ -61,9 +37,6 @@ env:
|
||||
jobs:
|
||||
release-green:
|
||||
name: Validate active release branch
|
||||
# On a push, only run for release/* pushes — a push to main is handled by the
|
||||
# main-green job below. Schedule/dispatch always run (they validate the highest release).
|
||||
if: ${{ github.event_name != 'push' || startsWith(github.ref_name, 'release/') }}
|
||||
# Dynamic runner: with USE_VPS_RUNNER=true (release window / on-demand pre-flight)
|
||||
# this runs on the dedicated VPS runner — clean env (no operator OMNIROUTE_API_KEY,
|
||||
# no local noauth CLIs => zero machine-specific false positives) and no contention.
|
||||
@@ -83,15 +56,10 @@ jobs:
|
||||
id: branch
|
||||
env:
|
||||
INPUT_BRANCH: ${{ github.event.inputs.branch }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
PUSHED_REF: ${{ github.ref_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${INPUT_BRANCH:-}" ]; then
|
||||
TARGET="$INPUT_BRANCH"
|
||||
elif [ "$EVENT_NAME" = "push" ]; then
|
||||
# validate exactly what was pushed, not the highest branch
|
||||
TARGET="$PUSHED_REF"
|
||||
else
|
||||
# highest release/vX.Y.Z by semver among remote branches
|
||||
TARGET=$(git for-each-ref --format='%(refname:short)' 'refs/remotes/origin/release/v*' \
|
||||
@@ -125,26 +93,11 @@ jobs:
|
||||
|
||||
- name: Release-green validation (full)
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set +e
|
||||
# --hermetic: scrub live-test trigger vars (self-hosted runner may carry
|
||||
# operator env; hosted ignores the unknown flag before #6300 lands).
|
||||
# push → --quick: fast HARD gates only (~5-8min), per-merge signal.
|
||||
# schedule/dispatch → --with-build --full-ci: ALSO run every static gate from
|
||||
# ci.yml's gate jobs (lint, quality-gate, quality-extended, docs-sync-strict,
|
||||
# pr-test-policy) + build + full suites. PRs into release/** only get the
|
||||
# fast-gates, so these accrue silently and explode in layers on the release PR
|
||||
# (v3.8.46: 11 static base-reds leaked).
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
MODE="--quick"
|
||||
else
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[release-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
node scripts/quality/validate-release-green.mjs --json --with-build --hermetic \
|
||||
1> release-green.json 2> release-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
echo "------- report -------"
|
||||
@@ -156,28 +109,15 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
TARGET: ${{ steps.branch.outputs.target }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BEFORE_SHA: ${{ github.event.before }}
|
||||
AFTER_SHA: ${{ github.event.after }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="🔴 Release branch not green: ${TARGET}"
|
||||
{
|
||||
echo "The **release-green** validation found HARD failures on \`${TARGET}\`."
|
||||
echo "The nightly **release-green** validation found HARD failures on \`${TARGET}\`."
|
||||
echo "These are real defects that would block the release PR — fix them in the"
|
||||
echo "originating PR branch (via co-authorship), not by demanding it from contributors."
|
||||
echo ""
|
||||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||||
# WS5.1 attribution: on push events the offending change IS this push's range
|
||||
# (one merge per push in the normal queue), so name it — no bisect needed.
|
||||
if [ "$EVENT_NAME" = "push" ] && [ -n "${BEFORE_SHA:-}" ] && \
|
||||
git cat-file -e "$BEFORE_SHA" 2>/dev/null; then
|
||||
echo ""
|
||||
echo "**Offending push range** (\`${BEFORE_SHA:0:9}..${AFTER_SHA:0:9}\`):"
|
||||
echo '```'
|
||||
git log --no-decorate --oneline "${BEFORE_SHA}..${AFTER_SHA}" | head -20
|
||||
echo '```'
|
||||
fi
|
||||
echo "**Run:** ${RUN_URL}"
|
||||
echo ""
|
||||
echo '```'
|
||||
sed -n '/──────── verdict ────────/,$p' release-green.log || tail -40 release-green.log
|
||||
@@ -197,107 +137,10 @@ jobs:
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-green-report
|
||||
path: |
|
||||
release-green.json
|
||||
release-green.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
# Companion arm for `main`. Under the parallel-cycle model, main only receives merged
|
||||
# work at the release squash — so a gate/infra fix that lands only on release leaves
|
||||
# main red the whole cycle, and repo-wide gates (CodeQL alert count, ratchet baselines)
|
||||
# turn EVERY PR into main red on a check unrelated to its diff. This detects that and
|
||||
# opens a "🔴 main not green" tracking issue. The PREVENTION is the companion-PR reflex
|
||||
# (Hard Rule #21 area / _shared/merge-gates.md §8); this is the automated backstop.
|
||||
main-green:
|
||||
name: Validate main branch
|
||||
# On a push, only run for a push to main — a push to release/* is handled by
|
||||
# release-green above. Schedule/dispatch always run (they also sweep main).
|
||||
if: ${{ github.event_name != 'push' || github.ref_name == 'main' }}
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && fromJSON('["self-hosted","omni-release"]')) || 'ubuntu-latest' }}
|
||||
env:
|
||||
JWT_SECRET: ci-nightly-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-nightly-api-key-secret-long
|
||||
DISABLE_SQLITE_AUTO_BACKUP: "true"
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: main # literal — no injection surface; scheduled runs default to the repo default branch (a release/v*), so pin main explicitly
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
|
||||
- name: Main-green validation
|
||||
id: validate
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set +e
|
||||
# push (a merge into main) → --quick fast HARD gates; schedule/dispatch → full sweep.
|
||||
if [ "$EVENT_NAME" = "push" ]; then
|
||||
MODE="--quick"
|
||||
else
|
||||
MODE="--with-build --full-ci"
|
||||
fi
|
||||
echo "[main-green] mode: $MODE (event: $EVENT_NAME)"
|
||||
# shellcheck disable=SC2086 — MODE is an intentional flag list
|
||||
node scripts/quality/validate-release-green.mjs --json --hermetic $MODE \
|
||||
1> main-green.json 2> main-green.log
|
||||
echo "exit=$?" >> "$GITHUB_OUTPUT"
|
||||
echo "------- report -------"
|
||||
cat main-green.log
|
||||
|
||||
- name: Open / update tracking issue on HARD failure
|
||||
if: steps.validate.outputs.exit != '0'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TITLE="🔴 main branch not green"
|
||||
{
|
||||
echo "The **main-green** validation found HARD failures on \`main\`."
|
||||
echo ""
|
||||
echo "Because \`main\` only receives merged work at the release squash, a gate/infra"
|
||||
echo "fix that landed only on the release branch leaves \`main\` broken for the whole"
|
||||
echo "cycle — and repo-wide gates (CodeQL alert count, ratchet baselines) then turn"
|
||||
echo "**every open PR into main** red on a check unrelated to its diff. The fix is a"
|
||||
echo "companion PR \`--base main\` carrying the release-side fix (see"
|
||||
echo "\`_shared/merge-gates.md\` §8), NOT chasing each contributor PR."
|
||||
echo ""
|
||||
echo "**Run:** ${RUN_URL} (mode: ${EVENT_NAME})"
|
||||
echo ""
|
||||
echo '```'
|
||||
sed -n '/──────── verdict ────────/,$p' main-green.log || tail -40 main-green.log
|
||||
echo '```'
|
||||
echo ""
|
||||
echo "_Ratchet drift (eslint warnings / cognitive-complexity / file-size) is expected mid-cycle and did NOT, on its own, open this issue._"
|
||||
} > issue-body.md
|
||||
|
||||
EXISTING=$(gh issue list --repo "$GITHUB_REPOSITORY" --state open \
|
||||
--search "in:title $TITLE" --json number --jq '.[0].number' 2>/dev/null || echo "")
|
||||
if [ -n "$EXISTING" ]; then
|
||||
gh issue comment "$EXISTING" --repo "$GITHUB_REPOSITORY" --body-file issue-body.md
|
||||
echo "Updated existing issue #$EXISTING"
|
||||
else
|
||||
gh issue create --repo "$GITHUB_REPOSITORY" --title "$TITLE" --body-file issue-body.md
|
||||
fi
|
||||
|
||||
- name: Upload report artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: main-green-report
|
||||
path: |
|
||||
main-green.json
|
||||
main-green.log
|
||||
if-no-files-found: ignore
|
||||
|
||||
64
.github/workflows/npm-publish.yml
vendored
64
.github/workflows/npm-publish.yml
vendored
@@ -22,14 +22,6 @@ on:
|
||||
- latest
|
||||
- next
|
||||
- historic
|
||||
publish_mode:
|
||||
description: "staged = npm stage publish (owner approves with 2FA after the staged boot-verify); direct = legacy immediate publish (emergency fallback only)"
|
||||
required: false
|
||||
default: "staged"
|
||||
type: choice
|
||||
options:
|
||||
- staged
|
||||
- direct
|
||||
workflow_call:
|
||||
inputs:
|
||||
version:
|
||||
@@ -174,34 +166,8 @@ jobs:
|
||||
TAG: ${{ github.ref_name }}
|
||||
run: gh release upload "$TAG" sbom-npm.cdx.json --clobber
|
||||
|
||||
# WS1.2/WS1.3 (#7065 class): the artifact that is about to be published must
|
||||
# BOOT. build:cli already assembled dist/ above; this packs+installs+boots the
|
||||
# real tarball and fails the publish before anything reaches the registry.
|
||||
- name: Boot-smoke the tarball before ANY publish
|
||||
- name: Publish to npm
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
run: npm run check:pack-boot
|
||||
|
||||
# WS1.3 (D2, v3.8.49 plan): STAGED publishing by default — `npm stage publish`
|
||||
# parks the exact bytes on the registry WITHOUT making them installable; the
|
||||
# owner then verifies and approves with 2FA (`npm stage approve`), moving the
|
||||
# human gate to AFTER the proof instead of before it. Requires npm >= 11.15
|
||||
# (staged publishing GA 2026-05-22). publish_mode=direct is the emergency
|
||||
# fallback (legacy immediate publish) via workflow_dispatch.
|
||||
- name: Ensure npm supports staged publishing
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
run: |
|
||||
set -euo pipefail
|
||||
CUR=$(npm --version)
|
||||
if ! node -e "const [a,b]='$(npm --version)'.split('.').map(Number); process.exit(a>11||(a===11&&b>=15)?0:1)"; then
|
||||
# Pinned exact version (supply-chain: never float @latest in the publish
|
||||
# job); bump deliberately when a newer npm is required.
|
||||
echo "npm $CUR < 11.15 — installing pinned npm 11.15.0 for staged publishing"
|
||||
npm install -g --ignore-scripts npm@11.15.0
|
||||
fi
|
||||
npm --version
|
||||
|
||||
- name: Publish to npm (staged — owner approves with 2FA)
|
||||
if: steps.resolve.outputs.skip != 'true' && (github.event_name != 'workflow_dispatch' || inputs.publish_mode != 'direct')
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
@@ -209,32 +175,10 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Always pass --tag explicitly. Defense in depth: even if VERSION is
|
||||
# accidentally an older release, the historic tag will NOT claim `@latest`.
|
||||
npm stage publish --provenance --access public --tag "$TAG"
|
||||
{
|
||||
echo "## 📦 omniroute@$VERSION STAGED (not yet installable)"
|
||||
echo ""
|
||||
echo "The exact bytes are parked on the registry. To release them:"
|
||||
echo '```'
|
||||
echo "npm stage list omniroute # find the stage id"
|
||||
echo "npm stage approve <id> # owner 2FA — THE publish"
|
||||
echo '```'
|
||||
echo "To verify the staged bytes first: npm stage download <id> → run"
|
||||
echo "scripts/check/check-pack-boot.mjs against them (see RELEASE_CHECKLIST)."
|
||||
echo "To discard: npm stage reject <id>."
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "✅ Staged omniroute@$VERSION (dist-tag=$TAG) — awaiting owner 'npm stage approve'"
|
||||
|
||||
- name: Publish to npm (DIRECT — emergency fallback)
|
||||
if: steps.resolve.outputs.skip != 'true' && github.event_name == 'workflow_dispatch' && inputs.publish_mode == 'direct'
|
||||
env:
|
||||
VERSION: ${{ steps.resolve.outputs.version }}
|
||||
TAG: ${{ steps.resolve.outputs.tag }}
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# accidentally an older release, `npm publish --tag historic` will
|
||||
# NOT promote it to `@latest`.
|
||||
npm publish --provenance --access public --tag "$TAG"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG) [DIRECT mode]"
|
||||
echo "✅ Published omniroute@$VERSION (dist-tag=$TAG)"
|
||||
|
||||
- name: Publish to GitHub Packages
|
||||
if: steps.resolve.outputs.skip != 'true'
|
||||
|
||||
198
.github/workflows/quality.yml
vendored
198
.github/workflows/quality.yml
vendored
@@ -21,73 +21,9 @@ env:
|
||||
CI_NODE_VERSION: "24"
|
||||
|
||||
jobs:
|
||||
# Same classifier as ci.yml (scripts/quality/classify-pr-changes.mjs) so PR→release
|
||||
# path filters share existence reasons: code / docs / i18n / workflow.
|
||||
changes:
|
||||
name: Change Classification
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
code: ${{ steps.classify.outputs.code }}
|
||||
docs: ${{ steps.classify.outputs.docs }}
|
||||
i18n: ${{ steps.classify.outputs.i18n }}
|
||||
workflow: ${{ steps.classify.outputs.workflow }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
- id: classify
|
||||
env:
|
||||
EVENT_NAME: ${{ github.event_name }}
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
if [ "$EVENT_NAME" != "pull_request" ]; then
|
||||
{
|
||||
echo "code=true"
|
||||
echo "docs=true"
|
||||
echo "i18n=true"
|
||||
echo "workflow=true"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
git diff --name-only "$BASE_SHA" "$HEAD_SHA" > changed-files.txt
|
||||
node scripts/quality/classify-pr-changes.mjs changed-files.txt >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Docs/OpenAPI contract gates only — existence reason is doc accuracy + route refs.
|
||||
# Split out of fast-gates so pure-docs PRs skip typecheck/unit while still validating docs.
|
||||
docs-gates:
|
||||
name: Docs Gates (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && (needs.changes.outputs.docs == 'true' || needs.changes.outputs.code == 'true')) }}
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
|
||||
- run: npm run check:api-docs-refs
|
||||
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
|
||||
run: npm run check:docs-all
|
||||
|
||||
fast-gates:
|
||||
name: Fast Quality Gates
|
||||
needs: changes
|
||||
# Code surface only — pure docs/i18n PRs skip this bag (docs-gates covers docs).
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner (same rule as ci.yml): use the self-hosted VPS pool only when the
|
||||
# release captain has USE_VPS_RUNNER=true AND this is not a fork PR (own-origin
|
||||
# branches only — a fork PR must never execute on the LAN runner). Var unset/false
|
||||
# or a fork PR falls back to ubuntu-latest, so this is inert until the flag flips.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
# tsx gates (known-symbols, route-guard-membership) import modules that open
|
||||
# SQLite on load; provide DB env so a fresh CI DB initializes cleanly.
|
||||
env:
|
||||
@@ -104,18 +40,12 @@ jobs:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-
|
||||
- run: npm run check:provider-consistency
|
||||
- run: npm run check:fetch-targets
|
||||
# docs-all / openapi-routes / docs-symbols live in docs-gates (path-filtered).
|
||||
- run: npm run check:openapi-routes
|
||||
- run: npm run check:docs-symbols
|
||||
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
|
||||
run: npm run check:docs-all
|
||||
- run: npm run check:deps
|
||||
- run: npm run check:file-size
|
||||
- run: npm run check:error-helper
|
||||
@@ -138,39 +68,25 @@ jobs:
|
||||
# leaking into the npm tarball (v3.8.36: 6 ops bin/*.sh) per-PR instead of only on
|
||||
# the release PR's heavy Package Artifact job.
|
||||
- run: npm run check:pack-policy
|
||||
# Complexity + cognitive-complexity: ONE ESLint walk (both baselines still
|
||||
# enforced separately by ruleId). Avoids two cold tree walks on fast-path.
|
||||
- run: npm run check:complexity-ratchets
|
||||
# Complexity + cognitive-complexity ratchets on the fast-path (PR→release) so
|
||||
# cycle drift is rebaselined PER-PR instead of cascading onto the release PR's
|
||||
# Quality Ratchet (v3.8.36: +30 complexity / +15 cognitive surfaced only post-merge).
|
||||
- run: npm run check:complexity
|
||||
- run: npm run check:cognitive-complexity
|
||||
- name: Typecheck (core)
|
||||
run: npm run typecheck:core
|
||||
# #7033: dashboard-scoped typecheck gate — src/app/(dashboard) TSX is not
|
||||
# covered by typecheck:core's curated allowlist. See check-dashboard-typecheck.mjs.
|
||||
- name: Typecheck (dashboard)
|
||||
run: npm run check:dashboard-typecheck
|
||||
# WS4.2 (v3.8.49 plan): TypeScript 7 native-compiler SHADOW — advisory only.
|
||||
# TS7 went GA 2026-07-08 with 8-12x type-check speedups; its Compiler API only
|
||||
# arrives in 7.1, so typescript-eslint / type-coverage / Stryker stay on 6.x
|
||||
# (the hybrid is the officially documented pattern). Isolated npx on purpose:
|
||||
# installing an alias package could collide node_modules/.bin/tsc with 6.x.
|
||||
# Promote to the blocking gate after ~1 week of parity with the step above.
|
||||
- name: Typecheck (core) — TS7 native shadow (advisory)
|
||||
continue-on-error: true
|
||||
run: |
|
||||
RC=0
|
||||
START=$(date +%s)
|
||||
npx -y -p typescript@7 tsc --pretty false -p tsconfig.typecheck-core.json || RC=$?
|
||||
echo "[ts7-shadow] exit=$RC elapsed=$(( $(date +%s) - START ))s — the 6.x step above stays authoritative"
|
||||
exit $RC
|
||||
# TIA: build the impact map at runtime (gitignored, ~21MB) and run only the
|
||||
# unit tests impacted by this PR's changed files. On hub/unmapped changes the
|
||||
# selector returns __RUN_ALL__ — full-suite authority is the parallel
|
||||
# `fast-unit` 4-shard job (test:unit:ci:shard; was 2-shard, #6781), NOT an
|
||||
# unsharded re-run here. Stacking unsharded test:unit:ci on top of fast-unit
|
||||
# doubled wall time (~16 min extra on ubuntu-latest) without extra coverage.
|
||||
# unit tests impacted by this PR's changed files. Fail-safe runs the FULL
|
||||
# unit suite on hub/unmapped changes — TIA accelerates, never replaces, the net.
|
||||
#
|
||||
# BLOCKING for the *impacted subset* (flipped 2026-06-17). Fail-safe full
|
||||
# coverage remains required via `Unit Tests fast-path` (fast-unit).
|
||||
- name: Impacted unit tests (TIA subset; blocking)
|
||||
# BLOCKING (flipped 2026-06-17). The pre-existing release unit test-debt that kept
|
||||
# this advisory was cleared: #4030 (16 Zod/registry reds, lossless restore) and
|
||||
# #4063 (the last red — the LiveWS boot test — root-caused as a real event-loop
|
||||
# stall in the WS sidecar, fixed + relocated to the integration suite). A full
|
||||
# ci.yml run on release/v3.8.28 then showed all 8 unit shards green, so PR->release
|
||||
# now blocks on unit-test regressions in the impacted set (typecheck:core already
|
||||
# blocked above). Fail-safe still runs the FULL unit suite on hub/unmapped changes.
|
||||
- name: Impacted unit tests (TIA, fail-safe full; blocking)
|
||||
env:
|
||||
GITHUB_BASE_REF: ${{ github.base_ref }}
|
||||
run: |
|
||||
@@ -184,39 +100,15 @@ jobs:
|
||||
# which must not happen on a blocking gate. DATA_DIR isolation keeps the parallel
|
||||
# run race-free regardless of concurrency.
|
||||
if echo "$SEL" | grep -q "__RUN_ALL__"; then
|
||||
echo "Fail-safe: __RUN_ALL__ — deferring FULL unit suite to fast-unit (4-shard)."
|
||||
echo "Not re-running unsharded test:unit:ci here (duplicate of fast-unit coverage)."
|
||||
exit 0
|
||||
echo "Fail-safe: running FULL unit suite (CI concurrency)"; npm run test:unit:ci; exit $?
|
||||
fi
|
||||
echo "Running impacted tests:"; echo "$SEL"
|
||||
mapfile -t FILES <<< "$SEL"
|
||||
# Loader parity with test:unit:ci:shard (#6787): tests/unit/dashboard/** runs
|
||||
# under `--import tsx` (CJS transform — required for ESM-only deep imports like
|
||||
# @lobehub/icons/es/* reached via lobeProviderIcons.ts); everything else under
|
||||
# `--import tsx/esm`. A single tsx/esm invocation false-reds every dashboard
|
||||
# module-shape test the impact map selects ("Unexpected token 'export'").
|
||||
DASH=(); REST=()
|
||||
for f in "${FILES[@]}"; do
|
||||
case "$f" in
|
||||
tests/unit/dashboard/*) DASH+=("$f") ;;
|
||||
*) REST+=("$f") ;;
|
||||
esac
|
||||
done
|
||||
RC=0
|
||||
if [ ${#REST[@]} -gt 0 ]; then
|
||||
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${REST[@]}" || RC=$?
|
||||
fi
|
||||
if [ ${#DASH[@]} -gt 0 ]; then
|
||||
node --import tsx --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${DASH[@]}" || RC=$?
|
||||
fi
|
||||
exit $RC
|
||||
node --import tsx/esm --import ./open-sse/utils/setupPolyfill.ts --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 "${FILES[@]}"
|
||||
|
||||
fast-vitest:
|
||||
name: Vitest (fast-path)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
@@ -230,33 +122,15 @@ jobs:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
|
||||
# which is where flaky-detection volume actually comes from (ci.yml's heavy
|
||||
# jobs only run on the release PR). Advisory upload, own-origin only.
|
||||
- run: npm run test:vitest -- --reporter=default --reporter=junit --outputFile.junit=trunk-junit/vitest-fastpath.xml
|
||||
- name: Upload test results to Trunk (advisory)
|
||||
if: ${{ always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) }}
|
||||
continue-on-error: true
|
||||
uses: trunk-io/analytics-uploader@385f1ccdf345b4532dc4b6c665dd432b702b8e28 # v2.1.2
|
||||
with:
|
||||
junit-paths: trunk-junit/**/*.xml
|
||||
org-slug: omniroute
|
||||
token: ${{ secrets.TRUNK_TOKEN }}
|
||||
- run: npm run test:vitest
|
||||
|
||||
fast-unit:
|
||||
name: Unit Tests fast-path (${{ matrix.shard }}/4)
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — see fast-gates (own-origin + flag; fork/unset → ubuntu-latest).
|
||||
# This is the heaviest fast-path job; 4-way sharding (was 2, #6781) halves the
|
||||
# critical path again (~8.5min → ~4.5min on ubuntu-latest; ~2min on the 8-slot
|
||||
# runner box). Node's native --test-shard=N/total takes any denominator — only
|
||||
# this matrix and the TEST_SHARD env below encode the shard count.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
name: Unit Tests fast-path (${{ matrix.shard }}/2)
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
shard: [1, 2, 3, 4]
|
||||
shard: [1, 2]
|
||||
env:
|
||||
JWT_SECRET: ci-lint-secret-with-sufficient-length-for-validation
|
||||
API_KEY_SECRET: ci-lint-api-key-secret-long
|
||||
@@ -275,7 +149,7 @@ jobs:
|
||||
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
|
||||
- run: npm run test:unit:ci:shard
|
||||
env:
|
||||
TEST_SHARD: ${{ matrix.shard }}/4
|
||||
TEST_SHARD: ${{ matrix.shard }}/2
|
||||
|
||||
# ── Pacote 4 (plano mestre testes+CI, aprovado 2026-07-04) ─────────────────────────
|
||||
# No-new-warnings por PR via ESLint bulk suppressions nativo (>=9.24). O baseline
|
||||
@@ -291,8 +165,6 @@ jobs:
|
||||
# contribuidor NUNCA é bloqueado nem cobrado).
|
||||
lint-guard:
|
||||
name: No new ESLint warnings
|
||||
needs: changes
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
steps:
|
||||
@@ -304,18 +176,8 @@ jobs:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
path: |
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
key: eslint-${{ runner.os }}-${{ hashFiles('eslint.config.mjs', 'eslint.complexity-ratchets.config.mjs', 'config/quality/eslint-suppressions.json', 'package-lock.json') }}
|
||||
restore-keys: |
|
||||
eslint-${{ runner.os }}-
|
||||
- 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
|
||||
run: npx eslint . --suppressions-location config/quality/eslint-suppressions.json --max-warnings 0
|
||||
|
||||
# Merge-integrity: pega no PR os dois vazamentos crônicos de merge que hoje só
|
||||
# explodem na release-PR. (1) CHANGELOG-eat — o auto-resolve do merge come
|
||||
@@ -330,8 +192,6 @@ jobs:
|
||||
# nunca é bloqueado.
|
||||
merge-integrity:
|
||||
name: Merge integrity (changelog + generated skills)
|
||||
# Always on non-draft PRs — CHANGELOG/skills can break on docs-only merges too.
|
||||
if: ${{ github.event_name != 'pull_request' || (github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) }}
|
||||
runs-on: ubuntu-latest
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == true }}
|
||||
env:
|
||||
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -72,7 +72,6 @@ yarn-error.log*
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
!.env.example
|
||||
!.env.homolog.example
|
||||
# Provider API keys (never commit)
|
||||
*.api-key
|
||||
.nvidia-api-key
|
||||
@@ -234,18 +233,3 @@ omniroute.md
|
||||
# mise configuration
|
||||
mise.toml
|
||||
_artifacts/
|
||||
.claude-flow/
|
||||
|
||||
# ESLint file cache (npm run lint --cache / complexity ratchets)
|
||||
.eslintcache
|
||||
.eslintcache-complexity
|
||||
|
||||
|
||||
# CI/local quality artifacts (eslint-results.json, etc.)
|
||||
.artifacts/
|
||||
|
||||
# Homologation E2E suite (npm run homolog) — real-environment credentials + report output
|
||||
.env.homolog
|
||||
tests/homolog/.auth/
|
||||
tests/homolog/ui/.auth/
|
||||
homolog-report/
|
||||
|
||||
@@ -74,16 +74,3 @@
|
||||
# '''tests/unit/''',
|
||||
# ]
|
||||
#
|
||||
|
||||
[[rules]]
|
||||
# Falsos-positivos comprovados do generic-api-key — zerados em 2026-07-13 (WS6/D3,
|
||||
# plano v3.8.49). Revisar em v3.9.0. Nenhum é credencial: dois são NOMES DE CAMPO
|
||||
# de métricas de latência; o terceiro é o valor PÚBLICO de um beta header da API
|
||||
# da Anthropic (documentado publicamente, não é segredo).
|
||||
id = "generic-api-key"
|
||||
[rules.allowlist]
|
||||
description = "Field names + public Anthropic beta-header value (não são segredos)"
|
||||
regexes = [
|
||||
'''latencyP\d{2}Ms''',
|
||||
'''interleaved-thinking-2025-05-14''',
|
||||
]
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
#!/usr/bin/env sh
|
||||
# .husky/pre-push — intentionally light.
|
||||
# any-budget + tracked-artifacts already run on pre-commit; re-running them on
|
||||
# every push only doubles local wall time for the same existence reason (CI still
|
||||
# enforces both). Keep this hook as a PATH/npm sanity check + reminder.
|
||||
# Intentionally excludes test:unit / typecheck (slow; covered by CI).
|
||||
# .husky/pre-push — fast deterministic gates (<10s total)
|
||||
# Intentionally excludes test:unit (slow; covered by CI pre-push remote run).
|
||||
# Activated: 2026-06-13 (6A.12 — replaced commented-out test:unit stub)
|
||||
|
||||
if ! command -v npm >/dev/null 2>&1; then
|
||||
echo "⚠️ npm not found in PATH — skipping pre-push hooks"
|
||||
@@ -11,5 +9,4 @@ if ! command -v npm >/dev/null 2>&1; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# No-op success: real local gates live in pre-commit; CI owns the rest.
|
||||
exit 0
|
||||
npm run check:any-budget:t11 && npm run check:tracked-artifacts
|
||||
|
||||
55
.mergify.yml
55
.mergify.yml
@@ -1,55 +0,0 @@
|
||||
# Mergify merge queue — WS3.4/D5 of the v3.8.49 quality/velocity master plan.
|
||||
#
|
||||
# WHY: ~85-100 active PR authors/month and 300+ PRs/week peaks, all merged by ONE
|
||||
# identity. The manual merge-train validated batches by hand; this queue automates
|
||||
# it with batching + automatic batch bisection (a red batch of N costs ~log2(N)
|
||||
# revalidations instead of N). Mergify Open Source plan: free, unlimited, public repo.
|
||||
#
|
||||
# GOVERNANCE (non-negotiable, mirrors CLAUDE.md Hard Rules #21/#22 + the owner's
|
||||
# pre-merge ⭐ gate):
|
||||
# • A PR enters the queue ONLY via the `queue` label — applied by the owner (or a
|
||||
# session acting for the owner) AFTER the pre-merge ⭐ report/decision. The label
|
||||
# IS the merge approval; Mergify only executes it.
|
||||
# • During a release-freeze (open issue labeled `release-freeze`), do NOT label PRs
|
||||
# targeting the frozen branch — the freeze is a human-honored coordination signal
|
||||
# the queue cannot see. Retarget to the active release/vX+1 first (Hard Rule #21).
|
||||
# • Never label a PR another session is actively working (Hard Rule #22b).
|
||||
# • Fallback path if Mergify misbehaves or the OSS plan changes: the manual
|
||||
# merge-train runbook (docs/ops/MERGE_TRAIN.md) — remove labels, proceed by hand.
|
||||
|
||||
queue_rules:
|
||||
- name: release
|
||||
# Any current or future release branch — the reason GitHub's native queue was
|
||||
# rejected (no wildcard support on personal-account repos).
|
||||
queue_conditions:
|
||||
- base~=^release/v\d+\.\d+\.\d+$
|
||||
- label=queue
|
||||
- -draft
|
||||
- -conflict
|
||||
# "Everything that ran is green, nothing still running, AND the always-on
|
||||
# anchor check succeeded" — robust to the path-filtered fast-gates (docs-only
|
||||
# PRs skip code jobs; matrix shard names vary) while never fail-open: a PR with
|
||||
# zero checks cannot vacuously merge, because `Merge integrity` runs on EVERY
|
||||
# non-draft PR (quality.yml) and must be an affirmative success. Review approval
|
||||
# is intentionally NOT a condition here: the owner-applied `queue` label IS the
|
||||
# approval in this repo's single-maintainer model (see governance header).
|
||||
merge_conditions:
|
||||
- "#check-failure=0"
|
||||
- "#check-pending=0"
|
||||
- "#check-success>=1"
|
||||
- check-success=Merge integrity (changelog + generated skills)
|
||||
# Batching: validate up to 10 queued PRs together (the manual train's sweet spot);
|
||||
# don't hold a lone PR hostage waiting for siblings.
|
||||
batch_size: 10
|
||||
batch_max_wait_time: 5 min
|
||||
# Squash keeps the one-commit-per-PR history the CHANGELOG reconciliation expects.
|
||||
merge_method: squash
|
||||
|
||||
pull_request_rules:
|
||||
- name: clean up the queue label after merge
|
||||
conditions:
|
||||
- merged
|
||||
actions:
|
||||
label:
|
||||
remove:
|
||||
- queue
|
||||
@@ -1,5 +1,2 @@
|
||||
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
|
||||
docs/reference/ENVIRONMENT.md
|
||||
|
||||
# Dense auto-generated free-tier budget rows (one object per line) — prettier multi-line expand blows past file-size cap 800.
|
||||
open-sse/config/freeModelCatalog.data.ts
|
||||
|
||||
10
.vscode/settings.json
vendored
10
.vscode/settings.json
vendored
@@ -48,19 +48,11 @@
|
||||
"**/.build",
|
||||
"**/dist",
|
||||
"**/coverage",
|
||||
"**/.worktrees",
|
||||
"**/.claude/worktrees",
|
||||
"**/electron",
|
||||
"**/_references",
|
||||
"**/_mono_repo",
|
||||
"**/_tasks"
|
||||
"**/.worktrees"
|
||||
]
|
||||
},
|
||||
|
||||
// Para esconder os diretórios gerados da árvore do Explorer, descomente:
|
||||
// (MANTIDO comentado — o dono precisa ver _references/_mono_repo/_tasks na árvore.
|
||||
// A performance é resolvida por watcherExclude + search.exclude + tsserver, sem
|
||||
// precisar escondê-los do Explorer.)
|
||||
// "files.exclude": {
|
||||
// "**/.worktrees": true,
|
||||
// "**/coverage": true,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"scripts": {
|
||||
"build": "tsup",
|
||||
"clean": "rm -rf dist",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts tests/provider-id-routing.test.ts",
|
||||
"test": "node --import tsx/esm --test tests/scaffold.test.ts tests/auth.test.ts tests/options-schema.test.ts tests/multi-instance.test.ts tests/fetch-interceptor.test.ts tests/provider.test.ts tests/gemini-sanitize.test.ts tests/combos.test.ts tests/config-shim.test.ts tests/features.test.ts tests/usable-combo.test.ts tests/disk-snapshot-perms.test.ts tests/fork-features.test.ts tests/auto-combo-context.test.ts",
|
||||
"prepublishOnly": "npm run clean && npm run build && npm test"
|
||||
},
|
||||
"keywords": [
|
||||
|
||||
@@ -238,26 +238,9 @@ function trimLeadingDashes(value: string): string {
|
||||
*/
|
||||
export function resolveOmniRoutePluginOptions(
|
||||
opts?: OmniRoutePluginOptions
|
||||
): Required<
|
||||
Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">
|
||||
> & {
|
||||
/**
|
||||
* #6859: the UNPREFIXED provider id ("omniroute", "omniroute-preprod", …).
|
||||
* `providerId` above is auto-prefixed with "opencode-" ONLY to satisfy OC
|
||||
* 1.17.8+'s native-adapter gate ({openai, anthropic, opencode*}) — that
|
||||
* prefixed value is OC-internal and must be used ONLY for AuthHook.provider
|
||||
* and provider-registration keys (the OC config-hook top-level
|
||||
* `provider.<id>` block). `omnirouteProviderId` MUST be used everywhere an
|
||||
* identifier reaches or represents something OmniRoute's own server parses
|
||||
* (model `id` prefix, `ModelV2.providerID`, combo catalog keys in the
|
||||
* dynamic provider hook) — OmniRoute's `parseModel()` has no alias for
|
||||
* "opencode-<x>", so a prefixed id there is unrecoverable and credential
|
||||
* lookup fails with "No credentials for opencode-<x>".
|
||||
*/
|
||||
omnirouteProviderId: string;
|
||||
} & Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
|
||||
): Required<Pick<OmniRoutePluginOptions, "providerId" | "displayName" | "modelCacheTtl">> &
|
||||
Pick<OmniRoutePluginOptions, "baseURL" | "features"> {
|
||||
const rawProviderId = opts?.providerId ?? OMNIROUTE_PROVIDER_KEY;
|
||||
const omnirouteProviderId = trimLeadingOpencodePrefix(rawProviderId);
|
||||
// OC 1.17.8+ native-adapter gate rejects providerID not in
|
||||
// {openai, anthropic, opencode*}. Silently prefix so existing
|
||||
// configs (providerId: "omniroute") keep working.
|
||||
@@ -275,7 +258,6 @@ export function resolveOmniRoutePluginOptions(
|
||||
: DEFAULT_MODEL_CACHE_TTL_MS;
|
||||
return {
|
||||
providerId,
|
||||
omnirouteProviderId,
|
||||
displayName,
|
||||
modelCacheTtl,
|
||||
baseURL: opts?.baseURL,
|
||||
@@ -283,18 +265,6 @@ export function resolveOmniRoutePluginOptions(
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip a leading "opencode-" prefix (added only for the OC native-adapter
|
||||
* gate — see `resolveOmniRoutePluginOptions`) so the returned id is safe to
|
||||
* embed in anything OmniRoute's own server parses. A user-supplied
|
||||
* `providerId: "opencode-omniroute"` (already prefixed) resolves to the same
|
||||
* unprefixed "omniroute" as the default, matching `providerId`'s own
|
||||
* idempotent-prefix handling above.
|
||||
*/
|
||||
function trimLeadingOpencodePrefix(rawProviderId: string): string {
|
||||
return rawProviderId.startsWith("opencode-") ? rawProviderId.slice("opencode-".length) : rawProviderId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strict parse of raw plugin options (as received from opencode.json or a
|
||||
* direct factory call) into the validated `OmniRoutePluginOptions` shape.
|
||||
@@ -2691,8 +2661,7 @@ export function createOmniRouteProviderHook(
|
||||
if (canonicalDedup.has(entry.id)) continue;
|
||||
if (usable && !isUsableRawModelId(entry.id, usable, rawEnrichment)) continue;
|
||||
const model = mapRawModelToModelV2(entry, {
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
providerId: resolved.omnirouteProviderId,
|
||||
providerId: resolved.providerId,
|
||||
baseURL,
|
||||
apiFormat: resolved.features?.apiFormat,
|
||||
});
|
||||
@@ -2857,8 +2826,7 @@ export function createOmniRouteProviderHook(
|
||||
const mapped = mapComboToModelV2(
|
||||
combo,
|
||||
memberEntries,
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
resolved.omnirouteProviderId,
|
||||
resolved.providerId,
|
||||
baseURL,
|
||||
features.apiFormat
|
||||
);
|
||||
@@ -2877,8 +2845,7 @@ export function createOmniRouteProviderHook(
|
||||
}
|
||||
}
|
||||
|
||||
// #6859: server-facing key — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
const comboKey = buildComboKey(combo, usedComboKeys, resolved.omnirouteProviderId);
|
||||
const comboKey = buildComboKey(combo, usedComboKeys, resolved.providerId);
|
||||
|
||||
// Collision policy: combos win. Warn ONCE per (cacheKey, comboKey)
|
||||
// when overwriting a same-key raw model so the operator can spot
|
||||
@@ -2980,8 +2947,7 @@ export function createOmniRouteProviderHook(
|
||||
},
|
||||
status: "active",
|
||||
release_date: "",
|
||||
// #6859: server-facing id — NOT the OC-gate-prefixed `resolved.providerId`.
|
||||
providerID: resolved.omnirouteProviderId,
|
||||
providerID: resolved.providerId,
|
||||
options: {},
|
||||
headers: {},
|
||||
};
|
||||
|
||||
@@ -447,14 +447,14 @@ test("models() returns combo entries merged into the map", async () => {
|
||||
|
||||
// 3 raw models + 1 combo = 4 entries
|
||||
assert.equal(Object.keys(out).length, 4);
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
assert.ok(out["omniroute/claude-secondary"]);
|
||||
assert.ok(out["omniroute/gemini-3-flash"]);
|
||||
assert.ok(out["omniroute/claude-tier"]);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
||||
assert.ok(out["opencode-omniroute/gemini-3-flash"]);
|
||||
assert.ok(out["opencode-omniroute/claude-tier"]);
|
||||
|
||||
const combo = out["omniroute/claude-tier"];
|
||||
const combo = out["opencode-omniroute/claude-tier"];
|
||||
assert.equal(combo.name, "Claude Tier");
|
||||
assert.equal(combo.providerID, "omniroute");
|
||||
assert.equal(combo.providerID, "opencode-omniroute");
|
||||
// LCD over claude-primary (200k, reasoning) + claude-secondary (100k, no reasoning)
|
||||
assert.equal(combo.limit.context, 100_000);
|
||||
assert.equal(combo.capabilities.reasoning, false);
|
||||
@@ -478,11 +478,11 @@ test("models(): combo with unknown member ids degrades to all-false LCD posture"
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["omniroute/phantom-combo"]);
|
||||
assert.ok(out["opencode-omniroute/phantom-combo"]);
|
||||
// With zero resolvable members, LCD = all-false (defensive posture).
|
||||
assert.equal(out["omniroute/phantom-combo"].capabilities.toolcall, false);
|
||||
assert.equal(out["omniroute/phantom-combo"].capabilities.reasoning, false);
|
||||
assert.equal(out["omniroute/phantom-combo"].limit.context, 0);
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.toolcall, false);
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].capabilities.reasoning, false);
|
||||
assert.equal(out["opencode-omniroute/phantom-combo"].limit.context, 0);
|
||||
});
|
||||
|
||||
test("models(): hidden combos are excluded from the map", async () => {
|
||||
@@ -505,8 +505,8 @@ test("models(): hidden combos are excluded from the map", async () => {
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.ok(out["omniroute/visible"]);
|
||||
assert.ok(!out["omniroute/hidden"], "hidden combo must be omitted");
|
||||
assert.ok(out["opencode-omniroute/visible"]);
|
||||
assert.ok(!out["opencode-omniroute/hidden"], "hidden combo must be omitted");
|
||||
});
|
||||
|
||||
test("models(): combo name exactly matches raw model id → raw deleted, raw deleted, no warn", async () => {
|
||||
@@ -530,8 +530,8 @@ test("models(): combo name exactly matches raw model id → raw deleted, raw del
|
||||
});
|
||||
|
||||
// Raw model replaced by combo of the same key; combo now lives at the bare slug.
|
||||
assert.ok(out["omniroute/claude-primary"], "combo surfaces under prefixed key");
|
||||
assert.equal(out["omniroute/claude-primary"].name, "claude-primary");
|
||||
assert.ok(out["opencode-omniroute/claude-primary"], "combo surfaces under prefixed key");
|
||||
assert.equal(out["opencode-omniroute/claude-primary"].name, "claude-primary");
|
||||
|
||||
// No collision warning fires — dedup makes keys disjoint.
|
||||
const collisionWarns = warnings.filter((w) => {
|
||||
@@ -565,8 +565,8 @@ test("models(): two combos with same slug → second gets disambiguator suffix",
|
||||
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
// First combo gets the bare slug; second gets disambiguated.
|
||||
assert.ok(out["omniroute/claude"], "first combo at prefixed slug");
|
||||
assert.ok(out["omniroute/claude-uuid"], "second combo disambiguated by id prefix");
|
||||
assert.ok(out["opencode-omniroute/claude"], "first combo at prefixed slug");
|
||||
assert.ok(out["opencode-omniroute/claude-uuid"], "second combo disambiguated by id prefix");
|
||||
});
|
||||
|
||||
test("models(): combos fetch fails → falls back to models-only, warn emitted, no throw", async () => {
|
||||
@@ -583,8 +583,8 @@ test("models(): combos fetch fails → falls back to models-only, warn emitted,
|
||||
|
||||
// Catalog includes the models but NOT any combo entries.
|
||||
assert.equal(Object.keys(out).length, 2);
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
assert.ok(out["omniroute/claude-secondary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-secondary"]);
|
||||
|
||||
// Soft-fail warning surfaced.
|
||||
const softFail = warnings.find((w) => {
|
||||
@@ -609,7 +609,7 @@ test("models(): combos cached + reused within TTL (one combo fetch per TTL windo
|
||||
const second = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
assert.equal(combosFetcher.callCount(), 1, "combos fetched only once within TTL");
|
||||
assert.equal(modelsFetcher.callCount(), 1, "models fetched only once within TTL");
|
||||
assert.ok(second["omniroute/claude-tier"]);
|
||||
assert.ok(second["opencode-omniroute/claude-tier"]);
|
||||
});
|
||||
|
||||
test("models(): combos refetched after TTL expiry (same key as models)", async () => {
|
||||
@@ -701,7 +701,7 @@ test("models(): nested combo-ref context is the min of nested + raw members", as
|
||||
{ fetcher: modelsFetcher, combosFetcher }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-z") as never });
|
||||
const masterLight = out["omniroute/master-light"];
|
||||
const masterLight = out["opencode-omniroute/master-light"];
|
||||
assert.ok(masterLight, "MASTER-LIGHT entry must exist");
|
||||
assert.equal(
|
||||
masterLight.limit.context,
|
||||
|
||||
@@ -376,8 +376,7 @@ test("provider hook: enrichment fetcher called when features.enrichment !== fals
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 1, "enrichment fetcher called once");
|
||||
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId.
|
||||
const m = out["omniroute/claude-sonnet-4-6"];
|
||||
const m = out["opencode-omniroute/claude-sonnet-4-6"];
|
||||
assert.equal(m.name, "Claude Sonnet 4.6", "enrichment name overlay applied");
|
||||
assert.equal(m.cost.input, 3, "enrichment pricing applied");
|
||||
assert.equal(m.cost.output, 15);
|
||||
@@ -403,7 +402,7 @@ test("provider hook: enrichment fetcher NOT called when features.enrichment:fals
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 0, "enrichment fetcher NOT called when gated off");
|
||||
assert.equal(
|
||||
out["omniroute/claude-sonnet-4-6"].name,
|
||||
out["opencode-omniroute/claude-sonnet-4-6"].name,
|
||||
"claude-sonnet-4-6",
|
||||
"raw id preserved"
|
||||
);
|
||||
@@ -464,7 +463,7 @@ test("provider hook: compression metadata fetcher called when opted in", async (
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk") as never });
|
||||
assert.equal(called, 1, "compression metadata fetcher called");
|
||||
const combo = out["omniroute/claude-primary"];
|
||||
const combo = out["opencode-omniroute/claude-primary"];
|
||||
assert.ok(combo, "combo entry present");
|
||||
assert.match(
|
||||
combo.name,
|
||||
|
||||
@@ -1,99 +0,0 @@
|
||||
/**
|
||||
* Regression test for #6859.
|
||||
*
|
||||
* `resolveOmniRoutePluginOptions()` auto-prefixes `providerId` with
|
||||
* `"opencode-"` (commit 75b52e286) so OpenCode 1.17.8+'s native-adapter gate
|
||||
* accepts it as an OC-registered provider id. That prefixed value must stay
|
||||
* OC-internal (AuthHook.provider / provider registration keys) — it must
|
||||
* NEVER leak into the identifiers OmniRoute's own server parses to resolve
|
||||
* credentials (`mapRawModelToModelV2`'s `id`/`providerID`,
|
||||
* `mapComboToModelV2`'s `providerID`, and the dynamic-hook catalog keys).
|
||||
*
|
||||
* OmniRoute's server-side `parseModel()` (open-sse/services/model.ts) splits
|
||||
* a dispatched model string on `/` to recover the provider name and look up
|
||||
* credentials. If the plugin embeds the OC-gate-prefixed id in that string,
|
||||
* the server looks up credentials for a provider named "opencode-omniroute"
|
||||
* (which never exists in `src/shared/constants/providers.ts`) instead of
|
||||
* "omniroute" — producing the exact "No credentials for opencode-omniroute" /
|
||||
* "No active credentials for provider: opencode-omniroute" errors reported
|
||||
* in #6859.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
createOmniRouteProviderHook,
|
||||
mapRawModelToModelV2,
|
||||
resolveOmniRoutePluginOptions,
|
||||
} from "../src/index.js";
|
||||
|
||||
/**
|
||||
* Minimal stand-in for OmniRoute's own `parseModel()` (open-sse/services/
|
||||
* model.ts), which splits a dispatched `<providerID>/<modelID>` string on the
|
||||
* FIRST "/" to recover the provider name used for credential lookup. Kept
|
||||
* local (rather than cross-importing the real module) so this package's
|
||||
* self-contained test suite (`cd @omniroute/opencode-plugin && npm test`)
|
||||
* doesn't depend on the root repo's `@/*` path-alias resolution.
|
||||
*/
|
||||
function splitProviderFromDispatchedModel(modelStr: string): string {
|
||||
const idx = modelStr.indexOf("/");
|
||||
return idx === -1 ? modelStr : modelStr.slice(0, idx);
|
||||
}
|
||||
|
||||
const apiAuth = (key: string) => ({ type: "api" as const, key });
|
||||
|
||||
test("#6859: server-facing model id/providerID must resolve to the unprefixed provider name", () => {
|
||||
const resolved = resolveOmniRoutePluginOptions();
|
||||
|
||||
// The OC-gate-compatible id stays prefixed — it is legitimate for
|
||||
// AuthHook.provider / provider registration.
|
||||
assert.equal(resolved.providerId, "opencode-omniroute");
|
||||
|
||||
// A second, unprefixed id must be exposed for anything that reaches
|
||||
// OmniRoute's own server (model id prefix, ModelV2.providerID, combo keys).
|
||||
assert.equal(
|
||||
resolved.omnirouteProviderId,
|
||||
"omniroute",
|
||||
"resolveOmniRoutePluginOptions() must expose an unprefixed omnirouteProviderId"
|
||||
);
|
||||
|
||||
// A bare raw /v1/models entry (no existing "/" in its id — the common
|
||||
// case for OmniRoute's catalog) mapped with the server-facing id.
|
||||
const model = mapRawModelToModelV2(
|
||||
{ id: "claude-opus-4-7" },
|
||||
{ providerId: resolved.omnirouteProviderId, baseURL: "http://localhost:20128" }
|
||||
);
|
||||
|
||||
assert.equal(model.providerID, "omniroute");
|
||||
assert.equal(model.id, "omniroute/claude-opus-4-7");
|
||||
|
||||
// OpenCode dispatches back to OmniRoute using `providerID/modelKey`
|
||||
// (matches the issue's own repro: `-m opencode-omniroute/oc/big-pickle`).
|
||||
const dispatchedModelString = `${model.providerID}/claude-opus-4-7`;
|
||||
const parsedProvider = splitProviderFromDispatchedModel(dispatchedModelString);
|
||||
|
||||
assert.equal(
|
||||
parsedProvider,
|
||||
"omniroute",
|
||||
`server-side provider split resolved '${parsedProvider}', expected 'omniroute' — ` +
|
||||
`credentials lookup would fail for an OC-gate-prefixed provider id`
|
||||
);
|
||||
});
|
||||
|
||||
test("#6859: createOmniRouteProviderHook end-to-end — catalog keys/providerID never carry the OC-gate prefix", async () => {
|
||||
const hook = createOmniRouteProviderHook(
|
||||
{ baseURL: "https://or.example.com/v1" },
|
||||
{
|
||||
fetcher: async () => [{ id: "claude-opus-4-7" }],
|
||||
combosFetcher: async () => [],
|
||||
}
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-test") as never });
|
||||
const model = out["omniroute/claude-opus-4-7"];
|
||||
assert.ok(model, "catalog keyed under the unprefixed provider name");
|
||||
assert.equal(model.providerID, "omniroute");
|
||||
assert.ok(
|
||||
!model.providerID.startsWith("opencode-"),
|
||||
"the OC-gate prefix must never leak into ModelV2.providerID"
|
||||
);
|
||||
});
|
||||
@@ -101,10 +101,7 @@ test("models: extracts apiKey from ctx.auth (type=api) and calls fetcher with it
|
||||
assert.equal(fetcher.callCount(), 1);
|
||||
assert.deepEqual(fetcher.callsBy()[0], ["https://or.example.com/v1", "sk-abc"]);
|
||||
assert.equal(Object.keys(out).length, 3);
|
||||
// #6859: dynamic-hook catalog keys use the unprefixed omnirouteProviderId
|
||||
// ("omniroute"), not the OC-gate-prefixed hook.id ("opencode-omniroute") —
|
||||
// that prefix must never leak into anything OmniRoute's server parses.
|
||||
assert.ok(out["omniroute/claude-primary"]);
|
||||
assert.ok(out["opencode-omniroute/claude-primary"]);
|
||||
});
|
||||
|
||||
test("models: returns {} when ctx.auth is null/undefined/wrong-type/empty-key", async () => {
|
||||
@@ -155,17 +152,13 @@ test("models: maps a sample /v1/models entry to ModelV2 (sanity)", async () => {
|
||||
{ fetcher, combosFetcher: async () => [] }
|
||||
);
|
||||
const out = await hook.models!({} as never, { auth: apiAuth("sk-abc") as never });
|
||||
// #6859: dynamic-hook catalog keys/ids/providerID use the unprefixed
|
||||
// omnirouteProviderId ("omniroute") — the OC-gate prefix ("opencode-")
|
||||
// must stay OC-internal (hook.id / AuthHook.provider) and never leak into
|
||||
// anything OmniRoute's own server parses for credential lookup.
|
||||
const claude = out["omniroute/claude-primary"];
|
||||
const claude = out["opencode-omniroute/claude-primary"];
|
||||
assert.ok(claude, "claude-primary present");
|
||||
// `mapRawModelToModelV2` stamps the provider prefix on the id so OC's
|
||||
// static-catalog reader resolves `(providerID, modelID)` from the key.
|
||||
assert.equal(claude.id, "omniroute/claude-primary");
|
||||
assert.equal(claude.id, "opencode-omniroute/claude-primary");
|
||||
assert.equal(claude.name, "claude-primary");
|
||||
assert.equal(claude.providerID, "omniroute");
|
||||
assert.equal(claude.providerID, "opencode-omniroute");
|
||||
assert.equal(claude.api.id, "openai-compatible");
|
||||
assert.equal(claude.api.url, "https://or.example.com/v1");
|
||||
assert.equal(claude.api.npm, "@ai-sdk/openai-compatible");
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
## Project
|
||||
|
||||
Unified AI proxy/router — route any LLM through one endpoint. Multi-provider support
|
||||
with **250 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
with **237 provider entries** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
|
||||
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
|
||||
with **MCP Server** (94 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
|
||||
> **Live counts (v3.8.47)**: providers 250 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
|
||||
> **Live counts (v3.8.43)**: providers 237 · MCP tools 94 · MCP scopes 30 · A2A skills 6 ·
|
||||
> open-sse services 134 · routing strategies 17 · auto-combo scoring factors 12 ·
|
||||
> DB modules 95 · DB migrations 110 · base tables 17 · search providers 11 ·
|
||||
> i18n locales 42. **Refresh with `npm run check:docs-all`.**
|
||||
@@ -539,7 +539,7 @@ For any non-trivial change, read the matching deep-dive first:
|
||||
| Repo navigation | [`docs/architecture/REPOSITORY_MAP.md`](docs/architecture/REPOSITORY_MAP.md) |
|
||||
| Architecture | [`docs/architecture/ARCHITECTURE.md`](docs/architecture/ARCHITECTURE.md) |
|
||||
| Engineering reference | [`docs/architecture/CODEBASE_DOCUMENTATION.md`](docs/architecture/CODEBASE_DOCUMENTATION.md) |
|
||||
| Auto-Combo (12-factor, 18 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
|
||||
| Auto-Combo (12-factor, 17 strategies) | [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md) |
|
||||
| Resilience (3 layers) | [`docs/architecture/RESILIENCE_GUIDE.md`](docs/architecture/RESILIENCE_GUIDE.md) |
|
||||
| Skills | [`docs/frameworks/SKILLS.md`](docs/frameworks/SKILLS.md) |
|
||||
| Memory | [`docs/frameworks/MEMORY.md`](docs/frameworks/MEMORY.md) |
|
||||
|
||||
341
CHANGELOG.md
341
CHANGELOG.md
@@ -4,344 +4,7 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.49] — TBD
|
||||
|
||||
---
|
||||
|
||||
## [3.8.48] — 2026-07-13
|
||||
|
||||
> ⚠️ **Hotfix release.** The published npm package for 3.8.47 crashed on every boot ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065)) and was deprecated — **3.8.48 is the first installable release of the v3.8.47 cycle**, so everything listed under [3.8.47] below ships here.
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(build):** ship `dist/head-response-guard.cjs` in the npm tarball — the prepublish prune allowlist lacked it, so every `omniroute` boot of the published 3.8.47 crashed with `ERR_MODULE_NOT_FOUND` (3rd occurrence of this class after tls-options/3.8.41); now allowlisted, enforced by `check:pack-artifact`, and guarded by a closure test that derives every `server-ws.mjs` sibling import ([#7065](https://github.com/diegosouzapw/OmniRoute/issues/7065), [#7040](https://github.com/diegosouzapw/OmniRoute/issues/7040))
|
||||
- **fix(build):** Electron Windows packaging — the better-sqlite3 Electron-ABI rebuild now spawns `npx.cmd` through a shell (Node's CVE-2024-27980 hardening made the shell-less spawn fail with `status null` on Windows runners, breaking the v3.8.47 desktop build)
|
||||
- **fix(ci):** Sonar quality gate zeroed on new code — the coverage lcov now reaches the scanner at `coverage/lcov.info` (it read 0% on every scan), the async `isCloudEnabled()` gate in the Kiro auto-import route is awaited (cloud sync ran even when disabled), the dead `structuredClone` fallback in the reasoning-split clone is a real JSON fallback, the codex executor handles the async `reader.cancel()` rejection, deterministic `localeCompare` sorts, a path-traversal guard in `classify-pr-changes.mjs`, and the Docker better-sqlite3 rebuild uses npm's bundled node-gyp instead of `npx --yes`
|
||||
- **chore(ci):** the Sonar quality gate is informational (`sonar.qualitygate.wait=false`) while the org's SonarCloud plan cannot associate the tuned "OmniRoute way" gate (coverage ≥60 aligned with the repo floor)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.47] — 2026-07-13
|
||||
|
||||
_Living section — bullets land here as PRs merge into `release/v3.8.47` (parallel-cycle model; cycle opened at the v3.8.46 release freeze). Finalized at the v3.8.47 release._
|
||||
|
||||
- **9router Codex import**: the Codex bulk-import endpoint (`POST /api/oauth/codex/import`) now accepts 9router's camelCase account export (`accessToken`/`refreshToken`/`idToken`/`expiresAt` + nested `providerSpecificData`), not just snake_case — `normalizeCodexImportRecord` maps the camelCase aliases onto the existing snake_case keys, filling each only when absent so snake_case/mixed exports keep working unchanged ([#6665](https://github.com/diegosouzapw/OmniRoute/issues/6665)) — thanks @deadcoder0904. Regression guard: `tests/unit/codexBulkImport.test.ts` (9router camelCase record, pre-supplied `providerSpecificData` without an id_token, snake_case-not-overridden, and a full `{accounts:[...]}` flatten).
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(plugins):** Langfuse observability plugin. ([#6577](https://github.com/diegosouzapw/OmniRoute/pull/6577) — thanks @chirag127)
|
||||
|
||||
- **feat(combo):** context requirements config for per-target filtering in combos. ([#6907](https://github.com/diegosouzapw/OmniRoute/pull/6907) — thanks @oyi77)
|
||||
- **feat(providers):** icons for 46 providers that were missing images. ([#6926](https://github.com/diegosouzapw/OmniRoute/pull/6926) — thanks @oyi77)
|
||||
- **feat(compression):** vendored GCF (Headroom) codec updated to spec v3.2 (nested flattening). ([#6838](https://github.com/diegosouzapw/OmniRoute/pull/6838) — thanks @blackwell-systems)
|
||||
- **feat(proxy):** shorthand proxy formats + protocol header mode for bulk import. ([#6867](https://github.com/diegosouzapw/OmniRoute/pull/6867) — thanks @growab)
|
||||
- **feat(provider):** OpenVecta AI inference gateway. ([#6833](https://github.com/diegosouzapw/OmniRoute/pull/6833) — thanks @hajilok)
|
||||
- **feat(i18n):** Traditional Chinese (zh-TW) localization for frontend and CLI. ([#6320](https://github.com/diegosouzapw/OmniRoute/pull/6320) — thanks @lunkerchen)
|
||||
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint. ([#6709](https://github.com/diegosouzapw/OmniRoute/pull/6709) — thanks @diegosouzapw)
|
||||
- **feat(routing):** per-model web-search/web-fetch interception rules. ([#3384](https://github.com/diegosouzapw/OmniRoute/issues/3384), [#6814](https://github.com/diegosouzapw/OmniRoute/pull/6814) — thanks @diegosouzapw)
|
||||
- **feat(release):** `changelog.d/` fragments — eliminates the CHANGELOG merge-storm cascade. ([#6783](https://github.com/diegosouzapw/OmniRoute/pull/6783) — thanks @diegosouzapw)
|
||||
- **feat(quality):** `validate-release-green --full-ci` reproduces the entire ci.yml static gate set locally. ([#6583](https://github.com/diegosouzapw/OmniRoute/pull/6583) — thanks @diegosouzapw)
|
||||
|
||||
- **feat(dashboard):** sidebar quick-filter — a search input at the top of the expanded dashboard sidebar (`src/shared/components/Sidebar.tsx`) filters nav sections/groups/items client-side by label as you type, reusing the existing `common.search`/`common.noResults` i18n keys (zero new locale edits) and the shared `Input` `icon="search"` pattern; matching sections auto-expand while searching (bypassing the accordion/pin state) and collapse back to normal once the query is cleared. Pure filtering logic extracted into `filterSidebarSectionsByQuery()` (`src/shared/utils/sidebarSearch.ts`) for isolated unit testing. Regression guard: `tests/unit/sidebar-search-filter.test.ts`, `src/shared/components/Sidebar.search.test.tsx`. (#4013 — thanks @crochabe-cyber)
|
||||
- **feat(combo):** `auto/*` combos gain a strict budget-cap fallback policy — `X-OmniRoute-Budget-Fallback: strict` (or the persisted `config.budgetFallback: "strict"`) makes an over-budget request fail fast with `HTTP 402` instead of the previous silent fallback to the globally cheapest candidate, which could still exceed the cap. The default (`cheapest`) preserves existing behavior. Builds on the existing `X-OmniRoute-Budget`/`X-OmniRoute-Mode` per-request controls (#6023/#6024/#6025), consolidated into `resolveRequestAutoControls()`. Regression guard: `tests/unit/auto-combo-budget-fallback-3470.test.ts`. (#3470)
|
||||
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
|
||||
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
|
||||
- **feat(dashboard):** 9router-parity **Routing Strategy** settings card on Settings → Routing, plus a per-provider account-routing override on the provider detail page ([#6678](https://github.com/diegosouzapw/OmniRoute/pull/6678)) — surfaces the existing account round-robin / sticky-limit knobs and adds a new combo-level sticky round-robin (`comboStickyRoundRobinLimit`, resolved via `resolveComboStickyRoundRobinLimit()` — per-combo → global combo sticky → account sticky cascade) so combo targets can batch calls per target the same way account fallback already does. A new `providerStrategies` setting (Zod-validated map, `src/shared/validation/settingsSchemas.ts`) lets a specific provider override the global `fallbackStrategy`/`stickyRoundRobinLimit` without touching the account-wide default, wired into `getProviderCredentials()` (`src/sse/services/auth.ts`) ahead of the global fallback. Regression guard: `tests/unit/combo-rr-sticky-9router.test.ts`, `tests/unit/settings-ui-layout-static.test.ts`. (thanks @SeaXen)
|
||||
- **feat(icons):** provider logos now resolve local SVG assets first for faster rendering, with a 5-tier fallback chain — local SVG → `@lobehub/icons` React components → `thesvg.org` CDN (external SVG for unknown providers) → local PNG → generic AI icon — replacing the previous LobeHub-first order. Adds dozens of first-party provider SVGs and migrates several bitmap logos (continue/copilot/cursor/deepgram/heroku/openclaw/ovhcloud) from PNG to SVG. Regression guard: `tests/unit/ui/ProviderIcon-icon-url.test.tsx`. ([#6317](https://github.com/diegosouzapw/OmniRoute/pull/6317) — thanks @hamsa0x7)
|
||||
- **Skill Collector CLI detection**: new `GET /api/skills/collect/detect` + `POST /api/skills/collect/install` (and the `cli-skill-collector` agent skill) detect which coding CLIs (Claude Code, Codex, Cursor, Copilot, Cline, Hermes, OpenCode, etc.) are installed locally via `getCliRuntimeStatus()`, match them against GitHub agent-skill repos, and plan an install path per tool — replacing the standalone Skill Collector Python app. Both new routes and `GET/POST /api/github-skills` now require management auth (`requireManagementAuth()`) and are loopback-gated (`LOCAL_ONLY_API_PREFIXES` + `SPAWN_CAPABLE_PREFIXES`) since the detect route spawns a child process per candidate CLI tool (Hard Rules #15 + #17). The `omniroute_github_skills_install` MCP tool now reports the honest `action: "planned"` instead of `"installed"`, matching the REST route (#6294 — thanks @Moseyuh333)
|
||||
- **ClinePass dual-auth**: ClinePass now offers both sign-in methods on its dashboard page — OAuth (reusing the Cline WorkOS flow) as the primary "Connect" path, or a pasted BYOK API key via "Manual API key", instead of only the API-key-only provider shipped in #5942. The registry alias was aligned to `cp` (matching the `OAUTH_PROVIDERS` catalog alias) so `<alias>/<modelId>` routing resolves correctly, the OAuth refresh dispatch now routes `clinepass` to the shared Cline refresh flow, and the duplicate API-key-only catalog entry was removed to keep ClinePass listed once. Regression guard: `tests/unit/clinepass-provider.test.ts`. (#6126 — thanks @hajilok)
|
||||
- **feat(oauth):** Kiro/Amazon Q auto-import now supports enterprise **External IdP** ("Your organization") logins via Microsoft Entra/Okta/Auth0/OneLogin/Ping/Google/Cognito — these org-issued tokens are not AWS SSO tokens (no `aorAAAAAG`-prefixed refresh token) and can't refresh through the AWS OIDC/Kiro-social path, so `tryAwsSsoCache()` now detects them (`authMethod`/`provider === "externalidp"`) and refreshes via the org IdP's own `tokenEndpoint` (public-client OAuth2 refresh grant, no client secret), persisting `TokenType: EXTERNAL_IDP` gating so the runtime executor sends the header the AWS CodeWhisperer API requires for these accounts; `tokenEndpoint` is SSRF-guarded against an HTTPS + known-IdP-host-suffix allowlist. (#6363 — thanks @artickc)
|
||||
- **Kiro long-lived API key auth**: new `/api/oauth/kiro/api-key` route + `KiroService.validateApiKey` let a Kiro account be linked with a long-lived AWS CodeWhisperer/Kiro API key instead of the interactive OAuth device flow, with live per-account model discovery (`ListAvailableModels`, 5-minute cache) layered over the existing static registry fallback (#6587 — thanks @strangersp)
|
||||
- **Chaos Mode**: multi-model parallel/collaborative task execution — dispatches a task to every active provider connection at once (parallel) or chains outputs sequentially so each model builds on the previous one's answer (collaborative), configurable via Dashboard → Chaos Mode (`GET`/`PUT`/`DELETE /api/chaos/config`) and gated per-API-key via a new `chaosModeEnabled` permission (opt-in — disabled by default globally and per key). `POST /api/chaos/run` (dashboard session) and `POST /api/skills/collect/chaos` (external Bearer-token) delegate to a shared `executeChaosRun()` engine (`src/lib/chaos/chaosExecutor.ts`) that dispatches in-process via the established synthetic-Request/route-handler pattern (no network hop, no hardcoded port), with a concurrency cap (max 10 parallel), configurable `max_tokens` (256–128k), a clear error when `stream` is requested, and collaborative-chain info (provider order + input size). Fixes external Bearer-auth bypass and stale config-cache leakage. Regression guard: `tests/unit/chaos-config.test.ts`, `tests/unit/chaos-executor.test.ts`, `tests/unit/chaos-api-routes.test.ts`. ([#6728](https://github.com/diegosouzapw/OmniRoute/pull/6728) — thanks @Moseyuh333)
|
||||
- **feat(cli):** 2 new CLI tool integrations on Dashboard → CLI Tools — **omp** (Oh My Pi) and **letta** — each with binary detection, config apply/reset, and a settings card following the existing tool-card pattern. Both settings routes shell out to `which omp`/`which letta` to detect the local install, so they're loopback-gated (`LOCAL_ONLY_API_PREFIXES`, Hard Rules #15/#17) in addition to the shared `requireCliToolsAuth()` management-auth guard every cli-tools route requires, and route errors through `sanitizeErrorMessage()`; `src/lib/db/omp.ts` isolates the `omp` CLI's own local SQLite reads behind parameterized queries. (Note: the original PR also proposed **pi**, **codewhale**, and **jcode** integrations — those three had already shipped via a separate PR by the time this one was reconciled, so only omp+letta landed here.) Regression guard: `tests/unit/db/omp.test.ts`, `tests/unit/cli-tools-auth-hardening.test.ts`, `tests/integration/cli-settings-omp.test.ts`, `tests/integration/cli-settings-letta.test.ts`. ([#6318](https://github.com/diegosouzapw/OmniRoute/pull/6318) — thanks @hamsa0x7)
|
||||
- **feat(providers):** custom models now support a manual **Context Window Override** so an operator can correct a provider's misreported context length (e.g. reports 1M when the real limit is 128K) instead of the model silently getting dropped from combo routing once the wrong value lands in the catalog ([#4125](https://github.com/diegosouzapw/OmniRoute/issues/4125) — thanks @rucciva). Reuses the existing Feature-5004 `model_context_overrides` table (`source: "manual"`) — already the priority-0 source `getModelContextLimit()` (the function combo's context-window filter calls) reads ahead of the models.dev/registry/static catalog — so no new resolver logic was needed, only the missing write path: `PUT /api/provider-models` now accepts an optional `contextWindowOverride` (number to set, `null` to clear), `GET` surfaces the current value back on each custom-model row, and the provider detail page's custom-model edit form gained a Context Window Override field + badge. Regression guard: `tests/unit/provider-models-context-window-override-4125.test.ts`.
|
||||
- **fix(providers):** register OpenRouter as a rerank provider so `openrouter/cohere/rerank-*` models resolve instead of erroring `Invalid rerank model` (#6574 — thanks @rafpigna)
|
||||
- **fix(api):** `HEAD` requests no longer hang until client timeout on any route — valid, unknown, authed, or unauthed ([#6400](https://github.com/diegosouzapw/OmniRoute/issues/6400)), broader follow-up to the route-specific #6517 (`/v1/models`). Root cause: Next.js 16's App Router _route-handler_ pipeline (`next/dist/server/send-response.js`) correctly skips piping a `Response` body for `HEAD`, but its _page_-rendering pipeline (`next/dist/server/pipe-readable.js` → `pipeToNodeResponse`, used for every app-router page/layout render — including the `not-found` boundary any unmatched path falls through to) has no such check and always streams the full rendered body regardless of method; combined with Node's default keep-alive framing this left some clients unsure whether the (implicitly bodyless) `HEAD` response had actually finished. A new `scripts/dev/head-response-guard.cjs`, wired into both the dev/start custom server (`scripts/dev/run-next.mjs`) and the packaged standalone server (`scripts/dev/standalone-server-ws.mjs`) at the same tier as the existing `http-method-guard.cjs`/`peer-stamp.mjs` wrappers, discards any body bytes written for a `HEAD` request and forces `Connection: close` once `.end()` is called — independent of route existence or auth state, satisfying RFC 9110 §9.3.2. Regression guard: `tests/unit/head-request-closes-6400.test.ts`.
|
||||
- **feat(dashboard):** Provider Quota page (`Dashboard → Quota`) fills horizontal whitespace before stacking vertically ([#3520](https://github.com/diegosouzapw/OmniRoute/issues/3520)) — `QuotaCardGrid` previously stacked every provider group in a single vertical `flex flex-col`, and each group's own card grid didn't go multi-column until `md` (`grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4`). Provider groups now flow into a 2-column CSS multi-column layout on very wide (`2xl`) screens instead of an unconditional vertical stack, and each group's card grid starts at 2 columns immediately (`grid-cols-2 md:grid-cols-3 xl:grid-cols-4`), reaching higher density sooner on narrower-but-not-mobile viewports. Regression guard: `tests/unit/quota-card-grid-horizontal-layout.test.ts` (thanks @gdevenyi).
|
||||
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).
|
||||
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).
|
||||
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).
|
||||
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)
|
||||
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)
|
||||
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).
|
||||
- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc).
|
||||
- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell).
|
||||
- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn).
|
||||
- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).
|
||||
- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`.
|
||||
- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan).
|
||||
- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127).
|
||||
- **feat(dashboard):** search box on the Playground's raw model `<select>` (#4086) — the shared `ModelSelectModal` (combo builder + CLI-code cards) already had search, but Playground's `StudioConfigPane` model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via `matchesSearch`), while the currently selected model always stays pinned in the list even if it no longer matches the query, so typing never silently swaps the active selection. Reuses the existing `common.search` i18n key (already translated in all 42 locales) — no new translation key needed. Regression guard: `tests/unit/playground-model-selection-3731.test.ts` (`filterModelsByQuery`), `tests/unit/ui/playground-model-search-4086.test.tsx`.
|
||||
- **feat(usage):** Antigravity/agy quota widget now surfaces the **weekly** window alongside the existing per-model 5-hour window ([#4017](https://github.com/diegosouzapw/OmniRoute/issues/4017)) — the weekly limit isn't part of the per-model `retrieveUserQuota` response the fetcher already calls; it only appears in a separate, undocumented `retrieveUserQuotaSummary` RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new `usage/antigravityWeeklyQuota.ts` leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into `gemini_weekly`/`claude_gpt_weekly` quota entries, merged into the same `quotas` map the widget already renders generically. Regression guard: `tests/unit/antigravity-weekly-quota-4017.test.ts` (bucket parsing, the alternate `quotaSummary`-nested envelope, and end-to-end merge via `getUsageForProvider`).
|
||||
- **feat(codex):** Codex CLI compatibility shim — the Responses API `response.created`/`response.in_progress`/`response.completed` payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested, effort-suffixed model id (e.g. `gpt-5.5-xhigh`) instead of the bare upstream id (`gpt-5.5`), so the Codex CLI status line/model button shows the active reasoning effort ([#3697](https://github.com/diegosouzapw/OmniRoute/issues/3697)). `openaiToOpenAIResponsesResponse` (`open-sse/translator/response/openai-responses.ts`) now threads the upstream model into the Responses event objects; a new `isCodexOriginatedHeaders()` (`open-sse/config/codexIdentity.ts`, reusing PR #3481's `originator`/User-Agent detection) makes chatCore's existing opt-in `echoRequestedModelName` (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when `codex/gpt-5.5-xhigh` is routed through a combo to a non-codex upstream; `echoModelInObject`/`echoModelInSseLine` (`open-sse/services/responseModelEcho.ts`) now also rewrite the nested `response.model` field the Responses API uses. `/v1/models` still returns `models: []` for Codex (unchanged). Regression guard: `tests/unit/codex-effort-model-echo-3697.test.ts`.
|
||||
- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `Authorization: Bearer <token>`, and normalizes both z.ai's internal `delta_content`/`phase` SSE envelope and a pass-through OpenAI-shaped `choices[].delta` frame into standard chat-completion chunks. Registered in `WEB_COOKIE_PROVIDERS`, `WEB_SESSION_CREDENTIAL_REQUIREMENTS`, the provider registry (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)
|
||||
- **feat(compression):** update the vendored **GCF** codec behind the **Headroom** engine to spec **v3.2 (nested flattening)** ([#6837](https://github.com/diegosouzapw/OmniRoute/issues/6837)). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via `>`-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (`meta:{...}`, `tags:[...]`) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, `cl100k_base`), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the `[N]:` inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a `__proto__`/`constructor` path segment never mutates `Object.prototype`, and keys shadowing built-ins like `toString` now round-trip correctly instead of misparsing). Regression guard: `tests/unit/compression/headroom-smartcrusher.test.ts` (deep-nested + prototype-pollution cases).
|
||||
- **feat(providers):** Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
|
||||
- **chore(providers):** Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(dashboard):** the Proxy Registry settings page crashed at runtime (`ReferenceError: poolLoaded/bulkImportOpen is not defined`) — the #6625/#6909 hook-extraction refactors deleted 10 state declarations (`poolLoaded`, `poolSaving`, and the 8-member bulk-import family) while ~30 usages remained; all restored (caught by the release E2E; `typecheck:core` does not cover dashboard TSX — follow-up #7021). (thanks @diegosouzapw)
|
||||
|
||||
- **fix(combo):** `comboStickyRoundRobinLimit` now defaults to inherit (`null`) instead of `1` — the literal default silently shadowed the documented batched round-robin rotation (`stickyRoundRobinLimit: 3`), flipping every round-robin combo to per-request alternation (#6678 follow-up, caught by the release CI). (thanks @diegosouzapw)
|
||||
- **fix(ws):** the standalone LiveWS startup script exited 0 without ever listening — its bootstrapped child re-spawned with the import-suppressor `OMNIROUTE_ENABLE_LIVE_WS=0` and then honored it as an operator disable (#6072 follow-up, caught by the release CI). (thanks @diegosouzapw)
|
||||
|
||||
- **fix(api):** compression PUT schema accepts every catalog engine. ([#6792](https://github.com/diegosouzapw/OmniRoute/pull/6792) — thanks @Pitchfork-and-Torch)
|
||||
- **fix(compression):** surface fallback reasons in the preview response. ([#6461](https://github.com/diegosouzapw/OmniRoute/issues/6461), [#6519](https://github.com/diegosouzapw/OmniRoute/pull/6519) — thanks @chirag127)
|
||||
- **fix(providers):** fail fast on an empty auto-combo pool instead of a 15s timeout. ([#6458](https://github.com/diegosouzapw/OmniRoute/issues/6458), [#6546](https://github.com/diegosouzapw/OmniRoute/pull/6546) — thanks @chirag127)
|
||||
- **fix(compression):** honor UI-toggled engines in the stackedPipeline dispatch + surface substitutions. ([#6463](https://github.com/diegosouzapw/OmniRoute/issues/6463), [#6534](https://github.com/diegosouzapw/OmniRoute/pull/6534) — thanks @chirag127)
|
||||
- **fix(api):** return 400 for missing/invalid `messages` before model resolution. ([#6402](https://github.com/diegosouzapw/OmniRoute/issues/6402), [#6515](https://github.com/diegosouzapw/OmniRoute/pull/6515) — thanks @chirag127)
|
||||
- **fix(providers):** enrich the model_cooldown 429 body with a `retry_after` ISO timestamp + credential count. ([#6460](https://github.com/diegosouzapw/OmniRoute/issues/6460), [#6523](https://github.com/diegosouzapw/OmniRoute/pull/6523) — thanks @chirag127)
|
||||
|
||||
- **fix(sse):** default reasoning summary for effort-only Responses requests. ([#6807](https://github.com/diegosouzapw/OmniRoute/pull/6807) — thanks @rushsinging)
|
||||
- **fix(sse):** compression no-op treated as zero-savings, not inflation/silent-drop. ([#6883](https://github.com/diegosouzapw/OmniRoute/pull/6883) — thanks @chirag127)
|
||||
- **fix(oauth):** Trae OAuth client_id embedded via `resolvePublicCred()` (Hard Rule #11). ([#6870](https://github.com/diegosouzapw/OmniRoute/pull/6870) — thanks @chirag127)
|
||||
- **fix(sse):** combo path no longer trips the whole-provider breaker on a plain 429. ([#6868](https://github.com/diegosouzapw/OmniRoute/pull/6868) — thanks @chirag127)
|
||||
- **fix(api):** malformed JSON bodies now return 400 instead of 500. ([#6871](https://github.com/diegosouzapw/OmniRoute/pull/6871) — thanks @chirag127)
|
||||
- **fix(fusion):** judge selected from a surviving panel member when no explicit judge is configured. ([#6869](https://github.com/diegosouzapw/OmniRoute/pull/6869) — thanks @chirag127)
|
||||
- **fix(sse):** combo model lockout honors the parsed upstream quota reset. ([#6863](https://github.com/diegosouzapw/OmniRoute/issues/6863), [#6866](https://github.com/diegosouzapw/OmniRoute/pull/6866) — thanks @AgentKiller45)
|
||||
- **fix(dashboard):** logs detail modal no longer reopens on first close. ([#6830](https://github.com/diegosouzapw/OmniRoute/pull/6830) — thanks @MikeTuev)
|
||||
- **fix(usage):** honor xAI provider-reported exact cost. ([#6711](https://github.com/diegosouzapw/OmniRoute/pull/6711) — thanks @diegosouzapw)
|
||||
- **fix(kiro):** probe IdC region during profileArn discovery, cross-region (recovers #6099). ([#6840](https://github.com/diegosouzapw/OmniRoute/pull/6840) — thanks @diegosouzapw)
|
||||
- **fix(antigravity):** sanitize Cloud Code safety settings. ([#6839](https://github.com/diegosouzapw/OmniRoute/pull/6839) — thanks @diegosouzapw)
|
||||
- **fix(translator):** defer `content_block_start` until GLM streams the tool name. ([#6730](https://github.com/diegosouzapw/OmniRoute/pull/6730) — thanks @diegosouzapw)
|
||||
- **fix(translator):** strip empty `cloud_base_branch` from Cursor Subagent tool calls. ([#6729](https://github.com/diegosouzapw/OmniRoute/pull/6729) — thanks @diegosouzapw)
|
||||
- **fix(antigravity):** surface aborted Gemini tool calls off `end_turn`. ([#6713](https://github.com/diegosouzapw/OmniRoute/pull/6713) — thanks @diegosouzapw)
|
||||
- **fix(volcengine):** clamp Kimi `max_tokens` to the Ark endpoint cap. ([#6712](https://github.com/diegosouzapw/OmniRoute/pull/6712) — thanks @diegosouzapw)
|
||||
- **fix(codex):** surface capacity errors embedded in 200-OK SSE streams. ([#6710](https://github.com/diegosouzapw/OmniRoute/pull/6710) — thanks @diegosouzapw)
|
||||
- **fix(sse):** skip `thinkingConfig` for gemma models in openai→gemini translation. ([#6708](https://github.com/diegosouzapw/OmniRoute/pull/6708) — thanks @diegosouzapw)
|
||||
- **fix(oauth):** avoid bare-email dedup of Codex OAuth logins. ([#6706](https://github.com/diegosouzapw/OmniRoute/pull/6706) — thanks @diegosouzapw)
|
||||
- **fix(sse):** unwrap bare `{function:{…}}` tools in openai→claude translation. ([#6704](https://github.com/diegosouzapw/OmniRoute/pull/6704) — thanks @diegosouzapw)
|
||||
- **fix(db):** eliminate a redundant `getApiKeyMetadata` call in the embeddings route. ([#6929](https://github.com/diegosouzapw/OmniRoute/pull/6929) — thanks @oyi77)
|
||||
- **fix(db):** `authType` filter support in `getProviderConnections`. ([#6946](https://github.com/diegosouzapw/OmniRoute/pull/6946) — thanks @oyi77)
|
||||
- **fix(i18n):** the provider-detail (`/dashboard/providers/[id]`) visibility + free/paid model filter labels (`showVisibleOnly`, `showHiddenOnly`, `freeFilterAll`, `freeFilterFreeOnly`, `freeFilterPaidOnly`, `hideAllModels`, plus the currently-unused `filterVisible`/`filterHidden`/`filterByVisibility`) rendered as the literal `__MISSING__:<english>` sentinel in 15 locales, including pt-BR ([#6694](https://github.com/diegosouzapw/OmniRoute/issues/6694)) — `providerText()` (`providerPageHelpers.ts`) checks `t.has(key)` before falling back to clean English, and `t.has()` returns `true` even when the stored value is the `__MISSING__:` sentinel `scripts/i18n/sync-ui-keys.mjs` writes when mirroring keys across locales, so the sentinel rendered verbatim instead of the fallback. Disjoint key set from #6290 (`filterAll`/`filterActive`/`filterError`/`filterBanned`/`filterCreditsExhausted`). All 9 keys now carry real translations across the 15 affected locale files (`it`, `ja`, `ko`, `mr`, `ms`, `nl`, `no`, `phi`, `pl`, `pt`, `pt-BR`, `ro`, `ru`, `sk`, `sv`). Regression guard: `tests/unit/i18n-provider-visibility-filter-keys-6694.test.ts`.
|
||||
|
||||
- **fix(cli):** the dashboard's Claude Code CLI card could report "Not detected"/"Not installed" even when Claude Code was genuinely installed and previously used ([#6701](https://github.com/diegosouzapw/OmniRoute/issues/6701)) — `getCliRuntimeStatus()` (`src/shared/services/cliRuntime.ts`) determined `installed` purely from binary resolution (known install paths + a `where`/`which` PATH search), with no fallback when that lookup fails for reasons unrelated to whether the CLI is actually installed (stale PATH inherited by a long-running/background process, the binary having moved, an install method not yet catalogued, etc.) — even though `~/.claude/settings.json` on disk proves the tool was installed and used before. Upstream 9router's equivalent route already has this exact fallback. A new `withSettingsFallback()` (`src/shared/services/cliInstallFallback.ts`) restores 9router parity: when the binary lookup's own reason is `"not_found"` (never for deliberate security rejections like unsafe/relative env overrides or symlink escapes) and the tool's settings file exists on disk, `installed` now reports `true`. Regression guard: `tests/unit/repro-6701-claude-detect-fallback.test.ts`.
|
||||
- **fix(cli):** per-agent AgentBridge DNS toggle was broken for 8 of the 9 supported agents, and a failed MITM startup step could orphan the spawned proxy child — `addDNSEntry`/`removeDNSEntry` (`src/mitm/dns/dnsConfig.ts`) always resolved the legacy Antigravity default hosts regardless of which agent's toggle was flipped, so enabling DNS for Cursor/Codex/Claude Code/etc. silently added only `daily-cloudcode-pa.googleapis.com` while the DB recorded `dns_enabled=true` for the selected agent. Both functions now accept an optional `agentId` and resolve hosts via `ALL_TARGETS`; `POST /api/tools/agent-bridge/agents/[id]/dns` passes the route's `id` through and now returns 404 for an id that doesn't match a known target instead of silently falling back. Separately, `startMitmInternal()` (`src/mitm/manager.ts`) now wraps `generateCert()` (log + rethrow), the `provisionDnsEntries()` call, and the PID-file write in try/catch so a mid-startup failure can't orphan the already-spawned MITM child process. On Windows, `addDNSEntries`/`removeDNSEntries` also batch every missing/present entry into a single elevated PowerShell invocation instead of one UAC prompt per host line. Regression guard: `tests/unit/dns-config-generic.test.ts` (agent-specific resolution + batching), `tests/unit/agent-bridge-dns-route-validation.test.ts` (404 for unknown agent id). ([#6338](https://github.com/diegosouzapw/OmniRoute/pull/6338) — thanks @hamsa0x7)
|
||||
- **fix(guardrails):** Vision Bridge's individual-model auto-reroute (route an image-bearing request straight to a vision-capable model instead of describe-then-forward) could bypass a policy-restricted API key's model allowlist/budget ([#6640](https://github.com/diegosouzapw/OmniRoute/pull/6640)) — `VisionBridgeGuardrail.preCall()` (`src/lib/guardrails/visionBridge.ts`) swaps `body.model` to the best available vision-capable model, but that swap happens in the guardrail pipeline AFTER `chat.ts` already called `enforceApiKeyPolicy()` against the ORIGINAL model, so a key scoped to a narrow `allowedModels` list could still execute against an unvetted (and possibly costlier) vision model the reroute picked. `chat.ts` now re-validates any guardrail-driven model change against the same per-key allowlist (`isModelAllowedForKey`) before honoring it, falling back to the original already-approved model when the reroute target is not allowed. The reroute path also now honors an explicit `settings.visionBridgeModel` operator override (previously ignored, unlike the combo/describe path a few lines below it, which already respects it via `getVisionBridgeConfig`). Regression guard: `tests/unit/guardrails/visionBridge.test.ts` (22 tests). (thanks @herjarsa)
|
||||
- **fix(auth):** an API key restricted via `allowedModels`/`allowedCombos` could bypass that restriction entirely over the Codex Responses-over-WebSocket bridge ([#6564](https://github.com/diegosouzapw/OmniRoute/issues/6564)) — `prepare()` in `src/app/api/internal/codex-responses-ws/route.ts` authenticated the WS bridge's API key (`authenticate()`/`authorizeWebSocketHandshake()`) and honored `allowedConnections`, but never called `enforceApiKeyPolicy()`, the same model/combo policy gate the HTTP `/v1/responses` path enforces via `handleChat()` — so a key scoped to e.g. `combo/model-1.0` could still reach a direct Codex model like `gpt-5.5` through this transport, as long as an eligible Codex OAuth connection existed. The bridge's WS auth token arrives via query params (`api_key`/`token`/`access_token`), not a normal `Authorization` header, so a new `enforceCodexWsApiKeyPolicy()` builds an equivalent `Request` carrying an explicit `Authorization: Bearer <apiKey>` header and calls `enforceApiKeyPolicy()` against the CLIENT-requested model, before any Codex-specific model remapping or credential selection. Regression guard: `tests/unit/codex-ws-policy-enforcement-6564.test.ts` (a model-restricted key is rejected 403 before reaching credential selection; a combo-restricted key is rejected 403 requesting a disallowed combo; a key that DOES allow the requested model still proceeds past policy). (thanks @Squawk7777 for the report and an independent fix via #6565)
|
||||
- **fix(security):** loopback-gate `/api/middleware/*` so a leaked JWT over a tunnel can't install or trigger a middleware hook — middleware hooks compile + run arbitrary JS via `new vm.Script` on the request hot path (`src/lib/middleware/registry.ts`), the same RCE class as the already-gated `/api/plugins/*`; `/api/middleware/` is now in `LOCAL_ONLY_API_PREFIXES` so loopback enforcement runs unconditionally before any auth check (Hard Rules #15 + #17). Regression guard: `tests/unit/route-guard-middleware-local-only.test.ts`. ([#6541](https://github.com/diegosouzapw/OmniRoute/pull/6541)) — see PR. (thanks @developerjillur)
|
||||
- **fix(startup):** AgentBridge's MITM server no longer fails to start with `ROUTER_API_KEY is required` on a normal install ([#6403](https://github.com/diegosouzapw/OmniRoute/issues/6403)) — `POST /api/tools/agent-bridge/server` resolved the spawned MITM child's router key from only an explicit `apiKey` body field (never sent by the AgentBridge UI — the schema has no such field) and the `ROUTER_API_KEY` env var (unset by default), so `startMitm()` always received `""` and the child hard-exited, even though OmniRoute already had a usable API key in its own DB. A new `resolveRouterApiKey()` now falls back to `pickApiKeyForInternalUse()` (the same DB-backed selector the combo-health-check / cloud-sync internal probes use), resolving in order: explicit key → `ROUTER_API_KEY` env → an existing DB key. Regression guard: `tests/unit/agentbridge-mitm-router-key-6403.test.ts`.
|
||||
- **fix(providers):** deploying a Cloudflare relay Worker from Dashboard → System → Proxy pool → Cloudflare relay failed immediately with `Cloudflare Worker upload failed: Content-Type must be one of: application/javascript, text/javascript, multipart/form-data`, even with a valid token/account ([#6416](https://github.com/diegosouzapw/OmniRoute/issues/6416)) — the Worker-script upload built a native `FormData` and let `fetch` derive the multipart Content-Type automatically, but in production `globalThis.fetch` is patched with `node_modules/undici`'s own fetch (`open-sse/utils/proxyFetch.ts`), whose `FormData`/`Request` classes differ from the runtime's global `FormData` (same cross-realm class mismatch already fixed once for image edits in #3273); passing a native `FormData` instance through undici's patched fetch made it serialize the body as the literal string `"[object FormData]"` with `Content-Type: text/plain;charset=UTF-8`, which Cloudflare rejects outright. `buildCloudflareWorkerUploadRequest()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now builds the multipart body as a raw `Buffer` with an explicit boundary and `Content-Type: multipart/form-data; boundary=…` header, accepted verbatim by any fetch implementation. Regression guard: `tests/unit/cloudflare-worker-upload-content-type-6416.test.ts` + updated `tests/unit/relay-deploy-5128.test.ts`.
|
||||
- **fix(security):** SSRF-guard the provider-validation probes so they can no longer be used as an open relay to cloud-metadata endpoints — `directHttpsRequest()` (web-cookie / NVIDIA / Z.AI validation, all with a caller-controllable `baseUrl`) ran with `guard:"none"` + `allowRedirect:true`; it now applies `getProviderValidationGuard()` (default `block-metadata`: LAN/localhost allowed, `169.254.169.254`/link-local IMDS rejected, opt-out via `OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS`) and `allowRedirect:false` so a provider can't 3xx-redirect the probe to metadata past the initial-URL guard. Regression guard: `tests/unit/provider-validation-ssrf-guard.test.ts`. ([#6542](https://github.com/diegosouzapw/OmniRoute/pull/6542)) — see PR. (thanks @developerjillur)
|
||||
- **fix(startup):** AgentBridge's MITM proxy served a mismatched cert for 3 of the 4 antigravity/cloudcode-pa hosts it terminates TLS for, breaking interception ([#6494](https://github.com/diegosouzapw/OmniRoute/issues/6494)) — `src/mitm/server.cjs`'s `TARGET_HOSTS` decrypts all 4 hosts locally (`daily-cloudcode-pa.googleapis.com`, `cloudcode-pa.googleapis.com`, `daily-cloudcode-pa.sandbox.googleapis.com`, `autopush-cloudcode-pa.sandbox.googleapis.com`), but `src/mitm/cert/generate.ts`'s self-signed cert only carried a SAN entry for the first host — a request to any of the other 3 got served a cert whose CN/SAN didn't match (confirmed via `curl -k https://cloudcode-pa.googleapis.com/` showing `CN=daily-cloudcode-pa.googleapis.com`). `generateCert()` now sources its host list from `ANTIGRAVITY_TARGET.hosts` (`src/mitm/targets/antigravity.ts`, the single authoritative registry already kept in lock-step with `server.cjs`/`dnsConfig.ts`/`mitmToolHosts.ts` by their own drift tests) and emits a SAN entry for all 4 hosts instead of hard-coding a second, incomplete copy. Regression guard: `tests/unit/agentbridge-antigravity-cert-hosts-6494.test.ts` (asserts the host list covers all 4 hosts and that the real generated cert's SAN includes each one).
|
||||
- **fix(resilience):** a `priority` combo never fell back when a target masked credit/quota exhaustion behind an HTTP 200 ([#6427](https://github.com/diegosouzapw/OmniRoute/issues/6427)) — `validateResponseQuality()` (`open-sse/services/combo/validateQuality.ts`) only inspected the response body's top-level `error` field when `choices` was ALSO missing/empty (the narrower #3424 empty-completion case); a masked 200 that echoed a non-empty stub `choices` alongside a structured error object, or a known exhaustion phrase (e.g. "insufficient credits", "quota exceeded") in the error envelope, slipped through as "valid" and the combo kept returning the dead target's response forever instead of failing over. The quality check now inspects the error envelope — a top-level OpenAI-shape `error` object, or a bounded, case-insensitive exhaustion-phrase match against `error.message`/`error.code`/`error.type`/top-level `message`/`detail` — unconditionally, before any shape-specific branch, and regardless of whether `choices`/`output` also look structurally present. The check never inspects `choices[].message.content`, so a legitimate completion that merely mentions "quota" or "credits" in assistant prose is not misclassified. Regression guard: `tests/unit/masked-200-exhaustion-fallback-6427.test.ts`.
|
||||
- **fix(security):** fail-closed CORS for the cookie/session-authed cloud-agent management routes — `getCloudAgentCorsHeaders()` reflected any caller's `Origin` and paired it with `Allow-Credentials: true` (a CSRF/exfil hole); it now defers to the central allowlist (`resolveAllowedOrigin`), echoes only an allowlisted origin with `Vary: Origin`, and emits `Allow-Credentials` only for an explicitly allowlisted origin — never for a `CORS_ALLOW_ALL` wildcard echo. Regression guard: `tests/unit/cloud-agent-cors-failclosed.test.ts`. ([#6543](https://github.com/diegosouzapw/OmniRoute/pull/6543)) — see PR. (thanks @developerjillur)
|
||||
- **fix(compression):** adaptive-compression ladder ranked 6 real catalog engines (`ccr`, `ionizer`, `relevance`, `llmlingua`, `llm`, `read-lifecycle`) as if they didn't exist ([#6533](https://github.com/diegosouzapw/OmniRoute/issues/6533)) — `ladder.ts`'s `AGGRESSIVENESS` and `REDUCTION_FACTOR` maps only covered the 7 engines wired into `DEFAULT_LADDER` (session-dedup/rtk/headroom/lite/caveman/aggressive/ultra); every other engine registered in `open-sse/services/compression/engines/index.ts` — including `ccr`/`llmlingua`, which the ladder doc comment already says are intentionally addable via `ladderOverride` — fell through to `aggressivenessOf()`'s `?? 0` default (same rank as `"off"`) and `expectedReductionFactor()`'s generic `?? 0.9` fallback, so `floor`-mode escalation could not rank or escalate past them once added to a custom ladder. Both maps now carry entries for all 6 missing real engines, rescaled ×10 (`off:0` … `ultra:70`) and placed by each engine's documented `stackPriority` (`ionizer` between `rtk`/`headroom`, `relevance` before `caveman`, `llmlingua`/`llm` between `aggressive` and `ultra`, etc.); `mcpAccessibility` — named in the report — is not a registered `CompressionEngine` (it's a separate MCP tool-response truncation mechanism) and was correctly left out. Regression guard: `tests/unit/ladder-engine-maps-6533.test.ts` (asserts every id from `listCompressionEngines()` ranks above `"off"` with a non-default reduction factor). (thanks @chirag127)
|
||||
- **fix(api):** tool-call arguments could render as `[object Object]` sequences instead of the real JSON through the `/anthropic` (Anthropic-shape `/messages`) routing path ([#6459](https://github.com/diegosouzapw/OmniRoute/issues/6459)) — `appendToolCallArgumentDelta()` (`open-sse/utils/toolCallArguments.ts`), the shared accumulator the streaming `openai-to-claude` response translator, `openai-responses` translator, and `responsesTransformer` all call to build up a tool call's `arguments`/`input_json_delta` buffer, treated any non-string `incoming` fragment as an empty string. Some upstreams deliver the full `tool_calls[].function.arguments` value as an already-parsed JSON object/array instead of the OpenAI-contracted JSON-encoded string; the old code silently discarded that fragment, leaving `tool_use.input` empty, and left downstream buffers open to a plain string coercion of the object (`[object Object]`) once client-side concatenation kicked in. `appendToolCallArgumentDelta()` now `JSON.stringify()`s a non-string, non-null object/array fragment into a valid JSON fragment instead of dropping it, so the assembled `partial_json` always parses back into the original structured value. Regression guard: `tests/unit/anthropic-toolcall-args-6459.test.ts`. (thanks @chirag127)
|
||||
- **fix(providers):** `fusion` combo returned the opaque `"All fusion panel models failed"` 503 even when only a minority of panel members were actually cooling down / rate-limited, and a user-supplied `fusionTuning.minPanel=1` was silently overridden ([#6454](https://github.com/diegosouzapw/OmniRoute/issues/6454)) — `handleFusionChat()` hard-clamped the quorum floor via `Math.min(Math.max(2, cfg.minPanel), panel.length)`, so an operator-configured `minPanel=1` never took effect: `collectPanel()`'s straggler-grace timer only starts once `ok >= minPanel`, and with the floor forced to 2 a single fast success plus N slow-failing stragglers never reached quorum, so the panel sat waiting instead of degrading to the survivor. Per-member failure reasons (`straggler_dropped`/`timeout`/`threw`/`status_XXX`/`empty_content`/`unparseable`) were also logged server-side but never surfaced in the 503 body, leaving operators unable to tell a rate-limit fan-fail from a broader outage. Fixed by honoring `Math.max(1, cfg.minPanel)` and threading a `failures: Array<{ model, reason }>` collector into the 503 message (`model=reason` per entry) — production fix already merged via #6521; this entry backfills the missing CHANGELOG bullet and adds an 11-member, `fusion-free`-scale regression test matching the original repro shape (a cooling minority must not sink a healthy majority; a genuinely all-failed panel still returns the documented 503). Regression guard: `tests/unit/services/fusion-min-panel-and-failure-detail.test.ts` + `tests/unit/fusion-partial-panel-failure-6454.test.ts`. (thanks @chirag127)
|
||||
- **fix(providers):** `fusion` combo strategy silently returned a panel member's raw answer instead of the configured `config.judgeModel` synthesis ([#6455](https://github.com/diegosouzapw/OmniRoute/issues/6455)) — `handleFusionChat()`'s single-survivor "degrade gracefully" path (added for #6454) returned the lone panel answer directly whenever only one panelist succeeded, regardless of whether an explicit `judgeModel` was configured; with the default `minPanel: 2` and a 2-model panel, any single flaky/rate-limited panelist forced this path on every request, so the configured judge (e.g. `auto/claude-opus`) was never invoked and the client-visible `.model` reflected whichever panelist happened to survive. The judge is now still invoked to synthesize a lone surviving answer whenever `judgeModel` is explicitly configured; the cheap direct-answer shortcut is kept only for the implicit case (no `judgeModel` set, where the "judge" is just `panel[0]`). Regression guard: `tests/unit/fusion-judge-model-6455.test.ts` + updated `tests/unit/combo-fusion-strategy.test.ts`. (thanks @chirag127)
|
||||
- **feat(combo):** sanitized diagnostic trace on an auto-combo terminal failure — instead of an opaque 503, a terminal combo failure now returns a whitelist-projected trace (candidate pool size, attempted count, excluded provider/reason codes, attempt order, and a terminal-reason code) via the new `errorResponseWithComboDiagnostics()`/`sanitizeComboDiagnostics()` in `open-sse/utils/error.ts` — provider/model ids and enumerated reason codes only, never keys/tokens/bodies, length- and count-capped. A reasoning-budget-exhausted panel now returns an actionable "increase max_tokens" message rather than a blind retry-limit 503. Regression guard: `tests/unit/combo-diagnostics-trace.test.ts`. ([#6545](https://github.com/diegosouzapw/OmniRoute/pull/6545)) — see PR. (thanks @developerjillur)
|
||||
- **fix(providers):** image/diffusion models discovered from an upstream catalog (e.g. HuggingFace's live `/v1/models`) are no longer advertised as chat models ([#6457](https://github.com/diegosouzapw/OmniRoute/issues/6457)) — the chat catalog builder defaulted synced models with no modality info to `endpoints: ["chat"]`, so `huggingface/stabilityai/stable-diffusion-xl-base-1.0` showed up in the chat `/v1/models` listing and returned `400 "not a chat model"` when called. `catalog.ts` now skips any synced model already registered as an image model for that provider (via the new `isRegisteredImageModel()`), leaving `getAllImageModels()` to list it with the correct `type: "image"`. Regression guard: `tests/unit/image-model-not-in-chat-catalog-6457.test.ts`.
|
||||
- **fix(resilience):** combo session stickiness never released a pin on a credits-exhausted/banned/expired account, permanently defeating failover for that conversation ([#6692](https://github.com/diegosouzapw/OmniRoute/issues/6692)) — `applySessionStickiness()` (`open-sse/services/combo/sessionStickiness.ts`) gated the sticky pin only on 5h/weekly usage-percentage headroom, which is orthogonal to account availability: a `credits_exhausted`/`banned`/`expired` connection, or one still inside its `rateLimitedUntil` cooldown, reports perfectly healthy headroom, so the pin was force-promoted back to the front of the target list on every subsequent turn. `clearStickyBinding()` also had zero call sites in `combo.ts`'s failure paths, so a quality-validation-rejected 200 (a masked daily-cap refusal) never released the pin either. The gate now also resolves the bound connection's terminal status/cooldown via a new injectable fetcher seam (fail-open on lookup errors, mirroring the existing saturation fetcher), and `combo.ts`'s two dispatchers (`handleComboChat`/`handleRoundRobinCombo`) release the pin immediately at both their connection-exhaustion classification point and their quality-validation-failure branch via the new `releaseStickyPinOnFailure()`. Regression guard: `tests/unit/repro-6692-sticky-terminal.test.ts` + extended `tests/unit/combo-session-stickiness.test.ts`.
|
||||
- **fix(test):** replace the bare `expect(true).toBe(true)` tautology in `playground-api-tab.test.tsx`'s SSE test and close the `check:test-masking` gap that let it slip through for a full cycle ([#6404](https://github.com/diegosouzapw/OmniRoute/issues/6404)) — a prior pass (#6548) had already swapped the literal to `expect(sendBtn).toBeDefined()`, but that stayed just as vacuous: the test's fetch mock returned an empty `/v1/models` list, so `ApiTab`'s Send button is always `disabled` (`!selectedModel`) and the SSE branch never runs — the "SSE infra is verified" comment was never true. The test now mocks a real model, drives the model `<select>` to enable Send, asserts `sendBtn.disabled === false` before clicking, and asserts the streamed SSE delta (`"Hello!"`) actually reached the response editor. Root cause on the detector side: `check-test-masking.mjs`'s tautology subcheck only compares base-vs-HEAD counts within a PR's own diff (`headExtTaut > baseExtTaut`) and no-ops locally when `GITHUB_BASE_SHA`/`GITHUB_BASE_REF` are unset ("sem base ref — pulando") — so a tautology merged once, or checked with a bare local run, was invisible forever after. Added a new always-on, PR-independent absolute-floor scan (`scanBareTautologies` + `countBareTautologies`) over every git-tracked test file for the bare `expect(true).toBe(true)` / `assert.equal(1,1)` / `assert.strictEqual(1,1)` patterns specifically (deliberately excluding `assert.ok(true)`, which has ~15 pre-existing verified-legitimate try/catch-fallback uses repo-wide and stays governed by the lenient diff-only subcheck) — verified zero pre-existing hits repo-wide once this file was fixed, so the new floor is safe to enforce unconditionally. Regression guard: `tests/unit/check-test-masking.test.ts` (new `scanBareTautologies`/`countBareTautologies` cases) + `tests/unit/ui/playground-api-tab.test.tsx`. (thanks @chirag127)
|
||||
- **fix(oauth):** Codex/ChatGPT (and every other OAuth provider) connection stays stuck showing "Auth Failed" even after a genuinely successful token refresh ([#6352](https://github.com/diegosouzapw/OmniRoute/issues/6352)) — `updateProviderCredentials()` (the shared `onPersist` callback for the manual "Refresh token" route, the reactive per-request refresh in `chat.ts`, and the Codex/Claude auth-file importers) correctly reused the stored `refresh_token`, persisted the new `access_token`, and replaced a rotated `refresh_token`, but never cleared the stale `testStatus`/`lastError*`/`errorCode` fields left over from a prior expired/invalid refresh or upstream 401/403 — only the separate background health-check sweep did that clearing. A successful refresh now resets `testStatus` to `"active"` and clears `lastError`, `lastErrorAt`, `lastErrorType`, `lastErrorSource`, and `errorCode` (an explicit `testStatus` from the caller still wins). Regression guard: `tests/unit/codex-oauth-refresh-persist-6352.test.ts`.
|
||||
- **perf(health):** short-TTL (1s) cache for the frequently-polled `GET /api/monitoring/health` payload — rebuilding it every request (DB reads + status aggregation across 8 subsystems) was wasteful under rapid polling; the cache stays near-real-time and is invalidated immediately on `DELETE` (circuit-breaker reset) so a manual reset is reflected at once. Regression guard: `tests/integration/monitoring-health-cache.test.ts`. ([#6553](https://github.com/diegosouzapw/OmniRoute/pull/6553)) — see PR. (thanks @developerjillur)
|
||||
- **fix(resilience):** `headroom` combo routing did not always select the Codex account with the most free quota ([#6379](https://github.com/diegosouzapw/OmniRoute/issues/6379)) — `orderTargetsByHeadroom` (`open-sse/services/combo/quotaStrategies.ts`) already loaded the per-connection DB snapshot (with decrypted credentials) via `expandTargetsByQuotaAwareConnections`, but discarded it before calling `getSaturation`; for Codex, `fetchCodexSaturation` forwards straight to `fetchCodexQuota(connectionId, connection)`, which needs `connection` (or a prior `registerCodexConnection()` call, which never happens before headroom ranking runs) to read `accessToken` — so it returned `null` for every candidate, saturation failed open to `0` across the board, and ranking fell back to the original combo order regardless of actual free quota. `getSaturation()` and the headroom `SaturationFetcher` seam now accept and thread the loaded connection snapshot through to `fetchCodexQuota`. Kilo's dup flag vs #5903 was a false positive — that issue is about session-sticky reset-aware/least-used selection, not headroom's Codex saturation lookup. Regression guard: `tests/unit/headroom-codex-quota-snapshot-6379.test.ts`. (thanks @eidoog)
|
||||
- **fix(providers):** custom models a provider actually has are no longer dropped from the Free Provider Rankings when both **"Configured only"** and **"Available only"** filters are applied ([#6368](https://github.com/diegosouzapw/OmniRoute/issues/6368)), follow-up to #6150 — `freeProviderRankings.ts::getProviderModels()` only ever walked the static `open-sse/config/providerRegistry.ts` catalog, so a user-added custom model (e.g. a Puter `claude-fable-5` model saved as "Claude Fable 5") never entered the candidate model list the ranking scores against, and could never survive the #6150 configured/available filters even when actually configured and available. It now additively merges the provider's custom models (`db/models.ts::getCustomModels`) into that candidate list via a new pure, de-duping `mergeProviderModels()` helper, before scoring/filtering runs — catalog free/paid filtering elsewhere is untouched. Regression guard: `tests/unit/free-provider-rankings-custom-models-6368.test.ts`. (thanks @shabeer)
|
||||
- **fix(playground):** accept a valid dashboard session for `GET/POST /api/playground/presets` under `REQUIRE_API_KEY=true` — the Playground page calls this route with a cookie/session and no API key, which previously 401'd the authenticated dashboard; `checkAuth()` now accepts a management/dashboard session (`requireManagementAuth`) as an alternative to an API key, while a presented API key must still be valid and the anonymous-allowed default is preserved. Regression guard: `tests/integration/presets-dashboard-auth.test.ts`. ([#6554](https://github.com/diegosouzapw/OmniRoute/pull/6554)) — see PR. (thanks @developerjillur)
|
||||
- **fix(providers):** `cloudflare-ai` no longer silently drops image/non-text content parts ([#6390](https://github.com/diegosouzapw/OmniRoute/issues/6390)) — `transformRequest()`'s `flattenContent()` (added for #2539 to satisfy the Workers AI `/ai/v1/chat/completions` plain-string `content` requirement) mapped any non-text OpenAI content part (e.g. `image_url`) to `""` and joined the rest, so a request carrying an image quietly went out as text-only with the attachment gone and no error surfaced. It now throws a clear error on the first non-text part instead of dropping it silently, which the existing top-level `chatCore.ts` catch already routes through `buildErrorBody()`/`sanitizeErrorMessage()` (same pattern as `buildUrl()`'s missing-Account-ID error). Regression guard: `tests/unit/cloudflare-ai-image-parts-6390.test.ts`.
|
||||
- **fix(providers):** stop Antigravity connections from falsely reporting **all-accounts quota-exhausted** ([#6295](https://github.com/diegosouzapw/OmniRoute/issues/6295)) — `genericQuotaFetcher.ts::percentUsedForQuota()` ignored the `fractionReported` flag and defaulted an unreported model's `remainingPercentage` to 0, which computed as 100% used; since `convertUsageToQuotaInfo()` takes the worst-case window across a connection, a single model with no reported fraction dragged the whole account into `limitReached` and `quotaPreflight` skipped it. `percentUsedForQuota()` now returns `null` (unknown, window ignored) whenever `fractionReported === false`, before falling back to `remainingPercentage`. Regression guard: `tests/unit/generic-quota-fetcher.test.ts`.
|
||||
- **fix(fusion):** the fusion judge no longer replays a panel member's answer via an idempotency-key collision — fusion's panel + judge sub-requests re-enter `chatCore` sharing the client's headers, so they derived the same `Idempotency-Key`/`x-request-id` and a panel answer saved under the key was replayed by the judge's check ~1ms later (inside the 5s window), returning a panel member's answer instead of the judge synthesis (observed live on `nexa/conversation-fusion`). `composeIdempotencyKey()` now namespaces the key by target provider/model + a digest of the request messages, so sub-requests can't collide while a genuine client retry (same key/model/body) still replays. Regression guard: `tests/unit/idempotency-fusion-collision.test.ts`. ([#6558](https://github.com/diegosouzapw/OmniRoute/pull/6558)) — see PR. (thanks @developerjillur)
|
||||
- **fix(providers):** grok-cli (Grok Build) now strips `reasoning_effort`/`reasoning` before forwarding the request ([#6288](https://github.com/diegosouzapw/OmniRoute/issues/6288)) — Claude Code sends `reasoning_effort` on every request (routing the Opus slot), which Grok Build's upstream chat-proxy endpoint rejects with a 400; `transformRequest()`'s existing `UNSUPPORTED` sampling-param strip list (#5273) never covered it. Regression guard: `tests/unit/grok-cli-reasoning-strip-6288.test.ts`.
|
||||
- **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 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`.
|
||||
- **fix(api):** `POST /api/keys` no longer hangs 20–90+ seconds on a fresh install, even with valid auth ([#6570](https://github.com/diegosouzapw/OmniRoute/issues/6570)) — `cloudEnabled` defaults to `true` in `src/lib/db/settings.ts::getSettings()` on any install with no persisted settings row (i.e. every fresh install), so the create-key handler's unconditional `await syncKeysToCloudIfEnabled()` always attempted a real outbound `fetch()` to `CLOUD_URL` via `syncToCloud()`; when that endpoint is unset/unreachable/slow, the HTTP response blocked until the request settled or timed out — unlike sibling routes (`POST /api/keys/:id/regenerate`, `GET /api/combos`), which never touch this side effect at all. `src/app/api/keys/route.ts` now dispatches `syncKeysToCloudIfEnabled()` fire-and-forget (`void`) instead of awaiting it; its internal try/catch already logs failures, so cloud sync still runs, it just no longer blocks the response. Regression guard: `tests/unit/api-keys-create-no-hang-6570.test.ts` (asserts the route resolves in well under 2s even when the Cloud-sync `fetch()` is stubbed to never settle) + updated `tests/integration/api-keys.test.ts` (the pre-existing "triggers cloud sync"/"still succeeds when cloud sync fails" tests, which previously hung indefinitely on this exact path, now await the fire-and-forget sync tick before asserting).
|
||||
- **fix(api):** editing any existing OpenAI Codex provider connection in the dashboard returned "Invalid request" and the edit could never be saved ([#6562](https://github.com/diegosouzapw/OmniRoute/issues/6562)) — `createProviderConnection()` (`src/lib/db/providers.ts`) auto-increments a new connection's `priority` to `MAX(priority)+1` per provider with no upper bound, and OAuth-imported connections (Codex `codex-auth/import` / `import-bulk`, up to 50 accounts per call, callable repeatedly — the standard Codex bulk-account-rotation workflow) never pass through `createProviderSchema`'s Zod validation at all, so nothing ever capped that value; `EditConnectionModal`'s `handleSubmit` always resends the connection's current `priority` unchanged on every save, and `updateProviderConnectionSchema` capped `priority`/`globalPriority` at `max(100)` — a UI-only ceiling the create path never enforced — so the first edit of any connection whose priority had already grown past 100 (routine once a Codex account count exceeds 100) failed validation regardless of which field the user actually changed. Raised the ceiling to `max(100_000)` on both fields — still bounded (a genuinely out-of-range value is still rejected), just wide enough to accept priorities the app itself already produces. Regression guard: `tests/unit/codex-connection-edit-6562.test.ts` (a Codex OAuth connection whose priority already exceeds the old 100 cap now validates + persists on edit; a control payload with a still-genuinely-invalid priority is still rejected with "Invalid request").
|
||||
- **mimocode**: rotate accounts on MiMoCode's rate-limit-style 400s (body-classified) instead of failing on the first account; malformed 400s still fail fast with the real upstream error (#6648 — thanks @pizzav-xyz)
|
||||
- **fix(cli):** compression CLI REST fallback now reads/writes the canonical `defaultMode` field (surfaced as `strategy`) instead of a nonexistent `engine` key, and table output renders nested objects as JSON instead of `[object Object]` (#6571 — thanks @charleszolot)
|
||||
- **fix(providers):** web-cookie providers without a `providerRegistry.ts` entry (`lmarena`, `gemini-business`, `poe-web`, `venice-web`, `v0-vercel-web`) now report `unsupported: true` instead of silently "OK" ([#6309](https://github.com/diegosouzapw/OmniRoute/pull/6309)) — `validateWebCookieProvider()` (`src/lib/providers/validation.ts`) previously required a registry entry and returned "Provider not found in registry" for these; a fallback to `WEB_COOKIE_PROVIDERS[provider].website` was proposed, but live verification showed the `${website}/models` probe does not reliably signal session validity for these providers (redirects/SPA 200s regardless of cookie validity — e.g. lmarena's real API is `arena.ai`, not `lmarena.ai`; Poe's real endpoint is a GraphQL POST, not a REST `/models`), so it would report an expired or garbage cookie as valid. Until each provider has a verified, side-effect-free auth probe against its real API host, the fallback now returns `unsupported` (no network call) instead of a false positive. Regression guard: `tests/unit/web-cookie-validation-fallback.test.ts`. (thanks @oyi77)
|
||||
- **fix(api):** `POST /api/middleware/hooks` and `PUT /api/middleware/hooks/[name]` no longer leak raw internal error messages in their 500 responses (#6645 — thanks @chirag127) — both catch blocks returned `error?.message` directly (Hard Rule #12), which could surface internal SQLite path fragments on a DB failure; both now route through `sanitizeErrorMessage()` from `open-sse/utils/error.ts`. Regression guard: `tests/unit/middleware-hooks-error-sanitization.test.ts`.
|
||||
- **fix(docker):** compile better-sqlite3 for the server Docker image (Dokploy/self-hosted builds) via a direct `node-gyp rebuild` inside `node_modules/better-sqlite3`, instead of `npm rebuild better-sqlite3` ([#6700](https://github.com/diegosouzapw/OmniRoute/pull/6700)) — the `builder` stage installs dependencies with `npm ci --ignore-scripts` (deliberate: closes the supply-chain surface where a transitive dep's install script runs arbitrary code) and re-enables the native build for the one package that needs it; `npm rebuild <pkg>` re-runs that indirectly through the package's own install script, which under npm 11 depends on npm's script-allowlist machinery correctly re-enabling it — some self-hosted build environments (e.g. Dokploy) hit a broken/mismatched native binding through that indirection. Invoking `node-gyp rebuild` directly bypasses npm's script-running layer entirely and is deterministic regardless of npm version. Regression guard: `tests/unit/dockerfile-better-sqlite3-node-gyp-6700.test.ts`. (thanks @nowhats-br)
|
||||
- **fix(providers):** the Cloudflare relay Worker deploy fix in #6416/#6618 still failed uploads in practice — it changed the multipart Content-Type but kept the emitted worker source as an ES module (`export default { fetch(...) }`) with `main_module` metadata; Cloudflare's Workers upload API parses a plain `application/javascript` script part as Service Worker syntax regardless of the `main_module` metadata field, and `main_module` requires the script to actually be an ES module (top-level `export`), so the mismatch still rejected the upload ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)). `buildCloudflareWorkerScript()` (`src/lib/proxyRelay/cloudflareWorkerScript.ts`) now emits Service Worker syntax (`addEventListener("fetch", ...)`, no top-level `export`) and the upload metadata uses `body_part` instead of `main_module`. Regression guard: `tests/unit/relay-deploy-5128.test.ts` (asserts the emitted script has no `export default`, registers a `fetch` listener, and the upload metadata carries `body_part`/omits `main_module`; also proves the inlined `isPrivateHostname()` SSRF guard still rejects bracketed IPv6 loopback/ULA hosts like `[::1]`/`[fd00::1]` after the script-body rewrite). (thanks @SeaXen)
|
||||
- **fix(providers):** ChatGPT Web (`chatgpt-web`) responses rendered raw ChatGPT UI citation markup — private-use marker tokens (e.g. `citeturn0search0`) and `url…` inline-link markers — instead of real Markdown links, since these only ever get resolved client-side by chatgpt.com's own JS using `message.metadata.content_references` ([#6635](https://github.com/diegosouzapw/OmniRoute/pull/6635)) — `cleanChatGptText()` now resolves `content_references` (grouped webpages, footnote sources, inline `webpage`/`url` mentions) into `[label](url)` Markdown links for both the streaming and non-streaming response builders, and for the GPT-5.5 Pro `stream_handoff` polled-answer path, falling back to stripping any marker that has no resolvable source instead of leaking the raw private-use bytes. The citation parsing/rendering logic was extracted into a new pure sibling module (`open-sse/executors/chatgpt-web/citations.ts`) to keep the executor under the frozen file-size cap. Regression guard: `tests/unit/chatgpt-web-citations.test.ts` (non-streaming citation resolution, streaming marker buffering across split SSE chunks, and the Pro-handoff polled-answer path). (thanks @Thinkscape)
|
||||
- **fix(dashboard):** the live-dashboard WebSocket descriptor handshake (`GET /api/v1/ws?handshake=1`) and the lightweight `GET /api/health/ping` liveness probe both 401'd for unauthenticated callers, even though both are metadata-only reads intended to be public ([#6335](https://github.com/diegosouzapw/OmniRoute/pull/6335)) — `clientApiPolicy` required a bearer/dashboard-session before the WS route handler could even return its own `wsAuth`/protocol descriptor, and `/api/health/ping` was never added to `PUBLIC_READONLY_API_ROUTE_PREFIXES` despite its own docstring documenting it as "No auth required." `clientApiPolicy.evaluate()` now allows an anonymous `{kind:"anonymous", id:"ws-handshake"}` subject for GET/HEAD/OPTIONS on `/api/v1/ws?handshake=1` (the route handler still performs its own real wsAuth/dashboard/API-key decision before opening the socket), and `/api/health/ping` is now in `PUBLIC_READONLY_API_ROUTE_PREFIXES`. Regression guard: `tests/unit/authz/client-api-policy.test.ts` (WS handshake allowed, including relative request URLs), `tests/unit/public-api-routes.test.ts`, and `tests/unit/authz/classify.test.ts` (`/api/health/ping` classified `PUBLIC`). (thanks @JxnLexn)
|
||||
- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142)
|
||||
- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272)
|
||||
- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276)
|
||||
- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun
|
||||
- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2).
|
||||
- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev).
|
||||
- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev).
|
||||
- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343)
|
||||
- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377)
|
||||
- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799)
|
||||
- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409)
|
||||
- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491)
|
||||
- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524)
|
||||
- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky).
|
||||
- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)).
|
||||
- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev).
|
||||
- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623)
|
||||
- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628)
|
||||
- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802)
|
||||
- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari).
|
||||
- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634)
|
||||
- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637)
|
||||
- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub).
|
||||
- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun).
|
||||
- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698).
|
||||
- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis)
|
||||
- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin)
|
||||
- **fix(codex):** detect "model at capacity"/overloaded errors embedded in a 200-OK SSE stream and surface them as a real error so account fallback rotates, instead of passing them through as a successful response. (thanks @ryanngit)
|
||||
- **fix(volcengine):** clamp `max_tokens` to the VolcEngine Ark endpoint cap for the Kimi model so oversized values no longer 400. (thanks @whale9820)
|
||||
- **fix(antigravity):** surface aborted/malformed Gemini tool calls (e.g. `MALFORMED_FUNCTION_CALL`) as an explicit non-`end_turn` finish reason instead of a silent clean completion. (thanks @anhdiepmmk)
|
||||
- **fix(routing):** the reasoning-token headroom buffer clamps to the model's explicit output cap instead of inflating past it, and `getExplicitModelOutputCap` falls through to the registry/spec cap when a synced capability row exists without a numeric `limit_output` ([#6714](https://github.com/diegosouzapw/OmniRoute/pull/6714)) — thanks @xz-dev
|
||||
- **fix(api):** `omniroute health` (and `health components`/`health watch`) returned `Error: HTTP 404` ([#6677](https://github.com/diegosouzapw/OmniRoute/issues/6677)) — `bin/cli/commands/health.mjs` called `apiFetch("/api/health", ...)`, a route that was moved to `GET /api/monitoring/health` (`src/app/api/monitoring/health/route.ts`) without updating the CLI; `src/app/api/health/` on disk only has `degradation/route.ts` and `ping/route.ts`, no top-level handler. `runHealthCommand()`/`runHealthComponentsCommand()` now call `/api/monitoring/health` and read its actual payload shape (`activeConnections`, `circuitBreakers: {open, halfOpen, closed}`, `memoryUsage`) instead of the old, nonexistent `requests`/`breakers`/`cache`/`memory` fields. Regression guard: `tests/unit/cli-health-monitoring-route.test.ts`.
|
||||
- **fix(startup):** webpack build broke on case-insensitive filesystems (macOS APFS default, Windows) with a casing-collision warning plus "not exported" errors in `StudioConfigPane.tsx`/`ChatTab.tsx` (#6584) — `src/app/(dashboard)/dashboard/playground/components/ReasoningControls.tsx` (the component) and `reasoningControls.ts` (the utils module) shared the same lower-cased stem in the same directory, and two importers used the extensionless form `from "./reasoningControls"`, the exact resolution path that becomes ambiguous once casing is folded. Renamed the utils module to `reasoningControlUtils.ts` (no collision) and updated the 3 import sites. Regression guard: `tests/unit/case-collision-6584.test.ts` (scans `src/`/`open-sse/` for any same-directory, case-only filename collision). (#6584)
|
||||
- **fix(build):** Turbopack production build emitted an "Overly broad patterns can lead to build performance issues" warning per entry point importing `src/lib/agentSkills/generator.ts` (603 warnings reported on v3.8.46, up from 379 on v3.8.45) ([#6582](https://github.com/diegosouzapw/OmniRoute/issues/6582)) — `generator.ts`'s `outputBase` is built as `path.isAbsolute(outputDir) ? outputDir : path.join(process.cwd(), outputDir)`, where `outputDir` is a runtime function parameter, not a compile-time literal, so Turbopack's build-time file-tracing analyzer can't statically narrow the several dynamic `readdirSync`/`rmSync`/`readFileSync`/`writeFileSync` call sites a few lines below and falls back to a project-wide glob; #6366's commit message claimed to "anchor the base path with a literal" but the shipped code never did. Since this fs access is legitimate and bounded (`skills/<id>/SKILL.md`, ~48 known IDs), `next.config.mjs`'s `turbopack.ignoreIssue` (Next.js 16.2+) now suppresses this specific, known-benign diagnostic, mirroring the existing `webpack.ignoreWarnings`/`isNextIntlExtractorDynamicImportWarning` precedent already in the same file for the webpack path. Regression guard: `tests/unit/next-config.test.ts` (asserts the `turbopack.ignoreIssue` rule shape targeting `src/lib/agentSkills/**`).
|
||||
- **fix(providers):** Codex Desktop requests to `gpt-5.3-codex-spark` failed with `[400]: Tool 'image_generation' is not supported with gpt-5.3-codex-spark`, even on paid-plan accounts ([#6651](https://github.com/diegosouzapw/OmniRoute/issues/6651)) — `CodexExecutor.transformRequest` (`open-sse/executors/codex.ts`) only dropped the Codex Desktop-injected `image_generation` hosted tool when `isCodexFreePlan()` matched the account's plan, with no awareness that Spark-scope Codex models reject `image_generation` upstream regardless of plan. `dropImageGeneration` now also drops it when `getCodexModelScope(model) === "spark"` (the existing Spark classifier from `open-sse/config/codexQuotaScopes.ts`), independent of account plan. Regression guard: `tests/unit/codex-spark-image-generation.test.ts` (thanks @alltomatos for independently catching and fixing it via #6819).
|
||||
- **fix(providers):** the provider quota card's weekly/session bars re-sorted by remaining percentage instead of staying in a fixed, deterministic order ([#6687](https://github.com/diegosouzapw/OmniRoute/issues/6687)) — `QuotaCardExpanded.tsx`'s `sortQuotasByRemaining()` (added in #5977) was applied unconditionally via `useMemo(() => sortQuotasByRemaining(quotas), [quotas])`, undoing the deterministic `CODEX_QUOTA_ORDER`/`GLM_QUOTA_ORDER` window order `quotaParsing.ts`'s `sortCodexOrder()`/`sortGlmOrder()` (added in #6336) already established for Codex and the GLM family — since #6336 never touched `QuotaCardExpanded.tsx`, the two orderings never composed, so e.g. a Codex `session` window with less headroom than `weekly` rendered after it instead of staying first. A new `hasFixedQuotaOrder()` (`quotaParsing.ts`) and `resolveQuotaDisplayOrder()` (`QuotaCardExpanded.tsx`) now skip the remaining-% re-sort for providers with a fixed window order, threading `providerId` from `QuotaCard.tsx` through to the display layer; every other provider still gets the remaining-% sort. Regression guard: `tests/unit/quota-card-expanded-fixed-order-6687.test.ts`.
|
||||
- **fix(i18n):** pt-BR was missing 194 UI keys present in `en.json` — a real, silent data-sync gap, not covered by any duplicate/mislabeled #6694 (that issue's 9 `providers.*` keys are disjoint, present-but-untranslated sentinels caused by a separate `providerText()` fallback bug) ([#6695](https://github.com/diegosouzapw/OmniRoute/issues/6695)) — `scripts/i18n/sync-ui-keys.mjs` (which mirrors newly-added `en.json` keys into every locale) wasn't re-run after recent `en.json` additions, and the CI `i18n:check-ui-coverage` gate only fails a locale below an 80% threshold, so pt-BR stayed green at 93.8% coverage despite the gap. Backfilled all 194 missing keys into `src/i18n/messages/pt-BR.json` (translated to Brazilian Portuguese, no leftover `__MISSING__` markers) via `npm run i18n:sync-ui -- --locale=pt-BR` + manual translation. Regression guard: `tests/unit/i18n-pt-br.test.ts` (new case asserting full `en.json`→`pt-BR.json` key parity, so a future drift fails a fast unit test instead of silently degrading the coverage percentage).
|
||||
- **fix(startup):** `omniroute --mcp` crashed at Node ESM link time with `ERR_MODULE_NOT_FOUND` for `ioredis` on installs where the published MCP bundle didn't happen to have `ioredis` rescued from a parent `node_modules` ([#6559](https://github.com/diegosouzapw/OmniRoute/issues/6559)) — `src/shared/utils/rateLimiter.ts` had a top-level static `import Redis from "ioredis"`; that module is only ever reached via a lazy `await import(...)` several call-sites deep in the MCP tool chain, but esbuild's `--packages=external` bundling of the MCP server (`scripts/build/prepublish.ts` Step 8.5) still hoisted rateLimiter.ts's own static import into a real top-level ESM import in the compiled `dist/open-sse/mcp-server/server.js`, forcing Node to resolve `ioredis` at module-link time — before any `--mcp` startup code runs — and `ioredis` is not guaranteed to ship in the MCP-only bundle's `node_modules`. `getRedisClient()` now lazily imports `ioredis` on first use (matching the established soft-dependency pattern in `src/lib/quota/redisQuotaStore.ts`) while still throwing synchronously when Redis isn't configured. Regression guard: `tests/unit/build/mcp-bundle-no-eager-ioredis.test.ts` (bundles the real MCP server entrypoint with the exact publish-time esbuild flags and asserts no top-level static `ioredis` import remains, while the pre-existing lazy `await import("ioredis")` in `redisQuotaStore.ts` stays intact).
|
||||
- **fix(providers):** Kiro sent the adaptive-thinking `additionalModelRequestFields` envelope for `claude-sonnet-4.5`/`claude-haiku-4.5`, which Kiro/CodeWhisperer rejects upstream with a raw `[400]: additionalModelRequestFields is not supported for this model` ([#6576](https://github.com/diegosouzapw/OmniRoute/issues/6576)) — `buildKiroPayload()` (`open-sse/translator/request/openai-to-kiro.ts`) gated the field on the generic Anthropic-API `supportsReasoning()` capability flag, which is `true` for both models on Anthropic's direct API but does not reflect what Kiro's CodeWhisperer backend actually accepts; only `claude-sonnet-5` is confirmed adaptive-thinking-capable there. A new Kiro-specific allowlist (`supportsKiroAdaptiveThinking()` in `open-sse/translator/request/openai-to-kiro/adaptiveThinking.ts`) now gates the envelope instead. Regression guard: `tests/unit/repro-6576-kiro-thinking-unsupported-model.test.ts`.
|
||||
- **fix(translator):** Cursor's local **Subagent** tool call is no longer rejected with `cloud_base_branch may only be specified when environment equals cloud` — the Responses→Chat tool-arg cleanup (`stripEmptyOptionalToolArgs`) was scoped to Claude Code's `Read` tool only, so Cursor's `Subagent` tool passed through with the cloud-only `cloud_base_branch: ""` (Cursor treats an empty string as "specified" and rejects the call before starting the local subagent). The cleanup now covers an allowlist of `Read` + `Subagent`; arbitrary tools are still left untouched (empty strings/arrays can be valid payloads for them). Regression guard: `tests/unit/openai-responses-subagent-strip-2446.test.ts`. (thanks @like3213934360-lab)
|
||||
- **fix(translator):** GLM 5.2 (and other OpenAI-compatible upstreams that stream a tool call's `id` and `function.name` in **separate** SSE chunks) no longer produce an empty tool name / `No such tool available:` error through the Claude `/messages` path — the `openai-to-claude` streaming translator emitted `content_block_start` immediately on the id-only chunk with an empty `name`, and the Claude SSE protocol cannot patch a block after it is emitted, so the later name-only chunk was silently dropped. It now **defers** `content_block_start` until the tool name arrives (falling back to starting the block when arguments arrive first), so the emitted `tool_use` always carries the real name. Regression guard: `tests/unit/openai-to-claude-glm-split-tool-name-2077.test.ts`. (thanks @itiwant)
|
||||
- **fix(resilience):** OmniRoute didn't respect an exhausted Ollama Cloud (or any other apikey-category provider) quota — it retried the account seconds later instead of waiting out the real reset window ([#6638](https://github.com/diegosouzapw/OmniRoute/issues/6638)) — `shouldPreserveQuotaSignalsFor429()`/`checkFallbackError()` (`open-sse/services/accountFallback.ts`) only applied body-text quota classification (daily/monthly/weekly quota-exhausted detection) to OAuth-category providers; apikey-category 429s (Ollama Cloud, OpenAI, etc.) always fell through to the generic short rate-limit cooldown regardless of what the error body said, and `parseRetryFromErrorText()` also had no support for day-granularity reset hints ("Your quota will reset in 3 days.") — only Xh/Ym/Zs combos. An explicit quota-exhausted signal in the body (`looksLikeQuotaExhausted()`) now overrides the apikey-category default via the new `shouldPreserveQuotaSignals()` (`open-sse/services/quotaResetParsing.ts`), and `parseDayGranularityResetMs()` parses whole-day reset countdowns so the real multi-day window is honored instead of a few seconds of backoff. Regression guard: `tests/unit/issue-6638-ollama-quota.test.ts` + 2 aligned `tests/unit/account-fallback-service.test.ts` cases that previously asserted the buggy rate_limit_exceeded/undefined-dailyQuotaExhausted behavior for apikey-provider quota text.
|
||||
- **fix(resilience):** a combo step "pinned" to one fingerprint account (mimocode/mcode/opencode multi-account providers) never actually resolved to that account, so it couldn't fail over when the pinned account was depleted ([#6696](https://github.com/diegosouzapw/OmniRoute/issues/6696), relates #6612) — the combo builder UI encodes an account pin as a composite connectionId (`${rowId}|fp|${fingerprint}`, `src/lib/combos/builderOptions.ts`), but `expandTargetsByFingerprints()` (`open-sse/services/combo/fingerprintExpansion.ts`) looked that composite string up directly in `connectionById` (keyed by real DB row ids), got `undefined`, and passed the target through unchanged, still carrying the bogus composite id — so downstream credential resolution could never match it either. `expandTargetsByFingerprints()` now splits the `|fp|` composite id back into the real connection row id + the pinned fingerprint (new `splitFingerprintPin()` helper) before any lookup, resolving the target to the real connectionId (with the pinned fingerprint carried on the new `pinnedFingerprint` field) instead of the inert composite string. Regression guard: `tests/unit/combo-fingerprint-pin-6696.test.ts`.
|
||||
- **fix(api):** Responses passthrough emitted event-only SSE frames (no `data:` line) for every dropped commentary event, breaking the OpenAI Python SDK's `sse.json()` parser ([#6561](https://github.com/diegosouzapw/OmniRoute/issues/6561)), follow-up to #6199/#6232 — the commentary-drop `continue;` branches in `open-sse/utils/stream.ts` skipped the `data:` line for a dropped commentary event but never cleared the already-buffered `event:` line for that same frame, so the next blank line flushed the stale `event:` line alone. Both drop sites now call `clearPendingPassthroughEvent()` before `continue`, discarding the buffered prefix along with the dropped payload; the commentary-drop decision itself was extracted into a new `open-sse/utils/responsesCommentaryDrop.ts` so the fix does not grow the frozen `stream.ts`. Regression guard: `tests/unit/responses-commentary-event-frame-6561.test.ts` (realistic `event:\ndata:\n\n` frames — the existing #6199 test only used bare `data:` lines and never exercised this path).
|
||||
- **fix(compression):** `/api/compression/preview`'s top-level `originalTokens`/`compressedTokens` diverged from `engineBreakdown[0]`'s counts for the same single-engine run (tiktoken outer counts vs the `JSON.stringify(...).length/4` estimate per engine), worst on small inputs. A new `reconcileSingleEngineTokens()` overwrites the single-engine breakdown entry with the outer, more accurate figures; multi-step pipeline breakdowns are left untouched ([#6488](https://github.com/diegosouzapw/OmniRoute/issues/6488)). Regression guard: `tests/unit/compression/preview-outer-engine-token-reconcile-6488.test.ts`.
|
||||
- **fix(resilience):** account selection could pick an account already out of quota upstream on every credentialed route except `chat`/`codex` ([#6686](https://github.com/diegosouzapw/OmniRoute/issues/6686)) — `getProviderCredentials()` (`src/sse/services/auth.ts`) only skips a connection when a _local cache_ already flags it exhausted (`isQuotaExhaustedForRequest`/`src/domain/quotaCache.ts`); it never itself calls the registered upstream `QuotaFetcher`. Only `getProviderCredentialsWithQuotaPreflight()` performs that live upstream check, and it was wired into exactly 2 call sites (`src/sse/handlers/chat.ts`, `src/app/api/internal/codex-responses-ws/route.ts`) — every other credentialed route (`rerank`, `images/generations`, `images/edits`, `audio/transcriptions|speech|translations`, `videos/generations`, `music/generations`, `ocr`, `providers/[provider]/embeddings`, `providers/[provider]/images/generations`, `web/fetch`, `moderations`, `search`) called the plain, cache-only selector, so an account whose cache entry was never populated (e.g. its first request landed on one of these routes) could be selected even at 0% quota remaining. Those 14 call sites now go through `getProviderCredentialsWithQuotaPreflight()` instead, matching chat/codex coverage. Regression guard: `tests/unit/issue-6686-quota-preflight-coverage.test.ts` (static check that none of the routes call the plain selector anymore + a behavioral check that the preflight-aware selector blocks a 100%-used account).
|
||||
- **fix(api):** `reasoning_content` (extended-thinking text) was silently dropped from `/v1/chat/completions` SSE on the `claude-web` and `v0-vercel-web` executors ([#6662](https://github.com/diegosouzapw/OmniRoute/issues/6662)) — every chunk builder in both adapters hardcoded `delta: { content: ... }` with no reasoning path, unlike the established pattern already used by `default.ts`/`deepseek-web.ts`/`bedrock.ts` and the real-Anthropic-API `claude-to-openai.ts` translator (`thinking_delta` → `reasoning_content`). `v0-vercel-web.ts` now forwards an upstream `delta.reasoning_content` field (streaming and non-streaming) the same way `deepseek-web.ts` does. `claude-web.ts`'s `buildClaudeStreamingResponse` now maps a `content_block_start`(`type: "thinking"`)/`content_block_delta`(`delta.thinking`) pair onto `delta.reasoning_content`, and `claude-web/payload.ts`'s `transformToClaude()` no longer hardcodes `thinking_mode: "off"` — a new `wantsExtendedThinking()` derives it from the request's `reasoning_effort`/`reasoning.effort`/`thinking.type` signal, so extended thinking can actually be requested. Regression guard: `tests/unit/issue-6662-repro.test.ts` (RED→GREEN for both adapters).
|
||||
- **fix(api):** the compression config PUT schema now accepts `enableRenderers` for the RTK engine instead of rejecting the documented option (#6703, #6757 — thanks @alltomatos, with an independent duplicate fix from @chirag127 via #6756).
|
||||
- **fix(api):** raised the provider `apiKey` length cap for cookie-based web providers, whose session-cookie credentials legitimately exceed the previous limit (#6715, #6759 — thanks @alltomatos).
|
||||
- fix(ci): publish electron-updater `latest*.yml` manifests in electron release assets so auto-update can find them ([#6766](https://github.com/diegosouzapw/OmniRoute/issues/6766))
|
||||
- **fix(i18n): translate hardcoded Portuguese dashboard strings to English (#6761, #6768)** (#6769 — thanks @chirag127).
|
||||
- fix(providers): strip redundant node prefix when resolving custom OpenAI/Anthropic-compatible connections by raw connection id, preventing double-namespaced model ids from 400ing upstream (#6772)
|
||||
- fix(providers): scope nvidia NIM 404s to the single failing model instead of cooling down the whole connection (#6773)
|
||||
- **fix(codex):** bump the default Codex CLI client identity from `0.142.0` to `0.144.0` for compatibility with newer Codex-backed models ([#6780](https://github.com/diegosouzapw/OmniRoute/pull/6780)) — thanks @quanturbo
|
||||
- **fix(ci):** the blocking "Impacted unit tests (TIA)" step false-redded any PR whose impact graph reached a dashboard component — it ran every selected test under `--import tsx/esm`, but `tests/unit/dashboard/**` requires the `--import tsx` CJS transform (ESM-only deep imports like `@lobehub/icons/es/*`), exactly as the canonical `test:unit:ci:shard` already does per segment. The impacted selection is now split by segment with matching loaders (closes #6787).
|
||||
- **fix(translator):** read PDF/video `file_data` attachments on the OpenAI→Gemini/Antigravity and OpenAI→Claude paths so multimodal documents (not just images) reach the upstream — PDFs map to `document`/`inlineData` and videos keep their `video/mp4` mime instead of being dropped (#6790 — thanks @Witroch4, with an independent report/fix from @samimozcan via #6762/#6753).
|
||||
- **fix(providers): ensure DeepSeek Web SSE emits [DONE] after FINISHED** (#6791 — thanks @Pitchfork-and-Torch).
|
||||
- **fix(api):** the compression config `PUT` schema (`stackedPipelineStepSchema`) now accepts every `ENGINE_CATALOG` id — the structural engines `session-dedup`/`ccr`/`headroom`/`relevance`/`llmlingua`/`omniglyph` and the `aggressive` `ultra` intensity — so a `GET`→`PUT` round-trip of a stacked pipeline no longer 400s on a valid engine the discriminated union had omitted (#6747 — thanks @Pitchfork-and-Torch).
|
||||
- **fix(cursor):** send the Agent CLI build id as `x-cursor-client-version` so Cursor upstream accepts requests from the current CLI build instead of a stale hardcoded version (#6795 — thanks @andrewmunsell).
|
||||
- fix(cli): waitForServer() no longer reports ready from a raw TCP accept alone — requires a fast HTTP rejection or a real health response, so the "OmniRoute is running!" banner no longer fires 30-60s before the server can actually answer requests (#6800)
|
||||
- fix(sse): de-flake timing-sensitive combo cooldown/breaker tests + add explicit MCP audit shutdown timeout (#6803)
|
||||
- **fix(codex): strip include from compact responses requests** (#6805 — thanks @yinaoxiong).
|
||||
- fix(dashboard): surface Claude extraUsage credits in quota card when quotas is empty (#6806)
|
||||
- **Request count by provider & date**: Dashboard → Analytics now shows a dedicated table of request counts grouped by provider and calendar date (plus token totals), for providers that bill per-request rather than per-token — sortable columns and a single-date filter. New `getProviderDailyUsageRows()` query (`src/lib/db/usageAnalytics.ts`) and its own `GET /api/usage/requests-by-provider-date` route (kept separate from the frozen `/api/usage/analytics` route). Regression guard: `tests/unit/db-provider-daily-usage-4009.test.ts`. (#4009 — thanks @tjengbudi)
|
||||
- **fix(resilience):** an Ollama Cloud (or any apikey-category provider) account that hit a weekly usage cap kept getting retried every few minutes instead of backing off ([#3709](https://github.com/diegosouzapw/OmniRoute/issues/3709)) — the upstream 429 body ("you (\<account\>) have reached your weekly usage limit") was invisible to `checkFallbackError`'s existing subscription-quota-text classifier (Issue #2321) because that branch is gated by `shouldUseQuotaSignal`, which is oauth-only, so apikey providers like `ollama-cloud` fell through to the generic exponential backoff (~1s, capped at 2min) — one account took 285x429 in 48h. A new `isWeeklyUsageLimitText`/`buildWeeklyQuotaFallback` classifier (extracted, with the existing subscription-quota logic, into a new `open-sse/services/quotaTextCooldowns.ts` module so the frozen `accountFallback.ts` didn't have to grow) runs unconditionally and applies a 24h `QUOTA_EXHAUSTED` cooldown regardless of provider category. Regression guard: `tests/unit/ollama-cloud-weekly-quota-cooldown-3709.test.ts`.
|
||||
- **fix(providers):** an explicit `thinking.budget_tokens: 0` is now honored in the OpenAI→Gemini transform (thinking disabled) instead of being treated as unset (#6813, #6821 — thanks @alltomatos).
|
||||
- **fix(bootstrap):** filter empty `process.env` values before spawning embedded services so a blank env var no longer crashes the Docker bootstrap in a restart loop (#6828 — thanks @AndrianBalanescu).
|
||||
- **fix(providers):** classify upstream `404` responses as `MODEL_NOT_FOUND` (model lockout) instead of a retryable provider error, stopping the retry storm when a single model is missing (#6829 — thanks @AndrianBalanescu).
|
||||
- fix(mcp): de-duplicate `TOTAL_MCP_TOOL_COUNT` by tool name instead of double-counting collections (#6854)
|
||||
- **fix(usage):** xAI's exact provider-reported `cost_in_usd_ticks` no longer silently accepts `null`/`""`/negative values as a valid `$0` exact cost — `extractUsageFromResponse()` (`open-sse/handlers/usageExtractor.ts`) and `normalizeUsage()` (`open-sse/utils/usageTracking.ts`) now require `typeof value === "number" && Number.isFinite(value) && value >= 0` instead of coercing with `Number(x)`, so a malformed exact cost correctly falls back to the token-based estimate instead of masking it with a bogus `$0`. Regression guard: `tests/unit/xai-exact-cost-2453.test.ts` (rejects null/empty/negative exact costs on both call sites). ([#6856](https://github.com/diegosouzapw/OmniRoute/pull/6856) — thanks @KooshaPari)
|
||||
- **fix(plugin):** the `@omniroute/opencode-plugin` dynamic provider hook stopped embedding its OC-1.17.8+-gate-compatible `opencode-`-prefixed provider id into model routing fields (`ModelV2.id`/`providerID`, combo catalog keys) — OmniRoute's server has no `opencode-<x>` provider alias, so every dispatched model failed credential lookup with "No credentials for opencode-omniroute" (#6859).
|
||||
- fix(sse): apply cliproxyapiModelMapping at CLIProxyAPI dispatch time (#6876)
|
||||
- fix(sse): defer `response.completed` until a trailing usage-only chunk arrives on `/v1/responses` streams (#6906)
|
||||
- **fix(cli):** ship `head-response-guard.cjs` into the standalone bundle — `server-ws.mjs` imported it without a matching `EXTRA_MODULE_ENTRIES` entry, so every `build:release` dist crashed at boot with `ERR_MODULE_NOT_FOUND`; a new regression test derives the required sidecars from `server-ws.mjs` imports (#6908)
|
||||
- fix(sse): wire the shared quota-fetch throttle into DeepSeek, Bailian, OpenCode, and Crof quota fetchers, not just Codex (#6911)
|
||||
- fix(sse): rename client-sent `max_completion_tokens` to `max_tokens` for providers/models that only accept the legacy field (e.g. Volcengine Ark / DeepSeek), mirroring the existing reverse rename from #1961 (#6912)
|
||||
- fix(sse): set includeServerSideToolInvocations on Antigravity tool cloak decoys (#6914)
|
||||
- fix(sse): classify LAN embeddings providers (10/8, 192.168/16, CGNAT) as no-auth instead of forcing bearer auth (#6925)
|
||||
- fix(sse): Qwen Web executor no longer sends `[object Object]` when a message uses structured (array) content — the text parts are now flattened (#6927)
|
||||
- perf(api): relay chat-completions routes now thread the already-fetched `RelayToken` into `checkRateLimit`, skipping a redundant `SELECT * FROM relay_tokens WHERE id = ?` re-query on every request (#6930)
|
||||
- fix(sse): `normalizeCodexMessageContentPart` now rewrites explicit `type: "input_text"` (not just `type: "text"`) to `output_text` on assistant-role Codex Responses input parts, so replayed assistant history sent by codex-cli as `input_text` is no longer rejected by the Codex/OpenAI backend (#6932)
|
||||
- fix(sse): omit removed `attachments` field from Muse Spark Web (Meta AI) persisted GraphQL query to fix `Unknown type "AttachmentInput"` 502s (#6935)
|
||||
- fix(dashboard): label audio/embeddings/image compatible providers by kind instead of "Chat" on ProviderCard (#6936)
|
||||
- fix(api): model-list discovery for LAN-local OpenAI-compatible providers (e.g. LM Studio) now uses the same local-first SSRF guard as the connection test, instead of the stricter guard that always blocked LAN hosts (#6939)
|
||||
- fix(providers): `openai->gemini` transform now maps `reasoning_effort: "none"` to `thinkingConfig.thinkingBudget: 0` (with `includeThoughts: false`), giving callers an explicit, documented off-switch for Gemini thinking; the no-knob-at-all default injection (#4170) is unchanged (#6813, thanks @rafaumeu)
|
||||
- **fix(sse):** escape backslash before brackets in ChatGPT-web citation link text, preventing a citation label containing `\` from corrupting the generated Markdown link ([#6944](https://github.com/diegosouzapw/OmniRoute/pull/6944)) — thanks @brick30llc-ctrl
|
||||
- fix(oauth): tokenHealthCheck now lowercase-normalizes `conn.provider` before checking `ROTATING_REFRESH_PROVIDERS.has()` and the GitHub Copilot sub-token refresh guard, so mixed-case provider values (e.g. "OpenAI", "Github") no longer bypass the rotating-refresh-token skip or the Copilot sub-token refresh (#6947)
|
||||
- fix(sse): make Codex Responses tool-arg normalization schema-aware — drop values equal to the tool's declared JSON Schema `default`, generalize empty-optional stripping to any tool (not just `Read`/`Subagent`) via `schema.required`, and thread each tool's schema from the request's `tools[]` into the streaming response translator (#6951)
|
||||
- fix(sse): drop internal commentary-phase Responses output in TRANSLATE-mode streams, not just PASSTHROUGH — codex/Responses-upstream routes translated into another client format (e.g. Claude Code) no longer leak duplicate prose and narrated tool-call arguments into the client text channel (#6952)
|
||||
- **fix(combos):** embeddings-only and rerank-only models (e.g. JinaAI, Gemini auto-imported, OpenRouter custom, reranker models) no longer disappear from the combo builder's model picker — the leftover chat-only `isChatCapable` gate in `addModelOption()` has been removed (#6975).
|
||||
- **fix(combos):** when 2+ distinct model ids from the same provider would render an identical display name in the combo builder picker (e.g. Mistral's `codestral-latest`/`codestral-2508` aliases sharing one upstream catalog name), each colliding entry now falls back to its own id as the display label so every row stays visually distinguishable and findable (#6957).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release-captain):** v3.8.47 pre-flight closed every deterministic release-tip base-red ([#6967](https://github.com/diegosouzapw/OmniRoute/issues/6967)): restored the `no-explicit-any` severity silently downgraded by #6786 (439 bulk suppressions had stopped matching), made the openadapter live-catalog test deterministic (`test.after()` was tearing the DB down mid-request), aligned the emergency-fallback and Cloud Code Gemini tests with the #6912/#6943 contracts, backfilled the #6909 i18n keys (en + pt-BR), re-exported `relayProbeStats` (db-rules), documented `OMNI_MAX_CONCURRENT_CONNECTIONS`, and allowlisted migration gap 121. (thanks @diegosouzapw)
|
||||
- **chore(security):** unbiased crypto digits for the doubao synthetic device id (CodeQL `js/biased-cryptographic-random`); 405 method-first for `/api/keys/{id}/devices` (dast-smoke); Zod-validation for `POST /api/github-skills`; the missing `omni-github-skills` registry entry + catalog count alignment. (thanks @diegosouzapw)
|
||||
- **chore(quality):** cycle-close ratchet work — cleared the cycle's 11 net-new ESLint errors and made `validate-release-green` suppressions-aware; cleared the 2 remaining heavy-gate reds on the release tip; cycle rebaselines (cognitive/file-size/zizmor/coverage pcts) with justification keys. (thanks @diegosouzapw)
|
||||
|
||||
- **chore(open-sse):** removed the vestigial `// @ts-nocheck` directive from `open-sse/utils/usageTracking.ts` ([#6173](https://github.com/diegosouzapw/OmniRoute/pull/6173)) — `tsc` under the standard `typecheck:core` gate reports 0 errors for this 595-line hot-path file (executed for every provider response), so the suppression was no longer needed; removing it restores type-checking on the token-usage extraction/normalization path. (thanks @KooshaPari)
|
||||
- **chore(cli):** the shell-completion cache paths (`readCache`/`refreshCache`/`writeCache`) in `bin/cli/commands/completion.mjs` no longer swallow errors into a bare `catch {}` ([#6257](https://github.com/diegosouzapw/OmniRoute/pull/6257)) — each now binds the error and, when the new `OMNIROUTE_DEBUG_COMPLETION` env var is set, emits a `[omniroute completion]` diagnostic to `stderr`; the caches still fail silently by default so a missing/corrupt cache never breaks tab-completion. (thanks @KooshaPari)
|
||||
- **chore(quality):** `validate-release-green --full-ci` reproduces the full `ci.yml` static gate set locally — the pre-flight now reads `ci.yml` itself and runs every `npm run check:*` from the `lint` / `quality-gate` / `quality-extended` / `docs-sync-strict` / `pr-test-policy` jobs (`--` ratchet flags preserved, `test-masking` against `GITHUB_BASE_REF=main`), skipping only the non-local `pr-evidence`/`codeql-ratchet`. Closes the gap where 11 static base-reds leaked to the v3.8.46 release PR in ~2h of layered CI. Also wired into `nightly-release-green` so a static base-red opens a tracking issue the night it lands. Regression guard: `tests/unit/validate-release-green.test.ts` (+5 `extractCiGates` cases).
|
||||
- **refactor(usage):** `saveRequestUsage(entry: any)` is now typed with a new `UsageEntry` interface mirroring the `usage_history` columns 1:1 (#3512) — the other stray `any`s in `src/lib/usage/usageHistory.ts` (`getUsageHistory` filter, the `getUsageDb` next-cursor cast, `appendRequestLog`'s legacy `tokens` param, `getRecentLogs`'s catch) were cleaned in the same pass, so the file now sits in the `check:any-budget:t11` zero-`any` allowlist. The DB-entity ↔ TS-interface convention is documented in `docs/architecture/CODEBASE_DOCUMENTATION.md` §11.
|
||||
- **Merge-train script** (`scripts/release/merge-train.sh`): batch-validates N queued PRs as ONE merged result on the runner box — merges every queued PR into a throwaway worktree cut from the release tip, runs the fast-gates parity suite once, and prints the `--admin` evidence block per PR (merge-gates §7). Replaces O(N²) per-PR CI re-runs in merge-storms. Regression guard: `tests/unit/merge-train-plan.test.ts`.
|
||||
- **release:** `list-uncovered-commits.mjs` now unions the CHANGELOG scan window with `changelog.d/` fragment refs (filename `<PR>-` prefix + every `#N` in the body), so a commit covered only by a fragment is no longer reported as an uncovered reconciliation gap ([#6857](https://github.com/diegosouzapw/OmniRoute/issues/6857) via [#6878](https://github.com/diegosouzapw/OmniRoute/pull/6878))
|
||||
- chore(quality): restore no-explicit-any severity to error (silently downgraded by #6786, broke 439 bulk suppressions), fix 2 unsuppressed anys in repro-6912 test, allowlist migration gap 121, freeze-bump stream.ts/ProxyRegistryManager/tokenHealthCheck (combined-merge drift)
|
||||
- chore(base): pt-BR/en i18n keys for #6909 relay-repair/free-pool UI + align Cloud Code Gemini defaults test with the #6943 non-thinking-model contract
|
||||
- chore(base): re-export relayProbeStats from localDb (db-rules gate, #6909 follow-up) and document OMNI_MAX_CONCURRENT_CONNECTIONS in .env.example/ENVIRONMENT.md (env-doc gate, #6590 follow-up)
|
||||
- **ci:** unit fast-path sharding doubled 2→4 (halves the heaviest job's wall time) (#6781); the 3 heaviest fast-path jobs can route to the self-hosted VPS runner pool behind `USE_VPS_RUNNER` (#6691); `VPS_ALWAYS_ON` keeps the dedicated 24/7 CI host up across releases (teardown becomes a no-op) (#6693).
|
||||
- **docs:** routing-strategy count reconciled to 18 across AUTO-COMBO.md, README and AGENTS.md, and `p2c` casing fixed to match `ROUTING_STRATEGY_VALUES` (#6643, #6644, #6646 — thanks @chirag127); CLAUDE.md updated with the renamed review/triage/implement skill-family names (#6663).
|
||||
- chore(release): v3.8.47 pre-flight — relocate #6943 orphan test to a collected path, restore no-explicit-any suppression match, testFrozen bump translator-openai-to-gemini (1541→1553), zizmor rebaseline 159→169
|
||||
- **docs(readme):** fix stale counts — 18 routing strategies (adds the missing `pipeline` row), 94 MCP tools, 12-factor Auto-Combo scoring.
|
||||
- **chore(ci):** fix two shared base-reds on the release tip that blocked the PR queue — register `cliproxyapi-model-mapping-dispatch.test.ts` in `stryker.conf.json` `tap.testFiles` (mutation-coverage gap left by #6903) and update `provider-models-route-codex.test.ts` to expect Codex client version `0.144.0` (stale assertion left by #6780's production bump).
|
||||
- **docs:** refresh `llm.txt` to the current project state (248 providers, 94 MCP tools / 30 scopes, 18 routing strategies, 12-factor Auto-Combo scoring, v3.8.47) and sync its 42 i18n mirrors; move the implemented design-system plan from the repo root to `docs/architecture/DESIGN_SYSTEM.md` rewritten as a reference doc.
|
||||
- **chore(security):** scrub hardcoded live-instance credentials (API key + auth cookie + host URL) from the `tests/boundary/*.live.test.ts` files landed via #6786 — they now read `OMNIROUTE_TEST_BASE` / `OMNIROUTE_TEST_BEARER` / `OMNIROUTE_TEST_COOKIE` from the environment and stay gated behind `RUN_BOUNDARY_LIVE=1`.
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
Thanks to everyone whose work landed in v3.8.47:
|
||||
|
||||
| Contributor | PRs / Issues |
|
||||
| -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| [@AgentKiller45](https://github.com/AgentKiller45) | #6863, #6866 |
|
||||
| [@alltomatos](https://github.com/alltomatos) | #6703, #6715, #6756, #6757, #6759, #6813, #6819, #6821 |
|
||||
| [@andrewmunsell](https://github.com/andrewmunsell) | #6774, #6779, #6795 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #6828, #6829 |
|
||||
| [@anhdiepmmk](https://github.com/anhdiepmmk) | direct commit / report |
|
||||
| [@artickc](https://github.com/artickc) | #6363, #6763 |
|
||||
| [@backryun](https://github.com/backryun) | #6280, #6675, #6862 |
|
||||
| [@blackwell-systems](https://github.com/blackwell-systems) | #6838 |
|
||||
| [@brick30llc-ctrl](https://github.com/brick30llc-ctrl) | #6944 |
|
||||
| [@charleszolot](https://github.com/charleszolot) | #6571 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6402, #6458, #6460, #6461, #6463, #6515, #6519, #6523, #6534, #6546, #6577, #6643, #6644, #6645, #6646, #6769, #6804, #6868, #6869, #6870, #6871, #6883 |
|
||||
| [@chy1211](https://github.com/chy1211) | direct commit / report |
|
||||
| [@crochabe-cyber](https://github.com/crochabe-cyber) | #4013 |
|
||||
| [@deadcoder0904](https://github.com/deadcoder0904) | #6665 |
|
||||
| [@developerjillur](https://github.com/developerjillur) | direct commit / report |
|
||||
| [@eidoog](https://github.com/eidoog) | direct commit / report |
|
||||
| [@enjoyer-hub](https://github.com/enjoyer-hub) | #6647 |
|
||||
| [@gdevenyi](https://github.com/gdevenyi) | direct commit / report |
|
||||
| [@growab](https://github.com/growab) | #6867 |
|
||||
| [@hajilok](https://github.com/hajilok) | #6126, #6833 |
|
||||
| [@hamsa0x7](https://github.com/hamsa0x7) | #6317, #6318, #6338 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | direct commit / report |
|
||||
| [@iamraydoan](https://github.com/iamraydoan) | #6798 |
|
||||
| [@ianriizky](https://github.com/ianriizky) | #6072, #6538 |
|
||||
| [@itiwant](https://github.com/itiwant) | direct commit / report |
|
||||
| [@janeza2](https://github.com/janeza2) | #6308 |
|
||||
| [@JxnLexn](https://github.com/JxnLexn) | #6776 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #6611, #6632, #6856 |
|
||||
| [@like3213934360-lab](https://github.com/like3213934360-lab) | direct commit / report |
|
||||
| [@lucasjustinudin](https://github.com/lucasjustinudin) | direct commit / report |
|
||||
| [@lunkerchen](https://github.com/lunkerchen) | #6320 |
|
||||
| [@MikeTuev](https://github.com/MikeTuev) | #6586, #6830 |
|
||||
| [@Moseyuh333](https://github.com/Moseyuh333) | #6294, #6728 |
|
||||
| [@nowhats-br](https://github.com/nowhats-br) | direct commit / report |
|
||||
| [@oyi77](https://github.com/oyi77) | #6907, #6926, #6929, #6946 |
|
||||
| [@Pitchfork-and-Torch](https://github.com/Pitchfork-and-Torch) | #6747, #6791, #6792 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #6648 |
|
||||
| [@quanturbo](https://github.com/quanturbo) | #6780 |
|
||||
| [@rafaumeu](https://github.com/rafaumeu) | #6813 |
|
||||
| [@rafpigna](https://github.com/rafpigna) | #6574 |
|
||||
| [@rucciva](https://github.com/rucciva) | #4125 |
|
||||
| [@rushsinging](https://github.com/rushsinging) | #6807 |
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@samir-abis](https://github.com/samir-abis) | direct commit / report |
|
||||
| [@SeaXen](https://github.com/SeaXen) | direct commit / report |
|
||||
| [@shabeer](https://github.com/shabeer) | direct commit / report |
|
||||
| [@Squawk7777](https://github.com/Squawk7777) | #6565 |
|
||||
| [@strangersp](https://github.com/strangersp) | #6587 |
|
||||
| [@Thinkscape](https://github.com/Thinkscape) | direct commit / report |
|
||||
| [@ThongAccount](https://github.com/ThongAccount) | #6625, #6649 |
|
||||
| [@tjengbudi](https://github.com/tjengbudi) | #4009 |
|
||||
| [@whale9820](https://github.com/whale9820) | direct commit / report |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #6753, #6762, #6790 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #6323, #6330, #6702, #6714, #6727 |
|
||||
| [@yinaoxiong](https://github.com/yinaoxiong) | #6805 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
## [3.8.46] — 2026-07-07
|
||||
## [3.8.46] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -479,7 +142,6 @@ Thanks to everyone whose work landed in v3.8.47:
|
||||
- **fix(api):** `POST /api/github-skills` now Zod-validates its request body; documented the new quality-gate env vars and pinned the merge-integrity GitHub Actions to a commit SHA.
|
||||
- **fix(skills):** generate the missing `omni-github-skills` registry entry and align the agent-skills catalog-count tests (follow-up to #6186).
|
||||
- **fix(quality):** clear the cycle's 11 net-new ESLint errors and make `validate-release-green` suppressions-aware.
|
||||
- **fix(security):** proxy-pool `random` rotation now selects via `crypto.randomInt` instead of `Math.random` — silences the post-release CodeQL `js/insecure-randomness` alerts (#698/#699) that flagged `Math.random` flowing into the selected proxy's credentials. Load-balancing selection is not a secret, but the crypto source is unbiased and clears the alert at the origin (#6365 follow-up).
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -3550,7 +3212,6 @@ And thank you to the OmniRoute community for the bug reports, reproductions, and
|
||||
|
||||
### Fixed
|
||||
|
||||
- **usage:** use xAI's exact provider-reported cost when present instead of always estimating from token counts. (thanks @ryanngit)
|
||||
- **memory:** the `recent` retrieval strategy no longer drops recent memories whose
|
||||
text doesn't overlap the current prompt. It was internally mapped to the `exact`
|
||||
path, which relevance-filtered by the forwarded prompt (`score > 0`), so
|
||||
|
||||
12
CLAUDE.md
12
CLAUDE.md
@@ -35,7 +35,7 @@ For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep archit
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 250 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 237 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
@@ -72,7 +72,7 @@ Client → /v1/chat/completions (Next.js route)
|
||||
|
||||
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
|
||||
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, p2c, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
**Combo routing** (`open-sse/services/combo.ts`): 18 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, reset-window, headroom, strict-random, auto, lkgp, context-optimized, context-relay, fusion, pipeline). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. The `fusion` strategy is the exception: it fans out to a panel of models in parallel, then a judge model synthesizes one final answer (`open-sse/services/fusion.ts`). See `docs/routing/AUTO-COMBO.md` for the 12-factor Auto-Combo scoring + the full strategy table and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
||||
|
||||
---
|
||||
|
||||
@@ -428,10 +428,8 @@ git push -u origin feat/your-feature
|
||||
|
||||
**Husky hooks**:
|
||||
|
||||
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11` + `check:tracked-artifacts`
|
||||
- **pre-push**: intentionally light (PATH/npm sanity only). `any-budget` + `tracked-artifacts`
|
||||
already run on pre-commit; re-running them on every push was pure double-pay. CI still
|
||||
enforces both. (Was Fase 6A.12 full pre-push gate; folded into pre-commit in #6716.)
|
||||
- **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
|
||||
- **pre-push**: fast deterministic gates (`check:any-budget:t11` + `check:tracked-artifacts`); intentionally excludes `test:unit` (slow — covered by the CI `test-unit` job). Activated 2026-06-13 (Quality Gates Fase 6A.12).
|
||||
|
||||
### Worktree isolation (MANDATORY for every development task)
|
||||
|
||||
@@ -542,7 +540,7 @@ the stale-enforcement added in Fase 6A.3.
|
||||
18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
|
||||
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
|
||||
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
|
||||
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-prs`, `/review-group-prs`, `/merge-prs`, `/triage-fix-bugs`, `/implement-fix-bugs`, `/triage-features`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
|
||||
21. **Release-freeze — the FROZEN release branch belongs to the release captain; development does NOT stop (parallel-cycle model, 2026-07-04).** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a), **immediately cuts the next cycle's branch `release/vX+1` from the frozen tip (Phase 0a.0b — bump + living release PR + re-home of open PRs)**, and closes the freeze once the release PR squash-merges to `main`. Before merging **any** PR, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active: **NEVER merge into the frozen `release/vX.Y.Z` named in the freeze title**; instead resolve the ACTIVE development branch (the **highest** `release/v*` by semver — normally `release/vX+1`, announced in a freeze-issue comment) and **retarget the PR there** (`gh pr edit <N> --base release/vX+1`, then VERIFY with `gh pr view <N> --json baseRefName` — the edit fails silently) and merge normally. **HOLD only when the highest release/v\* branch IS the frozen one** (the short window before 0a.0b completes, or a pre-parallel-cycle release) — in that case leave the PR ready and open, tell the operator, and resume when the next branch appears or the freeze lifts. The just-shipped fixes reach `release/vX+1` via the Phase 5 sync-back (`scripts/release/sync-next-cycle.mjs`); do not try to sync mid-release. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
|
||||
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
|
||||
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
|
||||
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create _this_ session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
|
||||
|
||||
@@ -341,7 +341,7 @@ Write unit tests in `tests/unit/` covering at minimum:
|
||||
- [ ] Error responses route through `buildErrorBody()` / `sanitizeErrorMessage()` — no raw stack traces in response bodies (see [`docs/security/ERROR_SANITIZATION.md`](./docs/security/ERROR_SANITIZATION.md))
|
||||
- [ ] Shell commands (`exec` / `spawn`) pass runtime values via `env`, not via string interpolation
|
||||
- [ ] All inputs validated with Zod schemas
|
||||
- [ ] Changelog **fragment** added under `changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md` for user-facing changes (see [`changelog.d/README.md`](./changelog.d/README.md)) — do **not** edit `CHANGELOG.md` directly; fragments are aggregated at release time and never conflict between PRs
|
||||
- [ ] CHANGELOG updated (if user-facing change)
|
||||
- [ ] Documentation updated (if applicable)
|
||||
- [ ] No new CodeQL / Secret-Scanning alerts opened, or each one dismissed with technical justification referencing the relevant `docs/security/` doc
|
||||
- [ ] Routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) classified as `isLocalOnlyPath()` in `src/server/authz/routeGuard.ts` — see [Hard Rule #15](docs/security/ROUTE_GUARD_TIERS.md)
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -55,20 +55,9 @@ ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
|
||||
# are reproducible.
|
||||
RUN test -f package-lock.json \
|
||||
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
|
||||
# `npm rebuild <pkg>` re-runs the package's own install script, so under npm 11 +
|
||||
# `--ignore-scripts` on the parent `npm ci` it depends on npm's script-allowlist
|
||||
# machinery correctly re-enabling that one package's script. Some self-hosted build
|
||||
# environments (e.g. Dokploy) hit a broken/incomplete better-sqlite3 native binding
|
||||
# from that indirection. Invoking `node-gyp rebuild` directly inside the package
|
||||
# directory bypasses npm's script-running layer entirely and is deterministic
|
||||
# regardless of npm version or ignore-scripts allowlist behavior.
|
||||
# node-gyp comes from npm's own bundled copy (deterministic, already in the image)
|
||||
# instead of `npx --yes`, which would install an arbitrary registry version
|
||||
# on-demand and run its lifecycle scripts (Sonar docker:S6505).
|
||||
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
|
||||
npm ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
|
||||
&& (cd node_modules/better-sqlite3 \
|
||||
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
|
||||
&& npm rebuild better-sqlite3 \
|
||||
&& node -e "require('better-sqlite3')(':memory:').close()"
|
||||
|
||||
# Build with Turbopack (stable in Next 16, the repo default). The v3.8.27-era
|
||||
@@ -114,12 +103,6 @@ LABEL org.opencontainers.image.title="omniroute" \
|
||||
ENV NODE_ENV=production
|
||||
ENV PORT=20128
|
||||
ENV HOSTNAME=0.0.0.0
|
||||
# Runtime heap ceiling. 1024MB is enough for normal traffic but can be tight
|
||||
# for large fusion-combo panels (many models fanned out in parallel, each
|
||||
# response buffered in full — see open-sse/services/fusion.ts::FUSION_DEFAULTS
|
||||
# .maxPanel, issue #1905). Override at `docker run` time with
|
||||
# `-e OMNIROUTE_MEMORY_MB=2048` (or higher) if you raise fusionTuning.maxPanel
|
||||
# above the default cap.
|
||||
ENV OMNIROUTE_MEMORY_MB=1024
|
||||
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
|
||||
|
||||
|
||||
255
README.md
255
README.md
@@ -6,7 +6,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
### Never stop coding. Connect every AI tool to **250 providers** — **90+ free** — through one endpoint.
|
||||
### Never stop coding. Connect every AI tool to **237 providers** — **90+ free** — through one endpoint.
|
||||
|
||||
**Plug Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini. Auto-fallback.**
|
||||
<br/>
|
||||
@@ -21,21 +21,16 @@
|
||||
|
||||
<h3>
|
||||
|
||||
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier.
|
||||
|
||||
⭐ Star the repo if OMNIROUTE helped you save money and make your work easier. [](https://github.com/diegosouzapw/OmniRoute)
|
||||
</h3>
|
||||
|
||||
[](https://github.com/diegosouzapw/OmniRoute)
|
||||
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
[](https://www.star-history.com/diegosouzapw/omniroute)
|
||||
|
||||
</br>
|
||||
|
||||
[](#-250-ai-providers--90-free)
|
||||
[](#-250-ai-providers--90-free)
|
||||
[](#-237-ai-providers--90-free)
|
||||
[](#-237-ai-providers--90-free)
|
||||
[](docs/reference/FREE_TIERS.md)
|
||||
[](#%EF%B8%8F-save-1595-tokens--automatically)
|
||||
[](#-combos--the-flagship)
|
||||
[](#-combos--the-flagship)
|
||||
[](#-quick-start)
|
||||
|
||||
<br/>
|
||||
@@ -61,7 +56,7 @@
|
||||

|
||||

|
||||
|
||||
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-250-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
|
||||
[**🚀 Quick Start**](#-quick-start) • [**🎯 Combos**](#-combos--the-flagship) • [**🌐 Providers**](#-237-ai-providers--90-free) • [**🔌 CLI & MCP**](#-full-cli--a2a--mcp) • [**🗜️ Compression**](#%EF%B8%8F-save-1595-tokens--automatically) • [**🌍 Website**](https://omniroute.online)
|
||||
|
||||
[💥 The Promise](#-the-promise) • [🤔 Why](#-why-omniroute) • [🏆 What Sets Apart](#-what-sets-omniroute-apart) • [🤖 Compatible CLIs](#-compatible-clis--coding-agents) • [🖥️ Where It Runs](#%EF%B8%8F-where-omniroute-runs--anywhere) • [🔒 Private](#-private--local-first) • [🎬 In Action](#-omniroute-in-action) • [📚 Explore More](#-explore-more) • [📧 Support](#-support--community)
|
||||
|
||||
@@ -71,53 +66,53 @@
|
||||
<b>🌐 In 42+ languages</b>
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center"><a href="README.md"><img src="docs/assets/flags/us.svg" width="26" alt="English (en)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/pt-BR/README.md"><img src="docs/assets/flags/br.svg" width="26" alt="Português — Brasil (pt-BR)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/pt/README.md"><img src="docs/assets/flags/pt.svg" width="26" alt="Português (pt)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/es/README.md"><img src="docs/assets/flags/es.svg" width="26" alt="Español (es)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/fr/README.md"><img src="docs/assets/flags/fr.svg" width="26" alt="Français (fr)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/it/README.md"><img src="docs/assets/flags/it.svg" width="26" alt="Italiano (it)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/de/README.md"><img src="docs/assets/flags/de.svg" width="26" alt="Deutsch (de)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/nl/README.md"><img src="docs/assets/flags/nl.svg" width="26" alt="Nederlands (nl)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ru/README.md"><img src="docs/assets/flags/ru.svg" width="26" alt="Русский (ru)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/uk-UA/README.md"><img src="docs/assets/flags/ua.svg" width="26" alt="Українська (uk-UA)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/pl/README.md"><img src="docs/assets/flags/pl.svg" width="26" alt="Polski (pl)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/cs/README.md"><img src="docs/assets/flags/cz.svg" width="26" alt="Čeština (cs)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/sk/README.md"><img src="docs/assets/flags/sk.svg" width="26" alt="Slovenčina (sk)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ro/README.md"><img src="docs/assets/flags/ro.svg" width="26" alt="Română (ro)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/hu/README.md"><img src="docs/assets/flags/hu.svg" width="26" alt="Magyar (hu)"></a></td>
|
||||
<td align="center"><a href="README.md">🇺🇸</a></td>
|
||||
<td align="center"><a href="docs/i18n/pt-BR/README.md">🇧🇷</a></td>
|
||||
<td align="center"><a href="docs/i18n/pt/README.md">🇵🇹</a></td>
|
||||
<td align="center"><a href="docs/i18n/es/README.md">🇪🇸</a></td>
|
||||
<td align="center"><a href="docs/i18n/fr/README.md">🇫🇷</a></td>
|
||||
<td align="center"><a href="docs/i18n/it/README.md">🇮🇹</a></td>
|
||||
<td align="center"><a href="docs/i18n/de/README.md">🇩🇪</a></td>
|
||||
<td align="center"><a href="docs/i18n/nl/README.md">🇳🇱</a></td>
|
||||
<td align="center"><a href="docs/i18n/ru/README.md">🇷🇺</a></td>
|
||||
<td align="center"><a href="docs/i18n/uk-UA/README.md">🇺🇦</a></td>
|
||||
<td align="center"><a href="docs/i18n/pl/README.md">🇵🇱</a></td>
|
||||
<td align="center"><a href="docs/i18n/cs/README.md">🇨🇿</a></td>
|
||||
<td align="center"><a href="docs/i18n/sk/README.md">🇸🇰</a></td>
|
||||
<td align="center"><a href="docs/i18n/ro/README.md">🇷🇴</a></td>
|
||||
<td align="center"><a href="docs/i18n/hu/README.md">🇭🇺</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="docs/i18n/bg/README.md"><img src="docs/assets/flags/bg.svg" width="26" alt="Български (bg)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/da/README.md"><img src="docs/assets/flags/dk.svg" width="26" alt="Dansk (da)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/fi/README.md"><img src="docs/assets/flags/fi.svg" width="26" alt="Suomi (fi)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/no/README.md"><img src="docs/assets/flags/no.svg" width="26" alt="Norsk (no)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/sv/README.md"><img src="docs/assets/flags/se.svg" width="26" alt="Svenska (sv)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/zh-CN/README.md"><img src="docs/assets/flags/cn.svg" width="26" alt="中文 — 简体 (zh-CN)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/zh-TW/README.md"><img src="docs/assets/flags/tw.svg" width="26" alt="中文 — 繁體 (zh-TW)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ja/README.md"><img src="docs/assets/flags/jp.svg" width="26" alt="日本語 (ja)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ko/README.md"><img src="docs/assets/flags/kr.svg" width="26" alt="한국어 (ko)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/th/README.md"><img src="docs/assets/flags/th.svg" width="26" alt="ไทย (th)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/vi/README.md"><img src="docs/assets/flags/vn.svg" width="26" alt="Tiếng Việt (vi)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/id/README.md"><img src="docs/assets/flags/id.svg" width="26" alt="Bahasa Indonesia (id)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ms/README.md"><img src="docs/assets/flags/my.svg" width="26" alt="Bahasa Melayu (ms)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/phi/README.md"><img src="docs/assets/flags/ph.svg" width="26" alt="Filipino (phi)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/bg/README.md">🇧🇬</a></td>
|
||||
<td align="center"><a href="docs/i18n/da/README.md">🇩🇰</a></td>
|
||||
<td align="center"><a href="docs/i18n/fi/README.md">🇫🇮</a></td>
|
||||
<td align="center"><a href="docs/i18n/no/README.md">🇳🇴</a></td>
|
||||
<td align="center"><a href="docs/i18n/sv/README.md">🇸🇪</a></td>
|
||||
<td align="center"><a href="docs/i18n/zh-CN/README.md">🇨🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/zh-TW/README.md">🇹🇼</a></td>
|
||||
<td align="center"><a href="docs/i18n/ja/README.md">🇯🇵</a></td>
|
||||
<td align="center"><a href="docs/i18n/ko/README.md">🇰🇷</a></td>
|
||||
<td align="center"><a href="docs/i18n/th/README.md">🇹🇭</a></td>
|
||||
<td align="center"><a href="docs/i18n/vi/README.md">🇻🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/id/README.md">🇮🇩</a></td>
|
||||
<td align="center"><a href="docs/i18n/ms/README.md">🇲🇾</a></td>
|
||||
<td align="center"><a href="docs/i18n/phi/README.md">🇵🇭</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align="center"><a href="docs/i18n/in/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="हिन्दी (in)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/hi/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="हिन्दी (hi)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/gu/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="ગુજરાતી (gu)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/mr/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="मराठी (mr)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ta/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="தமிழ் (ta)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/te/README.md"><img src="docs/assets/flags/in.svg" width="26" alt="తెలుగు (te)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/bn/README.md"><img src="docs/assets/flags/bd.svg" width="26" alt="বাংলা (bn)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ur/README.md"><img src="docs/assets/flags/pk.svg" width="26" alt="اردو (ur)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/fa/README.md"><img src="docs/assets/flags/ir.svg" width="26" alt="فارسی (fa)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/ar/README.md"><img src="docs/assets/flags/sa.svg" width="26" alt="العربية (ar)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/he/README.md"><img src="docs/assets/flags/il.svg" width="26" alt="עברית (he)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/tr/README.md"><img src="docs/assets/flags/tr.svg" width="26" alt="Türkçe (tr)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/az/README.md"><img src="docs/assets/flags/az.svg" width="26" alt="Azərbaycan (az)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/sw/README.md"><img src="docs/assets/flags/tz.svg" width="26" alt="Kiswahili (sw)"></a></td>
|
||||
<td align="center"><a href="docs/i18n/in/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/hi/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/gu/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/mr/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/ta/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/te/README.md">🇮🇳</a></td>
|
||||
<td align="center"><a href="docs/i18n/bn/README.md">🇧🇩</a></td>
|
||||
<td align="center"><a href="docs/i18n/ur/README.md">🇵🇰</a></td>
|
||||
<td align="center"><a href="docs/i18n/fa/README.md">🇮🇷</a></td>
|
||||
<td align="center"><a href="docs/i18n/ar/README.md">🇸🇦</a></td>
|
||||
<td align="center"><a href="docs/i18n/he/README.md">🇮🇱</a></td>
|
||||
<td align="center"><a href="docs/i18n/tr/README.md">🇹🇷</a></td>
|
||||
<td align="center"><a href="docs/i18n/az/README.md">🇦🇿</a></td>
|
||||
<td align="center"><a href="docs/i18n/sw/README.md">🇹🇿</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -149,18 +144,18 @@
|
||||
|
||||
</div>
|
||||
|
||||
> One endpoint. **250 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
> One endpoint. **237 providers.** Never stop building — and let OmniRoute pick the cheapest one that works.
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 250 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>🚫 Never hit limits</b><br/><sub>Auto-fallback across 237 providers in milliseconds. Quota out? Next provider takes over — zero downtime.</sub></td>
|
||||
<td width="33%" valign="top"><b>💸 Save up to 95% tokens</b><br/><sub>RTK + Caveman stacked compression cuts 15–95% of eligible tokens (~89% avg on tool-heavy sessions).</sub></td>
|
||||
<td width="33%" valign="top"><b>🆓 $0 to start</b><br/><sub>90+ providers with a free tier, 11 free <i>forever</i> (Kiro, Qoder, Pollinations, LongCat…). No card needed.</sub></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td width="33%" valign="top"><b>🔌 Every tool works</b><br/><sub>24+ coding agents — Claude Code, Codex, Cursor, Cline, Copilot, Antigravity — through one config.</sub></td>
|
||||
<td width="33%" valign="top"><b>🧩 One endpoint</b><br/><sub>OpenAI ↔ Claude ↔ Gemini ↔ Responses API translation. Point any tool at <code>/v1</code> and it just works.</sub></td>
|
||||
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (94 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
|
||||
<td width="33%" valign="top"><b>🛡️ Production-grade</b><br/><sub>Circuit breakers, TLS stealth, MCP (95 tools), A2A, memory, guardrails, evals. 21,000+ tests.</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -194,7 +189,7 @@
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────┐
|
||||
│ OmniRoute — Smart Router │
|
||||
│ RTK + Caveman compression · 18 routing strategies │
|
||||
│ RTK + Caveman compression · 17 routing strategies │
|
||||
│ Circuit breakers · TLS stealth · MCP · A2A · Guardrails │
|
||||
└─────────────────────────┬──────────────────────────────────┘
|
||||
┌─────────────┬────┴────────┬─────────────┐
|
||||
@@ -232,9 +227,9 @@ No combo to create. Set your model to `auto` (or a variant) and OmniRoute builds
|
||||
|
||||
##
|
||||
|
||||
### 🔀 Or build your own — 18 routing strategies
|
||||
### 🔀 Or build your own — 17 routing strategies
|
||||
|
||||
All **18** strategies — mix & match per combo step:
|
||||
All **17** strategies — mix & match per combo step:
|
||||
|
||||
| # | Strategy | What it does |
|
||||
| --- | ------------------- | ---------------------------------------------------------------- |
|
||||
@@ -253,11 +248,10 @@ All **18** strategies — mix & match per combo step:
|
||||
| 13 | `context-relay` | Hand off context across targets for long conversations 🧠 |
|
||||
| 14 | `context-optimized` | Pick the best fit for the current context size |
|
||||
| 15 | `lkgp` | Last-Known-Good Path — sticky to the last successful target |
|
||||
| 16 | `auto` | 12-factor live scoring across every connection 🤖 |
|
||||
| 16 | `auto` | 9-factor live scoring across every connection 🤖 |
|
||||
| 17 | `fusion` | Fan out to a panel of models + a judge synthesizes one answer 🧬 |
|
||||
| 18 | `pipeline` | Chain steps — each target's output feeds the next one 🔗 |
|
||||
|
||||
<sub>The Auto-Combo engine scores every candidate on **12 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
|
||||
<sub>The Auto-Combo engine scores every candidate on **9 factors** (health, quota, cost, latency, success rate, freshness…) — see [`docs/routing/AUTO-COMBO.md`](docs/routing/AUTO-COMBO.md).</sub>
|
||||
|
||||
##
|
||||
|
||||
@@ -314,11 +308,11 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
| Feature | OmniRoute | Other routers |
|
||||
| -------------------------------------- | ------------------------------------------------------------------- | ------------- |
|
||||
| 🌐 Providers | **250** | 20–100 |
|
||||
| 🌐 Providers | **237** | 20–100 |
|
||||
| 🆓 Free providers | **90+ (11 free forever)** | 1–5 |
|
||||
| 🔀 Routing strategies | **18** (priority, weighted, cost-optimized, context-relay, fusion…) | 1–3 |
|
||||
| 🔀 Routing strategies | **17** (priority, weighted, cost-optimized, context-relay, fusion…) | 1–3 |
|
||||
| 🗜️ Token compression | **RTK + Caveman stacked (15–95%)** | None / 20–40% |
|
||||
| 🧰 Built-in MCP server | **94 tools, 3 transports, 30 scopes** | Rare |
|
||||
| 🧰 Built-in MCP server | **95 tools, 3 transports, 30 scopes** | Rare |
|
||||
| 🤝 A2A agent protocol | **6 skills, JSON-RPC 2.0** | None |
|
||||
| 🧠 Memory (FTS5 + vector) | **Yes** | Rare |
|
||||
| 🛡️ Guardrails (PII, injection, vision) | **Yes** | Rare |
|
||||
@@ -337,23 +331,23 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
</div>
|
||||
|
||||
> Recent highlights from **v3.8.20 → v3.8.47**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
> Recent highlights from **v3.8.20 → v3.8.45**. Full history in [`CHANGELOG.md`](CHANGELOG.md).
|
||||
|
||||
- **🗜️ Compression hardening** — a default-on **inflation guard** (discard the stacked result and send the verbatim original whenever compression would _grow_ the prompt), completed **Caveman rule packs** for German / French / Japanese (dedup + ultra) plus a new **Chinese (文言 / wényán) input pack** with zh-vs-ja auto-detection, and **RTK filters for Gradle & .NET (`dotnet`)** build output. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
- **💸 Honest flat-rate cost** — subscription / coding-plan providers (ChatGPT Web, grok-web, the Minimax / Kimi / GLM / Alibaba Coding plans, Xiaomi MiMo…) now read **$0** in cost analytics instead of an inflated per-token estimate, while budget / quota / routing keep estimating unchanged. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **⚖️ Quota-Share routing** — a dedicated combo strategy that spreads load across accounts by _available quota_: Deficit-Round-Robin scheduling, per-connection `max_concurrent` with cooldown-wait queueing, multi-window usage buckets (5h / 7d / per-model), per-(key, model) caps, session stickiness for prompt-cache integrity (now with a per-combo / global disable toggle), and proactive saturation from upstream token-usage headers. → [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)
|
||||
- **🤖 One-command CLI/agent setup** — a dedicated `setup-*` command configures each coding tool to route through OmniRoute (Claude Code, Codex, Cline, Continue, Cursor, Roo Code, Kilo Code, Crush, Goose, Qwen Code, Aider, OpenCode); `omniroute launch` / `omniroute launch-codex` are zero-config launchers. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
- **🛰️ Remote mode** — drive a remote OmniRoute from any machine with scoped access tokens (`omniroute connect` / `omniroute contexts` / `omniroute tokens`), plus an `omniroute login antigravity` helper that runs Google "native/desktop" OAuth on your own machine and pastes a credential blob into a remote/VPS install (where the loopback redirect is unreachable). → [Remote Mode](docs/guides/REMOTE-MODE.md)
|
||||
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing (now with **per-model web-search/web-fetch interception rules**), native **xAI Grok `/v1/responses`** routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). Embeddings-only and rerank-only models (JinaAI, OpenRouter custom, reranker models…) no longer disappear from the combo builder's model picker. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **🗜️ Pluggable compression** — an async pipeline of **10 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), the vendored **GCF codec updated to spec v3.2** (nested flattening — deeply-nested payloads go from ~3% to ~32% compression vs JSON), a new **omniglyph** engine (context-as-image, ~10× fewer tokens on the converted block), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), a unified panel with named profiles + an active-profile selector, an opt-in **per-engine pipeline circuit-breaker**, an opt-in **LLM-tier engine** (a model pass for higher-ratio semantic compression), a **read-lifecycle engine** that collapses superseded file reads, **usage-observed prefix freeze**, a graduated **CCR retrieval-feedback ramp**, a `preserveSystemPrompt` mode enum, and a **drag-reorder pipeline editor** in the studio. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
- **🧭 Smarter auto-routing** — OpenRouter-style `auto/<category>:<tier>` combos (e.g. `auto/coding:fast`, `auto/reasoning:pro`), a **Fusion** strategy (fan out to a panel of models in parallel, then synthesize via a judge), **task-aware routing** (best-fit connection per task type), per-request `X-Route-Model` override, live Arena-ELO + models.dev model intelligence, per-step account allowlists, provider-wildcard combo steps, nested combo-ref execution, sticky weighted selection, `web_search`-aware routing, and **per-request Auto-Combo controls** (`X-OmniRoute-Mode` mode-preset override + `X-OmniRoute-Budget` hard USD cost ceiling, scoped to a single request). → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **🗜️ Pluggable compression** — an async pipeline of **10 composable engines** with Compression Studios, an LLMLingua-2 ONNX engine and a heuristic/SLM two-tier **Ultra**, RTK, delegated Anthropic Context Editing, **Output Styles** (output-axis steering: terse-prose / less-code / terse-CJK), an **adaptive context-budget dial** (escalate only as far as needed to fit the context window), per-request `x-omniroute-compression` control, an opt-in offline eval harness, one-click **Headroom** proxy lifecycle management from the dashboard (Docker sidecar supported), a synthetic **compression playground** (Play lanes + A/B Compare with USD-capped fidelity verdicts), an opt-in **per-step fidelity gate** that rejects a lossy engine before it degrades the prompt, a **best-of-N candidate encoder** (GCF vs TOON — keep whichever is shorter, with an A/B bytes/token table in the studio), **CCR ranged/grep/stats retrieval** (pull an exact byte/line slice or summary of a stored block instead of re-expanding it), a unified panel with named profiles + an active-profile selector, an opt-in **per-engine pipeline circuit-breaker**, an opt-in **LLM-tier engine** (a model pass for higher-ratio semantic compression), a **read-lifecycle engine** that collapses superseded file reads, **usage-observed prefix freeze**, a graduated **CCR retrieval-feedback ramp**, a `preserveSystemPrompt` mode enum, and a **drag-reorder pipeline editor** in the studio. → [Compression](docs/compression/COMPRESSION_ENGINES.md)
|
||||
- **🕵️ Transparent MITM decrypt (TPROXY)** — capture & translate traffic from CLIs that ignore proxy env vars, with a per-SNI certificate authority and a trust-store installer. → [MITM/TPROXY](docs/security/MITM-TPROXY-DECRYPT.md)
|
||||
- **💸 Cost telemetry everywhere** — `X-OmniRoute-*` cost/usage headers on every endpoint (including media), a non-token cost engine, a cache-HIT `X-OmniRoute-Cost-Saved` header, and per-key USD spend quotas. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🧠 Memory you control** — opt-in int8 vector quantization (Qdrant + sqlite-vec), opt-in **typed memory decay** (aged low-value memories fade on a per-type schedule), memory off by default, and a per-request `x-omniroute-no-memory` header. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — a prompt-injection guard across every LLM route (backed by a red-team suite), plus a free DuckDuckGo last-resort web search. → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style audio translation) round out the media API surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), server-side configured-only / available-only filters on the Free Provider Rankings page, and **Traditional Chinese (zh-TW)** localization for the frontend + CLI. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 250-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, **bulk API-key add for Cloudflare Workers AI**, and **OpenVecta** (AI inference gateway). → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, **Webshare** added as a paid fourth source in the free-proxy provider framework, and **shorthand proxy formats + protocol header mode** for bulk proxy import. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath` deployment (`OMNIROUTE_BASE_PATH`, e.g. serving OmniRoute under `/omniroute/`), browser-language auto-detect on first visit, per-API-key device/connection tracking (IP+UA fingerprint, masked, in-memory only), root-less MITM cert trust for user-namespaced containers (`OMNIROUTE_NO_SUDO`), and server-side configured-only / available-only filters on the Free Provider Rankings page. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent (a 4th cloud agent), CodeBuddy CN (`copilot.tencent.com`), a Google Flow video-generation provider, new gateways **DGrid** and **Pioneer AI** (Fastino Labs), inbound **xAI Grok** translators plus **Grok Build (xAI)** with an OAuth import-token flow, GPT-4 / GPT-4o-mini on the GitHub Copilot provider, multi-model **Factory Droid**, **ZenMux Free** (session-cookie free tier), **Alibaba DashScope** text-to-video (`wan2.7-t2v`), a refreshed 237-provider catalog (OrcaRouter, Wafer AI, OpenAdapter, dit.ai, TokenRouter, …), Vertex AI media generation (speech/transcription/music/video), a first-class **Ollama** local-provider card, the **SenseNova** free Token Plan (chat + text-to-image), one-click account import from CLIProxyAPI (`~/.cli-proxy-api/`), **Claude Sonnet 5** wired end-to-end, a new provider wave (**Kenari**, **SumoPod**, **X5Lab**, **Charm Hyper**, **Nube.sh**, **b.ai**, **Qiniu**, **ModelScope**, **Augment/Auggie CLI**, **ClinePass**, NVIDIA NIM image generation), Codex account import from a raw ChatGPT access token, the **Requesty** gateway (BYOK, ~200 free req/day), **Yuanbao (web)** as a cookie-session provider (DeepSeek V3/R1 + Hunyuan), the **Zed** hosted LLM aggregator (OAuth), **Claude 5 Sonnet** on the Claude Web provider, Kiro **adaptive-thinking reasoning** surfaced as `reasoning_content`, and **bulk API-key add for Cloudflare Workers AI**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **⚡ Local performance & infra** — a one-click local Redis launcher (`omniroute redis up`, plus a dashboard Redis panel), one-click **Cloudflare Workers** and **Deno Deploy** relay deployers wired into the proxy pool, a relay-backend selector (`OMNIROUTE_RELAY_BACKEND=ts|bifrost|auto`) so `/v1/relay` stays the stable surface while choosing the fastest backend internally, **Bifrost** (Go AI-gateway) and **Mux** (agent-orchestration daemon) promoted to first-class embedded/supervised services alongside 9Router/CLIProxyAPI, and **Webshare** added as a paid fourth source in the free-proxy provider framework. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -395,11 +389,11 @@ Result: 4 layers of fallback = zero downtime
|
||||
|
||||
<div align="center">
|
||||
|
||||
# 🌐 250 AI Providers — 90+ Free
|
||||
# 🌐 237 AI Providers — 90+ Free
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **250 providers**, **90+ with a free tier**, **11 free forever**.
|
||||
> The most complete catalog of any open-source router: **237 providers**, **90+ with a free tier**, **11 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -544,7 +538,7 @@ Expose OmniRoute over **MCP** or **A2A** and any capable agent gets the keys to
|
||||
| Protocol | Endpoint | Use it for |
|
||||
| ------------------ | ----------------------------------------------- | ------------------------------------------------------ |
|
||||
| 🧰 **MCP (stdio)** | `omniroute --mcp` | Plug into Claude Desktop, Cursor, any MCP client |
|
||||
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **94 tools**, 30 scopes, full audit trail |
|
||||
| 🌊 **MCP (HTTP)** | `http://localhost:20128/api/mcp/stream` | Remote MCP — **95 tools**, 30 scopes, full audit trail |
|
||||
| 📡 **MCP (SSE)** | `http://localhost:20128/api/mcp/sse` | Streaming MCP transport |
|
||||
| 🤝 **A2A** | `http://localhost:20128/.well-known/agent.json` | Agent-to-agent, **JSON-RPC 2.0** + SSE, 6 skills |
|
||||
|
||||
@@ -574,7 +568,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
|
||||
| 1 | **Session-Dedup** | Drops content repeated across turns (content-addressed, cross-turn) |
|
||||
| 2 | **CCR** | Archives large blocks behind retrieve markers, fetched on demand |
|
||||
| 3 | **RTK** | Smart tool-result filtering, dedup & truncation (command-aware) |
|
||||
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays, flat or nested (~30%), via a vendored **GCF** codec (spec v3.2) |
|
||||
| 4 | **Headroom** | Lossless tabular compaction of homogeneous JSON arrays (~30%+) |
|
||||
| 5 | **Relevance** | Extractive sentence scoring against the last user query |
|
||||
| 6 | **Caveman** | Rule-based prose compression (~65–75% on output) |
|
||||
| 7 | **LLMLingua-2** | ML semantic pruning via MobileBERT ONNX — code-safe, async |
|
||||
@@ -881,9 +875,9 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
|
||||
<br/>
|
||||
|
||||
**Routing:** 18 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
|
||||
**Routing:** 17 strategies · task-aware smart routing · thinking budget controls · wildcard routing · system prompt injection.
|
||||
**Compatibility:** OpenAI ↔ Claude ↔ Gemini ↔ Responses API · auto OAuth refresh (PKCE, 8 providers) · multi-account round-robin · Batch + Files API · live OpenAPI 3.0.
|
||||
**Protocols:** MCP (94 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
|
||||
**Protocols:** MCP (95 tools, 3 transports, 30 scopes) · A2A (JSON-RPC 2.0, SSE, 6 skills) · ACP · cloud agents (Codex, Cursor, Devin, Jules).
|
||||
**Plugins:** custom plugin marketplace (system-configured registry URL with SSRF-guarded fetch) · install / enable / disable · Notion + Obsidian knowledge-base integrations (WebDAV file server, vault search, note CRUD).
|
||||
**Embedded services:** one-click install & lifecycle management of local sidecar services (CLIProxy, NineRouter).
|
||||
**Quality & Ops:** built-in **Evals** (golden-set: exact/contains/regex/custom) · guardrails (PII, injection, vision) · health dashboard · p50/p95/p99 telemetry · webhooks · compliance audit.
|
||||
@@ -907,7 +901,7 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
**Will I be charged by OmniRoute?** No — it's free, open-source software on your machine. You only pay paid providers directly. OmniRoute has no billing system.
|
||||
**Are FREE providers really unlimited?** Mostly — Qoder, Pollinations, LongCat, and Cloudflare are free with no per-account credit cap. Kiro is free too but capped at ~50 credits/month per account. Stack multiple free providers in a combo and auto-fallback keeps you serving for $0.
|
||||
**Will compression hurt quality?** No — it only compresses the **input**; code, URLs, JSON are always protected.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 250 providers.
|
||||
**Does it work where AI is blocked?** Yes — 3-level proxy + 1proxy marketplace reach all 237 providers.
|
||||
|
||||
📖 [User Guide](docs/guides/USER_GUIDE.md) · [API Reference](docs/reference/API_REFERENCE.md) · [Environment Config](docs/reference/ENVIRONMENT.md)
|
||||
|
||||
@@ -995,56 +989,66 @@ Compression: aggressive (~50%) → double your free quota · Cost: $0/mo
|
||||
|
||||
### 📘 Getting Started
|
||||
|
||||
- **[User Guide](docs/guides/USER_GUIDE.md)** — Providers, combos, CLI integration, deployment
|
||||
- **[Setup Guide](docs/guides/SETUP_GUIDE.md)** — Full install methods, CLI tool configs, protocol setup, timeout tuning
|
||||
- **[CLI Tools Guide](docs/reference/CLI-TOOLS.md)** — Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot
|
||||
- **[Remote Mode](docs/guides/REMOTE-MODE.md)** — Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens
|
||||
- **[Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md)** — Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles
|
||||
- **[Quick Start](README.md#-quick-start)** — 3-step install → connect → configure
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| [User Guide](docs/guides/USER_GUIDE.md) | Providers, combos, CLI integration, deployment |
|
||||
| [Setup Guide](docs/guides/SETUP_GUIDE.md) | Full install methods, CLI tool configs, protocol setup, timeout tuning |
|
||||
| [CLI Tools Guide](docs/reference/CLI-TOOLS.md) | Per-tool setup for Claude Code, Codex, Cursor, Cline, OpenClaw, Kilo, Copilot |
|
||||
| [Remote Mode](docs/guides/REMOTE-MODE.md) | Drive a remote OmniRoute (VPS) from your laptop CLI via scoped access tokens |
|
||||
| [Claude Code Config](docs/guides/CLAUDE-CODE-CONFIGURATION.md) | Point Claude Code at OmniRoute (local/remote) with `launch` + per-model profiles |
|
||||
| [Quick Start](README.md#-quick-start) | 3-step install → connect → configure |
|
||||
|
||||
### 🔧 Operations & Deployment
|
||||
|
||||
- **[Docker Guide](docs/guides/DOCKER_GUIDE.md)** — Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags
|
||||
- **[Podman Guide](contrib/podman/README.md)** — Quadlet systemd integration, podman-compose, SELinux
|
||||
- **[VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md)** — Complete guide: VM + nginx + Cloudflare setup
|
||||
- **[Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md)** — Deploy to Fly.io with persistent storage
|
||||
- **[Termux Guide](docs/guides/TERMUX_GUIDE.md)** — Run OmniRoute on Android via Termux
|
||||
- **[PWA Guide](docs/guides/PWA_GUIDE.md)** — Progressive Web App install, caching, architecture
|
||||
- **[Uninstall Guide](docs/guides/UNINSTALL.md)** — Clean removal for all install methods
|
||||
- **[Environment Config](docs/reference/ENVIRONMENT.md)** — Complete `.env` variables and references
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------- | -------------------------------------------------------------- |
|
||||
| [Docker Guide](docs/guides/DOCKER_GUIDE.md) | Docker run, Compose profiles, Caddy HTTPS, tunnels, image tags |
|
||||
| [Podman Guide](contrib/podman/README.md) | Quadlet systemd integration, podman-compose, SELinux |
|
||||
| [VM Deployment](docs/ops/VM_DEPLOYMENT_GUIDE.md) | Complete guide: VM + nginx + Cloudflare setup |
|
||||
| [Fly.io Deployment](docs/ops/FLY_IO_DEPLOYMENT_GUIDE.md) | Deploy to Fly.io with persistent storage |
|
||||
| [Termux Guide](docs/guides/TERMUX_GUIDE.md) | Run OmniRoute on Android via Termux |
|
||||
| [PWA Guide](docs/guides/PWA_GUIDE.md) | Progressive Web App install, caching, architecture |
|
||||
| [Uninstall Guide](docs/guides/UNINSTALL.md) | Clean removal for all install methods |
|
||||
| [Environment Config](docs/reference/ENVIRONMENT.md) | Complete `.env` variables and references |
|
||||
|
||||
### 🧠 Features & Architecture
|
||||
|
||||
- **[Architecture](docs/architecture/ARCHITECTURE.md)** — System architecture, data flow, and internals
|
||||
- **[Compression Guide](docs/compression/COMPRESSION_GUIDE.md)** — 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked
|
||||
- **[RTK Compression](docs/compression/RTK_COMPRESSION.md)** — Command-output compression, filters, trust, verify, raw-output recovery
|
||||
- **[Compression Engines](docs/compression/COMPRESSION_ENGINES.md)** — Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces
|
||||
- **[Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md)** — JSON rule-pack schemas for Caveman and RTK filters
|
||||
- **[Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md)** — Language detection and Caveman rule-pack authoring
|
||||
- **[Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)** — Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing
|
||||
- **[Auto-Combo Engine](docs/routing/AUTO-COMBO.md)** — 12-factor scoring, mode packs, self-healing
|
||||
- **[Proxy Guide](docs/ops/PROXY_GUIDE.md)** — 3-level proxy system, 1proxy marketplace, registry CRUD
|
||||
- **[Free Tiers](docs/reference/FREE_TIERS.md)** — 25+ free API providers consolidated directory
|
||||
- **[Features Gallery](docs/guides/FEATURES.md)** — Visual dashboard tour with screenshots
|
||||
- **[Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md)** — Beginner-friendly codebase walkthrough
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| [Architecture](docs/architecture/ARCHITECTURE.md) | System architecture, data flow, and internals |
|
||||
| [Compression Guide](docs/compression/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
|
||||
| [RTK Compression](docs/compression/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
|
||||
| [Compression Engines](docs/compression/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
|
||||
| [Compression Rules Format](docs/compression/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
|
||||
| [Compression Language Packs](docs/compression/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
|
||||
| [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
|
||||
| [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) | 9-factor scoring, mode packs, self-healing |
|
||||
| [Proxy Guide](docs/ops/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
|
||||
| [Free Tiers](docs/reference/FREE_TIERS.md) | 25+ free API providers consolidated directory |
|
||||
| [Features Gallery](docs/guides/FEATURES.md) | Visual dashboard tour with screenshots |
|
||||
| [Codebase Documentation](docs/architecture/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
|
||||
|
||||
### 🤖 Protocols & APIs
|
||||
|
||||
- **[API Reference](docs/reference/API_REFERENCE.md)** — All endpoints with examples
|
||||
- **[OpenAPI Spec](docs/openapi.yaml)** — OpenAPI 3.0 specification
|
||||
- **[MCP Server](open-sse/mcp-server/README.md)** — 95 MCP tools, IDE configs, Python/TS/Go clients
|
||||
- **[MCP Server Guide](docs/frameworks/MCP-SERVER.md)** — MCP installation, transports, and tool reference
|
||||
- **[A2A Server](src/lib/a2a/README.md)** — JSON-RPC 2.0 protocol, skills, streaming, task mgmt
|
||||
- **[A2A Server Guide](docs/frameworks/A2A-SERVER.md)** — A2A agent card, tasks, skills, and streaming
|
||||
| Document | Description |
|
||||
| ------------------------------------------------- | --------------------------------------------------- |
|
||||
| [API Reference](docs/reference/API_REFERENCE.md) | All endpoints with examples |
|
||||
| [OpenAPI Spec](docs/openapi.yaml) | OpenAPI 3.0 specification |
|
||||
| [MCP Server](open-sse/mcp-server/README.md) | 95 MCP tools, IDE configs, Python/TS/Go clients |
|
||||
| [MCP Server Guide](docs/frameworks/MCP-SERVER.md) | MCP installation, transports, and tool reference |
|
||||
| [A2A Server](src/lib/a2a/README.md) | JSON-RPC 2.0 protocol, skills, streaming, task mgmt |
|
||||
| [A2A Server Guide](docs/frameworks/A2A-SERVER.md) | A2A agent card, tasks, skills, and streaming |
|
||||
|
||||
### 📋 Project & Quality
|
||||
|
||||
- **[Contributing](CONTRIBUTING.md)** — Development setup and guidelines
|
||||
- **[Changelog](CHANGELOG.md)** — Full per-version release history
|
||||
- **[Security Policy](SECURITY.md)** — Vulnerability reporting and security practices
|
||||
- **[i18n Guide](docs/guides/I18N.md)** — 40+ language support, translation workflow, RTL
|
||||
- **[Release Checklist](docs/ops/RELEASE_CHECKLIST.md)** — Pre-release validation steps
|
||||
- **[Coverage Plan](docs/ops/COVERAGE_PLAN.md)** — Test coverage strategy and 21,000+ test suite
|
||||
| Document | Description |
|
||||
| -------------------------------------------------- | ----------------------------------------------- |
|
||||
| [Contributing](CONTRIBUTING.md) | Development setup and guidelines |
|
||||
| [Changelog](CHANGELOG.md) | Full per-version release history |
|
||||
| [Security Policy](SECURITY.md) | Vulnerability reporting and security practices |
|
||||
| [i18n Guide](docs/guides/I18N.md) | 40+ language support, translation workflow, RTL |
|
||||
| [Release Checklist](docs/ops/RELEASE_CHECKLIST.md) | Pre-release validation steps |
|
||||
| [Coverage Plan](docs/ops/COVERAGE_PLAN.md) | Test coverage strategy and 21,000+ test suite |
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -1146,13 +1150,14 @@ gh release create v3.8.2 --title "v3.8.2" --generate-notes
|
||||
|
||||
## 📊 Stars
|
||||
|
||||
<a href="https://www.star-history.com/?repos=diegosouzapw%2FOmniRoute&type=date&legend=top-left">
|
||||
<a href="https://www.star-history.com/?repos=diegosouzapw%2Fomniroute&type=date&legend=top-left">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&theme=dark&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/OmniRoute&type=date&legend=top-left&sealed_token=XP_ycEjv7s31p1edvhsMOXry51OWYsUjDRWjflSG7jQKRpO9hPGg7i_EHvwhI6QtrARTMH-YGjJhi8sumRYflEJD0DPlH_MMHjizhBYCX8fbHFrHEiNvVA" />
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&theme=dark&legend=top-left" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
|
||||
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=diegosouzapw/omniroute&type=date&legend=top-left" />
|
||||
</picture>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
@@ -1206,7 +1211,7 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
|
||||
| Project | ⭐ | How it inspired OmniRoute |
|
||||
| ---------------------------------------------------------------------------------------------- | ----: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **[TOON](https://github.com/toon-format/toon)** · toon-format | 24.7k | Token-Oriented Object Notation — its columnar, header-plus-rows model shaped our tabular compaction stage. |
|
||||
| **[GCF – Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | First inspired our tabular compaction stage; now its zero-dependency, lossless generic-profile encoder is **vendored directly** as the Headroom codec (MIT, SPDX-marked), current with GCF spec v3.2. |
|
||||
| **[GCF – Graph Compact Format](https://github.com/blackwell-systems/gcf)** · Blackwell Systems | 14 | Schema-aware "JSON for LLMs" notation — co-inspired our lossless homogeneous-array compaction with `[N rows]` markers. |
|
||||
| **[token-optimizer-mcp](https://github.com/ooples/token-optimizer-mcp)** · ooples | 421 | Brotli/SQLite cache + per-session context-delta — inspired our `session-dedup` engine. |
|
||||
| **[token-savior](https://github.com/Mibayy/token-savior)** · Mibayy | 1.0k | Bash-output compaction + MCP profiles — inspired our compression bail-out discipline and MCP tool-manifest reduction. |
|
||||
| **[token-saver](https://github.com/ppgranger/token-saver)** · ppgranger | 110 | Content-aware, per-file-type output compression with failure-aware bail-out — validated our per-type dispatch and minimum-gain skip. |
|
||||
@@ -1250,12 +1255,6 @@ OmniRoute stands on the shoulders of giants. It started as a fork of **[9router]
|
||||
| ------------------------------------------------------------------------------------------- | --: | ------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| **[awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)** · tldrsec | 708 | A curated list of secure-by-default libraries that guides our security choices (Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink). |
|
||||
|
||||
### 🧭 Complementary tools
|
||||
|
||||
| Project | How it composes with OmniRoute |
|
||||
| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| **[CodeWebChat](https://github.com/robertpiosik/CodeWebChat)** · robertpiosik | Editor-side companion — VS Code + browser extension that autofills 15+ chatbot web UIs with editor context. Owns the free-web-UI rail alongside OmniRoute's API rail; can point its API mode at OmniRoute. |
|
||||
|
||||
## ❤️ Support
|
||||
|
||||
OmniRoute is free and open source, built and maintained in the open. If it saves you time or money, consider supporting development:
|
||||
|
||||
@@ -15,11 +15,7 @@ function readCache() {
|
||||
try {
|
||||
const raw = JSON.parse(readFileSync(cachePath(), "utf8"));
|
||||
if (raw && typeof raw.ts === "number" && Date.now() - raw.ts < CACHE_TTL_MS) return raw;
|
||||
} catch (err) {
|
||||
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
|
||||
console.error("[omniroute completion] readCache failed:", err?.message ?? err);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -45,20 +41,12 @@ async function refreshCache(opts = {}) {
|
||||
const j = await mr.value.json();
|
||||
models = (Array.isArray(j) ? j : j.data || []).map((m) => m.id).filter(Boolean);
|
||||
}
|
||||
} catch (err) {
|
||||
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
|
||||
console.error("[omniroute completion] refreshCache failed:", err?.message ?? err);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
const data = { combos, providers, models, ts: Date.now() };
|
||||
try {
|
||||
mkdirSync(dirname(cachePath()), { recursive: true });
|
||||
writeFileSync(cachePath(), JSON.stringify(data));
|
||||
} catch (err) {
|
||||
if (process.env.OMNIROUTE_DEBUG_COMPLETION) {
|
||||
console.error("[omniroute completion] writeCache failed:", err?.message ?? err);
|
||||
}
|
||||
}
|
||||
} catch {}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ async function restCompressionStatus() {
|
||||
const combosBody = combosRes.ok ? await combosRes.json() : { combos: [] };
|
||||
const analytics = analyticsRes && analyticsRes.ok ? await analyticsRes.json() : null;
|
||||
return {
|
||||
strategy: settings.defaultMode || "standard",
|
||||
engine: settings.engine ?? null,
|
||||
settings,
|
||||
combos: combosBody.combos ?? combosBody,
|
||||
analytics,
|
||||
@@ -33,10 +33,7 @@ async function restCompressionStatus() {
|
||||
|
||||
async function restCompressionConfigure(config) {
|
||||
const body = { ...config };
|
||||
if (body.strategy) {
|
||||
body.defaultMode = body.strategy === "caveman" ? "standard" : normalizeEngine(body.strategy);
|
||||
delete body.strategy;
|
||||
}
|
||||
if (body.engine) body.engine = normalizeEngine(body.engine);
|
||||
const res = await apiFetch("/api/settings/compression", { method: "PUT", body });
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
@@ -46,10 +43,9 @@ async function restCompressionConfigure(config) {
|
||||
}
|
||||
|
||||
async function restSetEngine(name) {
|
||||
const normalized = normalizeEngine(name);
|
||||
const res = await apiFetch("/api/settings/compression", {
|
||||
method: "PUT",
|
||||
body: { defaultMode: normalized === "caveman" ? "standard" : normalized },
|
||||
body: { engine: normalizeEngine(name) },
|
||||
});
|
||||
if (!res.ok) {
|
||||
process.stderr.write(`Error: ${res.status}\n`);
|
||||
@@ -107,11 +103,7 @@ export async function runCompressionStatus(opts, cmd) {
|
||||
|
||||
export async function runCompressionConfigure(opts, cmd) {
|
||||
const config = {};
|
||||
// #6571 — both the MCP tool schema (compressionConfigureInput) and
|
||||
// handleCompressionConfigure expect `strategy`, not `engine`; a non-strict
|
||||
// MCP schema silently strips an unrecognized `engine` key on the primary
|
||||
// (MCP-mounted) path, so this must be `strategy` on both paths.
|
||||
if (opts.engine) config.strategy = normalizeEngine(opts.engine);
|
||||
if (opts.engine) config.engine = opts.engine;
|
||||
if (opts.cavemanAggressiveness !== undefined)
|
||||
config.caveman = { aggressiveness: opts.cavemanAggressiveness };
|
||||
if (opts.rtkBudget !== undefined) config.rtk = { tokenBudget: opts.rtkBudget };
|
||||
@@ -171,7 +163,7 @@ export function registerCompression(program) {
|
||||
engine.command("set <name>").action(runCompressionEngineSet);
|
||||
engine.command("get").action(async (opts, cmd) => {
|
||||
const data = await mcpCall("omniroute_compression_status", {}, restCompressionStatus);
|
||||
process.stdout.write(`${data.strategy ?? "(default)"}\n`);
|
||||
process.stdout.write(`${data.engine ?? "(default)"}\n`);
|
||||
});
|
||||
|
||||
const combos = cmp.command("combos").description(t("compression.combos.description"));
|
||||
|
||||
@@ -48,11 +48,7 @@ export async function runHealthCommand(opts = {}) {
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await apiFetch("/api/monitoring/health", {
|
||||
retry: false,
|
||||
timeout: 5000,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
@@ -70,22 +66,29 @@ export async function runHealthCommand(opts = {}) {
|
||||
if (health.uptime) console.log(t("health.uptime", { uptime: health.uptime }));
|
||||
if (health.version) console.log(` Version: ${health.version}`);
|
||||
|
||||
if (health.activeConnections !== undefined) {
|
||||
console.log(t("health.requests", { count: health.activeConnections }));
|
||||
if (health.requests !== undefined) {
|
||||
console.log(t("health.requests", { count: health.requests }));
|
||||
}
|
||||
|
||||
if (health.circuitBreakers && opts.verbose) {
|
||||
if (health.breakers && opts.verbose) {
|
||||
console.log("\n \x1b[1mCircuit Breakers\x1b[0m");
|
||||
const { open = 0, halfOpen = 0, closed = 0 } = health.circuitBreakers;
|
||||
console.log(` \x1b[32m● closed\x1b[0m ${closed}`);
|
||||
console.log(` \x1b[33m○ half-open\x1b[0m ${halfOpen}`);
|
||||
console.log(` \x1b[31m○ open\x1b[0m ${open}`);
|
||||
for (const [name, status] of Object.entries(health.breakers)) {
|
||||
const state =
|
||||
status.state === "closed" ? "\x1b[32m● closed\x1b[0m" : "\x1b[33m○ open\x1b[0m";
|
||||
console.log(` ${name.padEnd(20)} ${state}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (opts.verbose && health.memoryUsage) {
|
||||
if (health.cache && opts.verbose) {
|
||||
console.log("\n \x1b[1mCache\x1b[0m");
|
||||
console.log(` Semantic hits: ${health.cache.semanticHits || 0}`);
|
||||
console.log(` Signature hits: ${health.cache.signatureHits || 0}`);
|
||||
}
|
||||
|
||||
if (opts.verbose && health.memory) {
|
||||
console.log("\n \x1b[1mMemory\x1b[0m");
|
||||
console.log(` RSS: ${health.memoryUsage.rss || "N/A"}`);
|
||||
console.log(` Heap used: ${health.memoryUsage.heapUsed || "N/A"}`);
|
||||
console.log(` RSS: ${health.memory.rss || "N/A"}`);
|
||||
console.log(` Heap used: ${health.memory.heapUsed || "N/A"}`);
|
||||
}
|
||||
|
||||
return 0;
|
||||
@@ -97,17 +100,13 @@ export async function runHealthCommand(opts = {}) {
|
||||
|
||||
export async function runHealthComponentsCommand(opts = {}) {
|
||||
try {
|
||||
const res = await apiFetch("/api/monitoring/health", {
|
||||
retry: false,
|
||||
timeout: 5000,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
const res = await apiFetch("/api/health", { retry: false, timeout: 5000, acceptNotOk: true });
|
||||
if (!res.ok) {
|
||||
console.error(`HTTP ${res.status}`);
|
||||
return 1;
|
||||
}
|
||||
const health = await res.json();
|
||||
const components = health.components || health.circuitBreakers || {};
|
||||
const components = health.components || health.breakers || {};
|
||||
for (const [name, info] of Object.entries(components)) {
|
||||
const status =
|
||||
typeof info === "object" ? info.state || info.status || "unknown" : String(info);
|
||||
|
||||
@@ -351,35 +351,11 @@ async function runWithSupervisor(
|
||||
if (up) {
|
||||
if (useTray) await maybeStartTray(dashboardPort, apiPort, supervisor);
|
||||
onReady(dashboardPort, apiPort, noOpen, startedAt);
|
||||
} else {
|
||||
reportReadinessTimeout(dashboardPort, supervisor);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// #6321: waitForServer resolving `false` used to fall through silently — the CLI
|
||||
// printed the banner + "⏳ Starting server..." and then produced ZERO further
|
||||
// output forever, even though the child process may well have crashed or be
|
||||
// stuck (issue reports show the server sometimes actually comes up later, or is
|
||||
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
|
||||
// plus whatever stdout/stderr the child buffered instead of going silent.
|
||||
export function reportReadinessTimeout(dashboardPort, supervisor) {
|
||||
console.error(
|
||||
`\n\x1b[33m⚠ Server did not respond within 60s.\x1b[0m It may still be starting, or may` +
|
||||
` have failed silently.`
|
||||
);
|
||||
console.error(` Try: curl -I http://localhost:${dashboardPort}/api/monitoring/health`);
|
||||
console.error(` Or: rerun with \x1b[36m--log\x1b[0m to see live server output.\n`);
|
||||
|
||||
const recentLog = supervisor?.getRecentLog?.() ?? [];
|
||||
if (recentLog.length) {
|
||||
console.error("--- Recent server output ---");
|
||||
recentLog.forEach((l) => console.error(l));
|
||||
console.error("--- End recent output ---\n");
|
||||
}
|
||||
}
|
||||
|
||||
let _killTray = null;
|
||||
function killTrayIfActive() {
|
||||
if (_killTray) {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -37,7 +37,6 @@ function inferSchema(sample) {
|
||||
function formatCell(v, col) {
|
||||
if (v == null) return "";
|
||||
if (col.formatter) return col.formatter(v);
|
||||
if (typeof v === "object") return JSON.stringify(v);
|
||||
return String(v);
|
||||
}
|
||||
|
||||
|
||||
@@ -52,31 +52,6 @@ export function hasModule(name) {
|
||||
return existsSync(join(runtimeModules(), name, "package.json"));
|
||||
}
|
||||
|
||||
/**
|
||||
* Probe whether a native addon (.node) file can actually be dlopen'd by the Node runtime that
|
||||
* is going to load it. Runs in a throwaway subprocess so a real ABI mismatch (which can segfault
|
||||
* the process instead of throwing) never takes down the caller — only the probe subprocess.
|
||||
*/
|
||||
function probeNativeBinaryLoadable(binary) {
|
||||
try {
|
||||
const res = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
"-e",
|
||||
"try { require(process.argv[1]); process.exit(0); } catch (e) { process.exit(1); }",
|
||||
binary,
|
||||
],
|
||||
{ timeout: 10_000, stdio: "ignore" }
|
||||
);
|
||||
// status === 0 means require() (and therefore dlopen) succeeded. Anything else — a thrown
|
||||
// ERR_DLOPEN_FAILED/NODE_MODULE_VERSION mismatch (status 1) or a crash (status null with a
|
||||
// signal, e.g. SIGSEGV) — means the binary is not safe to load.
|
||||
return res.status === 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export function isBetterSqliteBinaryValid() {
|
||||
const binary = join(
|
||||
runtimeModules(),
|
||||
@@ -93,18 +68,10 @@ export function isBetterSqliteBinaryValid() {
|
||||
closeSync(fd);
|
||||
const magic = buf.toString("hex");
|
||||
const os = platform();
|
||||
let formatOk;
|
||||
if (os === "linux") formatOk = magic.startsWith("7f454c46"); // ELF
|
||||
else if (os === "darwin")
|
||||
formatOk = magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
|
||||
else if (os === "win32") formatOk = magic.startsWith("4d5a"); // PE/MZ
|
||||
else formatOk = true;
|
||||
if (!formatOk) return false;
|
||||
// File-format magic bytes alone do not guarantee the binary was built for the Node ABI
|
||||
// (NODE_MODULE_VERSION) that will load it — a stale/foreign-ABI binary passes the header
|
||||
// check and then crashes (segfault) on load instead of triggering a rebuild. Actually
|
||||
// attempt to load it, isolated in a subprocess.
|
||||
return probeNativeBinaryLoadable(binary);
|
||||
if (os === "linux") return magic.startsWith("7f454c46"); // ELF
|
||||
if (os === "darwin") return magic.startsWith("cffaedfe") || magic.startsWith("cefaedfe"); // Mach-O
|
||||
if (os === "win32") return magic.startsWith("4d5a"); // PE/MZ
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -35,32 +35,22 @@ export class ServerSupervisor {
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value). The
|
||||
// calibrated heap is already carried by env.NODE_OPTIONS either way.
|
||||
const heapArgs = buildNodeHeapArgs(process.env, this.memoryLimit);
|
||||
// #6321: stdout used to be discarded (`"ignore"`) whenever `--log`/OMNIROUTE_SHOW_LOG
|
||||
// wasn't set (the default) — any debug/pino output written to stdout vanished
|
||||
// silently, so a boot that never becomes ready looked like a dead hang with zero
|
||||
// output even at APP_LOG_LEVEL=debug. Pipe stdout too and buffer it alongside
|
||||
// stderr so a readiness timeout can surface what the child actually printed.
|
||||
this.child = spawn("node", [...heapArgs, this.serverPath], {
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
stdio: showLog ? "inherit" : ["ignore", "ignore", "pipe"],
|
||||
});
|
||||
|
||||
writePidFile("server", this.child.pid);
|
||||
|
||||
const bufferOutput = (data) => {
|
||||
const lines = data.toString().split("\n").filter(Boolean);
|
||||
this.crashLog.push(...lines);
|
||||
if (this.crashLog.length > CRASH_LOG_LINES) {
|
||||
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
|
||||
}
|
||||
};
|
||||
|
||||
if (this.child.stdout) {
|
||||
this.child.stdout.on("data", bufferOutput);
|
||||
}
|
||||
if (this.child.stderr) {
|
||||
this.child.stderr.on("data", bufferOutput);
|
||||
this.child.stderr.on("data", (data) => {
|
||||
const lines = data.toString().split("\n").filter(Boolean);
|
||||
this.crashLog.push(...lines);
|
||||
if (this.crashLog.length > CRASH_LOG_LINES) {
|
||||
this.crashLog = this.crashLog.slice(-CRASH_LOG_LINES);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
this.child.on("error", (err) => this.handleExit(-1, err));
|
||||
@@ -117,12 +107,6 @@ export class ServerSupervisor {
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// #6321: exposes the buffered stdout+stderr lines so a caller (e.g. a readiness
|
||||
// timeout) can print what the child actually said instead of silence.
|
||||
getRecentLog() {
|
||||
return [...this.crashLog];
|
||||
}
|
||||
|
||||
dumpCrashLog() {
|
||||
console.error("\n--- Server crash log ---");
|
||||
this.crashLog.forEach((l) => console.error(l));
|
||||
|
||||
@@ -65,52 +65,34 @@ export function sleep(ms) {
|
||||
// cold start due to filesystem watchers, antivirus, etc.) get a working
|
||||
// "server ready" signal instead of a phantom timeout while the server is
|
||||
// still booting. TCP fallback marks the server as ready when the port
|
||||
// has been listening for >= 3s consecutively AND the health route is
|
||||
// actively rejecting/resetting connections fast (route not mounted yet,
|
||||
// but the HTTP server is clearly alive and responsive) — never for a
|
||||
// socket that merely accepts TCP and then hangs without ever completing
|
||||
// a single request (#6800: that's a still-booting/CPU-bound process, not
|
||||
// a "route not mounted" gap, and must NOT be reported as ready).
|
||||
// has been listening for >= 3s consecutively but /api/monitoring/health
|
||||
// has not yet been mounted — common during dev cold start.
|
||||
export async function waitForServer(port, timeout = 60000) {
|
||||
const start = Date.now();
|
||||
let tcpListeningSince = null;
|
||||
while (Date.now() - start < timeout) {
|
||||
const outcome = await pollHealthOnce(port);
|
||||
if (outcome === "ready") return true;
|
||||
if (outcome === "fast-reject") {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
if (res.ok) return true;
|
||||
// Server responded but health endpoint is not ready yet — keep
|
||||
// polling, but the fact that we got a response means TCP is open.
|
||||
if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
||||
if (Date.now() - tcpListeningSince >= 3000) return true;
|
||||
} else {
|
||||
// "hanging" (request timed out with no response at all) or
|
||||
// "not-listening" — neither counts toward the grace window.
|
||||
tcpListeningSince = null;
|
||||
} catch {
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
if (listening) {
|
||||
if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
||||
if (Date.now() - tcpListeningSince >= 3000) return true;
|
||||
} else {
|
||||
tcpListeningSince = null;
|
||||
}
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Polls /api/monitoring/health once and classifies the outcome:
|
||||
// - "ready": got a 2xx HTTP response.
|
||||
// - "fast-reject": got a non-2xx HTTP response, or the connection was
|
||||
// actively refused/reset (not a timeout) — the HTTP server is alive and
|
||||
// answering quickly, just not routing this endpoint yet (#2460).
|
||||
// - "hanging": the request timed out waiting for any response — the
|
||||
// process accepted the TCP connection but never answered (#6800).
|
||||
// - "not-listening": nothing is accepting connections on the port at all.
|
||||
async function pollHealthOnce(port) {
|
||||
try {
|
||||
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return res.ok ? "ready" : "fast-reject";
|
||||
} catch (err) {
|
||||
if (err?.name === "TimeoutError") return "hanging";
|
||||
const listening = await isPortListening(port).catch(() => false);
|
||||
return listening ? "fast-reject" : "not-listening";
|
||||
}
|
||||
}
|
||||
|
||||
async function isPortListening(port) {
|
||||
const net = await import("node:net");
|
||||
return new Promise((resolve) => {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
# changelog.d/ — changelog fragments
|
||||
|
||||
**A PR never edits `CHANGELOG.md` directly during the cycle.** Instead it adds ONE new
|
||||
file here — its changelog entry as a *fragment*. Two PRs never touch the same file, so
|
||||
changelog merge conflicts (the "CHANGELOG-eat" cascade that forced a re-sync push + full
|
||||
CI re-run after every sibling merge) are structurally impossible.
|
||||
|
||||
## Convention
|
||||
|
||||
| Directory | Aggregates under |
|
||||
| -------------- | ----------------------- |
|
||||
| `features/` | `### ✨ New Features` |
|
||||
| `fixes/` | `### 🐛 Bug Fixes` |
|
||||
| `maintenance/` | `### 📝 Maintenance` |
|
||||
|
||||
- **Filename**: `<PR-number>-<short-slug>.md` (e.g. `fixes/6700-dockerfile-better-sqlite3.md`).
|
||||
The PR number prefix keeps aggregation order deterministic.
|
||||
- **Content**: the exact bullet line(s) that should land in `CHANGELOG.md`, starting with
|
||||
`- `. Multi-line (continuation) bullets are fine. Keep the repo's credit format:
|
||||
`(#PR — thanks @user)`.
|
||||
- One fragment per PR (rarely more, e.g. a PR that both fixes and adds).
|
||||
|
||||
## Example
|
||||
|
||||
`changelog.d/fixes/6496-cloudflare-relay-worker-syntax.md`:
|
||||
|
||||
```markdown
|
||||
- **fix(providers):** Cloudflare relay Worker deploys use Service Worker syntax with `body_part` metadata ([#6496](https://github.com/diegosouzapw/OmniRoute/pull/6496)) — thanks @SeaXen
|
||||
```
|
||||
|
||||
## Aggregation
|
||||
|
||||
The release captain (or `/generate-release`) folds all fragments into `CHANGELOG.md` and
|
||||
deletes them:
|
||||
|
||||
```bash
|
||||
node scripts/release/aggregate-changelog.mjs # write + delete fragments
|
||||
node scripts/release/aggregate-changelog.mjs --dry-run # preview only
|
||||
```
|
||||
|
||||
Fragment well-formedness is enforced by `npm run check:changelog-integrity` (the same
|
||||
gate that guards against CHANGELOG-eat for legacy direct edits).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(ws):** the live-dashboard WebSocket server now auto-starts in-process (via `instrumentation-node.ts`) across every deployment mode — dev, production, Docker, Electron — with no separate sidecar script; the default WS port moved from 20129 to **20132** to avoid colliding with `API_PORT` in split-port setups, the deprecated `OMNIROUTE_DISABLE_LIVE_WS` env was consolidated into `OMNIROUTE_ENABLE_LIVE_WS` (default enabled), and the WS path is now derived from `NEXT_PUBLIC_LIVE_WS_PUBLIC_URL`'s pathname (`/live-ws` fallback) (#6072 — thanks @ianriizky).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(compression):** new **omniglyph** engine (context-as-image) — renders system prompt, tool docs, and dense history as compact PNG pages the model reads instead of text (~10× fewer tokens on the converted block; 59–70% end-to-end measured). Works stacked with RTK/Caveman (`stackPriority: 90`) or standalone (`mode: omniglyph`); restricted to Claude Fable 5 over the direct Anthropic route, fail-closed gates with `skip:<reason>` techniques, preview (`stable: false`, off by default) (#6556). Dependency bumped to `omniglyph@^1.0.2` for upstream ReDoS fixes (#6661).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sandbox):** the skill sandbox gained a container-provider abstraction that auto-detects and uses the best native runtime per host — Apple Container (macOS 26+), WSL container (`wslc.exe`), OrbStack, Podman — instead of hardcoding `docker run`, removing the Docker Desktop requirement on macOS/Windows (#6611 — thanks @KooshaPari).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** skip `thinkingConfig` for Gemma models on the OpenAI→Gemini path so OpenAI-shape clients no longer get a 400 from Vertex. (thanks @chy1211)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(xai):** route xAI clients to Grok's native `/v1/responses` endpoint instead of the chat-completions bridge. (thanks @ryanngit)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(models):** add a Settings → AI "Model Overrides" UI plus `/api/model-capability-overrides` CRUD and a `model_capability_overrides` table, letting operators set a manual max-output-token override per provider/model (#6727 — thanks @xz-dev).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(resilience):** operator-configurable account rotation policy — a new `rotationConfig` layer lets operators tune how connections rotate on failure, wired into `accountFallback` (#6763 — thanks @artickc).
|
||||
@@ -1 +0,0 @@
|
||||
- **chore(cursor): add Grok 4.5 effort/fast model IDs** (#6774 — thanks @andrewmunsell).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(codex):** Codex provider model discovery now fetches the live catalog from `chatgpt.com/backend-api/codex/models` using Codex-shaped headers, falling back to a GitHub-hosted model manifest and then to the local static catalog when the live/GitHub sources are unavailable or return an unexpected shape — new `src/app/api/providers/[id]/models/discovery/codex.ts` (normalization, version-gating, merge/enrich against the local catalog) covered by `tests/unit/provider-models-discovery-split.test.ts` and `tests/unit/provider-models-route-codex.test.ts` (#6776 — thanks @JxnLexn).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(cursor):** register the Opus 4.8, Fable 5, and Sonnet 5 model families for the Cursor Agent provider so the latest Claude/Fable model ids route correctly (#6779 — thanks @andrewmunsell).
|
||||
@@ -1 +0,0 @@
|
||||
- **Changelog fragments (`changelog.d/`)**: PRs now add their changelog entry as a new fragment file (`changelog.d/{features|fixes|maintenance}/<PR>-<slug>.md`) instead of editing `CHANGELOG.md` — two PRs never touch the same file, structurally eliminating the CHANGELOG-eat merge conflicts that forced a re-sync push + full CI re-run after every sibling merge (O(N²) CI runs in a merge-storm). `scripts/release/aggregate-changelog.mjs` (npm run changelog:aggregate) folds fragments into the living section at release reconciliation, and `check:changelog-integrity` now also validates fragment well-formedness. Regression guard: `tests/unit/changelog-fragments.test.ts`.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(proxy):** add a latency-optimized proxy rotation strategy that ranks pool entries by measured round-trip latency, extending the existing round-robin/random/sticky proxy-pool selection (#6798 — thanks @iamraydoan).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(fusion):** the fusion judge may now draw on its own knowledge and override the panel when every panel answer is wrong or incomplete, instead of being restricted to synthesizing only from panel output (#6804 — thanks @chirag127).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(dashboard):** search box on the Playground's raw model `<select>` (#4086) — the shared `ModelSelectModal` (combo builder + CLI-code cards) already had search, but Playground's `StudioConfigPane` model dropdown stayed a flat unsearchable list, unusable once a provider like OpenRouter contributed 50+ models. Typing now filters the dropdown (Turkish-safe accent/case-insensitive match via `matchesSearch`), while the currently selected model always stays pinned in the list even if it no longer matches the query, so typing never silently swaps the active selection. Reuses the existing `common.search` i18n key (already translated in all 42 locales) — no new translation key needed. Regression guard: `tests/unit/playground-model-selection-3731.test.ts` (`filterModelsByQuery`), `tests/unit/ui/playground-model-search-4086.test.tsx`.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** Antigravity/agy quota widget now surfaces the **weekly** window alongside the existing per-model 5-hour window ([#4017](https://github.com/diegosouzapw/OmniRoute/issues/4017)) — the weekly limit isn't part of the per-model `retrieveUserQuota` response the fetcher already calls; it only appears in a separate, undocumented `retrieveUserQuotaSummary` RPC that groups models into families ("Gemini Models", "Claude and GPT models") with one weekly bucket per family. A new `usage/antigravityWeeklyQuota.ts` leaf fetches that RPC (cached, best-effort — a failure or unavailable RPC never breaks the existing per-model quotas) and parses the weekly bucket per group into `gemini_weekly`/`claude_gpt_weekly` quota entries, merged into the same `quotas` map the widget already renders generically. Regression guard: `tests/unit/antigravity-weekly-quota-4017.test.ts` (bucket parsing, the alternate `quotaSummary`-nested envelope, and end-to-end merge via `getUsageForProvider`).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(codex):** Codex CLI compatibility shim — the Responses API `response.created`/`response.in_progress`/`response.completed` payloads now carry a `model` field (previously absent), and for Codex-CLI-originated requests it echoes the client-requested, effort-suffixed model id (e.g. `gpt-5.5-xhigh`) instead of the bare upstream id (`gpt-5.5`), so the Codex CLI status line/model button shows the active reasoning effort ([#3697](https://github.com/diegosouzapw/OmniRoute/issues/3697)). `openaiToOpenAIResponsesResponse` (`open-sse/translator/response/openai-responses.ts`) now threads the upstream model into the Responses event objects; a new `isCodexOriginatedHeaders()` (`open-sse/config/codexIdentity.ts`, reusing PR #3481's `originator`/User-Agent detection) makes chatCore's existing opt-in `echoRequestedModelName` (#1311) model-echo pipeline fire automatically for Codex clients regardless of the setting, detected by request headers so it still applies when `codex/gpt-5.5-xhigh` is routed through a combo to a non-codex upstream; `echoModelInObject`/`echoModelInSseLine` (`open-sse/services/responseModelEcho.ts`) now also rewrite the nested `response.model` field the Responses API uses. `/v1/models` still returns `models: []` for Codex (unchanged). Regression guard: `tests/unit/codex-effort-model-echo-3697.test.ts`.
|
||||
@@ -1 +0,0 @@
|
||||
- **Z.ai Web (free web-session provider)**: new `zai-web` web-cookie provider drives the free chat.z.ai consumer chat UI via a pasted browser session cookie, distinct from the existing API-key `zai`/`glm`/`glm-cn`/`glmt` providers (`api.z.ai`) — modeled on the `doubao-web`/`venice-web` cookie executors and the pre-existing `chatglm-web` credential requirement/token-extraction entries. `ZaiWebExecutor` (`open-sse/executors/zai-web.ts`) posts to `chat.z.ai/api/chat/completions` with the cookie forwarded both as `Cookie` and as `Authorization: Bearer <token>`, and normalizes both z.ai's internal `delta_content`/`phase` SSE envelope and a pass-through OpenAI-shaped `choices[].delta` frame into standard chat-completion chunks. Registered in `WEB_COOKIE_PROVIDERS`, `WEB_SESSION_CREDENTIAL_REQUIREMENTS`, the provider registry (`zai-web` entry, GLM-4.6/4.5/4.5V models), and `tokenExtractionConfig.ts` for in-app cookie capture. Regression guard: `tests/unit/executor-zai-web.test.ts` (16 tests — token extraction, frame parsing for both SSE shapes, streaming and non-streaming aggregation, error paths). (#4056)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(compression):** update the vendored **GCF** codec behind the **Headroom** engine to spec **v3.2 (nested flattening)** ([#6837](https://github.com/diegosouzapw/OmniRoute/issues/6837)). Homogeneous arrays whose rows carry nested objects/arrays now tabularize via `>`-prefixed path fields instead of a low-yield per-row fallback, so nested MCP tool-result rows (`meta:{...}`, `tags:[...]`) compact like flat rows. On representative shapes the update takes deeply-nested payloads the old codec left near-uncompressed from ~3% to ~32% vs JSON (k8s pods, `cl100k_base`), with shallow-nested rows seeing a small bump and flat arrays unchanged. Re-vendored from current gcf-typescript (zero runtime deps, MIT, SPDX-marked, generic-profile only); also folds in the `[N]:` inline-array quoting fix and canonical decimal formatting. Round-trip stays lossless (order-insensitive), and the decoder is hardened against prototype pollution (a `__proto__`/`constructor` path segment never mutates `Object.prototype`, and keys shadowing built-ins like `toString` now round-trip correctly instead of misparsing). Regression guard: `tests/unit/compression/headroom-smartcrusher.test.ts` (deep-nested + prototype-pollution cases).
|
||||
@@ -1,2 +0,0 @@
|
||||
- **feat(providers):** Add GPT-5.6 support across OpenAI API, Codex, and ChatGPT Web, including Codex Max/Ultra efforts, VS Code metadata, Fast-tier credit accounting, curated live discovery, the Codex 0.144.1 client identity, and correct chat routing for models that also support image generation ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
|
||||
- **chore(providers):** Align emitted Claude Code identity headers, bridge fingerprints, provider profiles, and documented defaults with claude-cli 2.1.207 ([#6862](https://github.com/diegosouzapw/OmniRoute/pull/6862)) - thanks @backryun
|
||||
@@ -1 +0,0 @@
|
||||
- **Homologation suite**: new `npm run homolog` runs the full release-homologation battery against the deployed VPS — health/version parity, API + real SSE streaming with an ephemeral API key (created and revoked by the run), minimal-cost real-provider smoke (promptfoo generated from the live catalog), and a Playwright sweep that loads every dashboard route and exercises the API-key UI flow — emitting a unified CTRF report that backs the release STOP #2 checklist
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api):** Vercel Relay deploy now checks the Deployment Protection (SSO) PATCH response and surfaces `ssoProtectionWarning` when Vercel rejects it, instead of silently activating a relay that later returns an undiagnosed `403 Access denied`. (thanks @ricatix)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(combo):** streaming Claude responses whose content block opens (`content_block_start`) and closes with no usable text/tool_use — a shape some upstreams return for tool-heavy requests on HTTP 200 — are now detected by `validateResponseQuality`'s SSE peek and trigger combo failover instead of being forwarded to the client as a silent empty completion (thanks @heishen6).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(codex):** strip regex `pattern` lookaround (lookahead/lookbehind) from tool JSON Schemas on the Codex/OpenAI native passthrough path — previously only the translated-request path coerced tool schemas, so a `pattern` like `^(?=.*@).+$` reached OpenAI unmodified and was rejected with `regex lookaround is not supported`. (thanks @evinjohnn) (#7100)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** `stopMitm()` now removes /etc/hosts DNS-spoof entries before killing the MITM server process, closing the window where a client's DNS still resolved a target host to `127.0.0.1` while nothing was listening there — the cause of `connect ECONNREFUSED 127.0.0.1:443` right after stopping the MITM proxy (thanks @dionisius95).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** Cursor Composer/Auto tool calls that separate the arg name and value with a space instead of a newline (e.g. `path /Users/.../test`) no longer produce empty-valued, malformed argument keys, fixing silent no-op Write/tool calls. (thanks @way-art)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** the "Custom Models" add/edit form now has a "Vision capable" toggle so a custom OpenAI-compatible model can be manually flagged as vision-capable when the provider's discovery metadata doesn't report an image input modality (thanks @nguyenphi37)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(combos):** fusion combos now reject an oversized panel (>40 models by default, tunable via `fusionTuning.maxPanel`) with a clean 400 before fanning out, instead of buffering dozens of concurrent full responses in memory and OOM-crashing the whole container. (thanks @fontvu)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** the OpenAI-compatible "Check" validation flow now surfaces a warning when the chat-completions probe returns `404` (e.g. `model_not_found`) instead of silently passing as `Valid` — a bogus/non-standard model id (Featherless/OpenRouter-style `vendor/model` typos) previously went undetected at Check time and only surfaced once a real request tripped the per-model lockout. (thanks @advane204f)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(executors):** forward agent-supplied `X-Session-ID`/`X-Title` metadata headers to upstream providers — previously dropped for every client outside the `x-opencode-*` allowlist. (thanks @chitholian) (#7104)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** Antigravity streaming requests that hit a non-ok upstream response (e.g. a 403) no longer pipe the raw upstream bytes straight through to the client — a binary/non-UTF8 error body (observed as gzip-magic-byte garbage) is now routed through the same sanitized `buildAntigravityUpstreamError()` path the non-streaming branch already used, instead of corrupting the client-visible error message. Regression guard: `tests/unit/antigravity-streaming-error-body-sanitized-2461.test.ts` — thanks @Duongkhanhtool
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** the runtime self-heal now verifies a cached `better-sqlite3` native binary actually loads for the running Node before trusting it — the old check only inspected the file's magic bytes (ELF/Mach-O/PE header), so a binary built for a different Node ABI passed validation and segfaulted the process on first use instead of triggering a rebuild. (thanks @mrprohack) (#7105)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): preserve relayAuth for vercel/deno/cloudflare relay proxies referenced by-id from the no-auth-provider Proxy Pool dropdown (#5716)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): wire the Devin cloud-agent provider into the generic provider-page validator and static model catalog, matching the existing `jules` cloud-agent pattern (#6142)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): honor a provider-level proxy assigned to no-auth providers like MiMoCode Free (#6272)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): merge tool_call continuation deltas that carry only `id` (no `index`) so tool-call arguments are no longer split/lost in request/response logs (#6276)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** modernize the `lmarena` provider for the Arena.ai rebrand — route chat through `arena.ai` create-evaluation with Chrome TLS impersonation, seed a static Direct-chat Text/Search + Image catalog, and keep the `lmarena`/`lma` wire id for back-compat ([#6280](https://github.com/diegosouzapw/OmniRoute/pull/6280)) — thanks @backryun
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** web-provider model discovery updated — qwen-web uses the slash-terminated models endpoint (avoiding a blocked 307 redirect), and kimi-web matches the current request shape (POST with bearer + `kimi-auth` cookie replay) with its catalog refreshed to the current non-agent models (#6308 — thanks @janeza2).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(logs):** the request-log detail modal no longer reopens by itself after being closed — a stale in-flight detail refresh resolved after close and re-triggered the modal open state (#6323 — thanks @xz-dev).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** update SenseNova Token Plan support — register the token-plan model ids/constants and adjust the SenseNova registry so token-plan accounts route correctly (#6330 — thanks @xz-dev).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): give v0-vercel-web its own alias so its credentials are detected (#6343)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): route AgentRouter key validation through the CC wire image so a valid key no longer 403s as "Invalid API key" (#6377)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(db): stop legacy log-archive migration from deleting the live app-logger directory and crashing startup on a stat/stream race (#6401, #6799)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(docs): document Turbopack build memory tradeoff and `OMNIROUTE_USE_TURBOPACK=0` webpack fallback for RAM-constrained machines (#6409)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(compression): surface silently-dropped stacked-pipeline steps (session-dedup, ccr) and stop the aggregate inflation guard from misfiring on a genuine no-op (#6479, #6480, #6491)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): honor the `max_token` capability override in the reasoning-token-buffer output cap (#6524)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** the onboarding tier-flow diagram rendered broken — its SVGs lived in the repo-root `images/` (not a served path); moved to `public/images/` so Next.js serves them (#6538 — thanks @ianriizky).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(routing):** the `auto` combo's no-auth candidate pool now honors a disabled provider connection's own `isActive=false` (the toggle on the main Providers grid card), not just the separate global `blockedProviders` setting — disabling opencode/mimocode/etc. via the grid toggle no longer leaves it in rotation ([#6557](https://github.com/diegosouzapw/OmniRoute/issues/6557)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** server-tool literal names (e.g. `web_search`) are preserved in message history and `tool_choice` instead of being namespaced/rewritten, so follow-up turns referencing those tools keep working (#6586 — thanks @MikeTuev).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): sanitize non-Latin1 characters before embedding combo diagnostics in HTTP headers, preventing a ByteString crash on quality-check failure (#6612)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): recognize OpenRouter reasoning/reasoning_details in non-streaming OpenAI-to-Claude conversion (#6623)
|
||||
@@ -1,2 +0,0 @@
|
||||
- fix(db): share one in-flight sql.js load across concurrent `preInitSqlJs()` callers to stop the boot-time thundering-herd re-decode of the whole database file (#6628)
|
||||
- fix(db): unwrap lone named-parameter objects before `sql.js` `stmt.bind()` so `@`/`:`/`$`-style named placeholders bind correctly instead of throwing "Wrong API use" (#6802)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db): break probe-failed/restore loop on large storage.sqlite** (#6632 — thanks @KooshaPari).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(ci): exclude check-test-masking.test.ts's own tautology fixtures from the diff-based test-masking gate and recognize `✗` in validate-release-green's failure-line detector (#6634)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(routing): recognize Kimi-style "exceeded model token limit" 400 as context overflow so combo fallback continues to the next target (#6637)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** Claude Code installed via WinGet is now detected on Windows (the WinGet install path was missing from the binary lookup) (#6647 — thanks @enjoyer-hub).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** removed obsolete/defunct providers from the catalog (glhf, kluster, cablyai, inclusionai) (#6675 — thanks @backryun).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** requests rejected before `handleChatCore` (circuit-breaker/cooldown gate or combo with all targets exhausted) are now recorded in `usage_history` too, so a key whose traffic was entirely gate-rejected no longer shows "zero requests" in the per-API-key usage counter (#6698).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): reject chat-completions requests for cloud-agent-only providers like jules instead of silently mis-routing them to OpenAI's endpoint (#6699)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** unwrap bare `{function:{…}}` tools so OpenAI-shape clients no longer have tools silently dropped in Claude translation. (thanks @samir-abis)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(oauth):** stop merging distinct Codex OAuth logins that share an email but lack a verifiable account id, preventing silent token overwrite. (thanks @lucasjustinudin)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user