mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
Compare commits
1 Commits
feat/relay
...
docs/agent
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce0b2bb122 |
179
.env.example
179
.env.example
@@ -173,11 +173,6 @@ PORT=20128
|
||||
# stay consistent without relying on window.location.origin alone:
|
||||
# NEXT_PUBLIC_BASE_URL=https://host/omniroute
|
||||
#
|
||||
# Client-side fallback port for display URLs when no origin is known (SSR/tests):
|
||||
# read before PORT so a browser bundle built with a different public port still
|
||||
# renders the right http://localhost:<port> links (src/shared/hooks/useDisplayBaseUrl.ts).
|
||||
# NEXT_PUBLIC_PORT=20128
|
||||
#
|
||||
# Explicit path probed by the container health check. Unset, the probe derives it
|
||||
# from OMNIROUTE_BASE_PATH; setting it opts back into the deep monitoring endpoint.
|
||||
# Used by: scripts/dev/healthcheck.mjs
|
||||
@@ -546,48 +541,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
|
||||
# OUTBOUND_SSRF_GUARD_ENABLED=true
|
||||
|
||||
# ── Self-hosted unified OpenAI-compatible entry (RIC-738, D4) ────────────────────
|
||||
# When set, /v1/chat/completions diverts to the self-hosted provider adapters
|
||||
# (open-sse/services/selfHostedEntry.ts) instead of the cloud pipeline. YAML inline
|
||||
# (example) — or point OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE at a YAML file. Secrets
|
||||
# are runtime-only, never logged. While ANY of these is set, the entry is active;
|
||||
# config present but unparseable returns a 500 (never silently falls through).
|
||||
# OMNIROUTE_SELF_HOSTED_PROVIDERS='
|
||||
# providers:
|
||||
# - id: local
|
||||
# kind: openai
|
||||
# baseUrl: http://127.0.0.1:11434/v1
|
||||
# model: llama3
|
||||
# - id: claude
|
||||
# kind: anthropic
|
||||
# baseUrl: http://127.0.0.1:8080
|
||||
# model: claude-sonnet
|
||||
# '
|
||||
# OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE=/etc/omniroute/providers.yaml
|
||||
# Optional shared API key for the unified entry (D5 reserved). When set, require
|
||||
# `Authorization: Bearer <key>`; empty = open loopback/trusted-network route.
|
||||
# OMNIROUTE_SELF_HOSTED_API_KEY=
|
||||
|
||||
# ── Deterministic routing strategies (M2 / RIC-740, D3 可审计路由) ─────────────
|
||||
# Optional `strategy:` block — either inline in the providers document above, or a
|
||||
# standalone document via these env vars. One rule per line; every decision is
|
||||
# explainable via the `x-omniroute-route-decision` response header. No ML/predict.
|
||||
# Malformed strategy config returns a 500 (never silently becomes a no-op).
|
||||
# Example (inline, same shape as `strategy:` inside the providers YAML):
|
||||
# OMNIROUTE_SELF_HOSTED_STRATEGY='
|
||||
# blacklist: []
|
||||
# whitelist: [cheap, fast, premium]
|
||||
# costPriority: true
|
||||
# latencyAware:
|
||||
# enabled: true
|
||||
# cooldown:
|
||||
# consecutiveFailures: 2
|
||||
# cooldownMs: 30000
|
||||
# fallbackChain: [cheap, fast, premium]
|
||||
# '
|
||||
# OMNIROUTE_SELF_HOSTED_STRATEGY_FILE=/etc/omniroute/strategy.yaml
|
||||
# See docs/routing/DETERMINISTIC_ROUTING.md for the full strategy surface.
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -669,15 +622,6 @@ ALLOW_API_KEY_REVEAL=false
|
||||
# Validated to >= 1, clamped to <= 32. | Default: 3
|
||||
# COMBO_CONCURRENCY_PER_MODEL=3
|
||||
|
||||
# Disable conversation-history tracking (#13150).
|
||||
# Used by: open-sse/services/conversationTracker.ts. resolveConversationId()
|
||||
# returns an untracked result before it reads SQLite or parses message history,
|
||||
# and the switch also covers client-supplied session IDs. Routing sessions are
|
||||
# unaffected and existing records are not deleted. Use it when the dashboard's
|
||||
# conversation view is unused and the turn table has grown large.
|
||||
# Set to 1 to disable. | Default: unset (tracking enabled)
|
||||
# OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 7. URLS & CLOUD SYNC
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -768,26 +712,14 @@ NEXT_PUBLIC_CLOUD_URL=
|
||||
# OpenCode Go/Zen VPS egress (#5997): on a datacenter VPS, Cloudflare in front of
|
||||
# opencode.ai/zen/go 403s chat requests that lack OpenCode CLI identity headers.
|
||||
# When your clients don't already send them, set this to synthesize the CLI headers
|
||||
# (User-Agent, x-opencode-client, x-opencode-project, canonical request/session ids) on
|
||||
# absent keys. ON by default — a client value always wins, these only fill gaps.
|
||||
# (User-Agent, x-opencode-client, x-opencode-project, fresh request/session UUIDs) on
|
||||
# absent keys. OFF by default — forward-only is safer when clients already send them.
|
||||
# Values are overridable via OPENCODE_GO_USER_AGENT / OPENCODE_USER_AGENT / OPENCODE_CLIENT /
|
||||
# OPENCODE_PROJECT (defaults: opencode/1.18.31 / desktop / global).
|
||||
# OPENCODE_PROJECT (defaults: opencode-cli/1.0.0 / cli / default).
|
||||
#OPENCODE_SYNTHESIZE_CLI_HEADERS=true
|
||||
#OPENCODE_USER_AGENT=opencode/1.18.31
|
||||
#OPENCODE_CLIENT=desktop
|
||||
#OPENCODE_PROJECT=global
|
||||
|
||||
# Keyless OpenCode models are answered only when the request declares a non-empty tool
|
||||
# list, and the upstream inspects which names it carries. OmniRoute reuses the list a
|
||||
# request of the same conversation was last seen getting through, so a request that
|
||||
# carries none — a title or a summary — goes out with the list its own client already
|
||||
# declared. Set to off to stop adjusting request bodies entirely; headers are unaffected.
|
||||
#OPENCODE_FREE_TIER_REQUEST_CONTRACT=off
|
||||
|
||||
# Tool names to declare when nothing has been observed yet for a model, comma-separated.
|
||||
# Empty falls back to a single placeholder the model is told not to call. Only useful on
|
||||
# an install where no client sends tools, since there is then nothing to learn from.
|
||||
#OPENCODE_FREE_TIER_PLACEHOLDER_TOOLS=glob,grep,read
|
||||
#OPENCODE_USER_AGENT=opencode-cli/1.0.0
|
||||
#OPENCODE_CLIENT=cli
|
||||
#OPENCODE_PROJECT=default
|
||||
|
||||
# Ollama Cloud quota scraping. Prefer configuring this per connection in
|
||||
# Dashboard → Providers → Ollama Cloud. The cookie is sensitive.
|
||||
@@ -1161,11 +1093,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
||||
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
||||
|
||||
# Character cap for Lite proactive tool-result truncation when lite.maxToolLength
|
||||
# is unset. Range 256-1000000. Dashboard setting wins over this env.
|
||||
# Used by: open-sse/services/compression/lite.ts. Default: 2000.
|
||||
#OMNIROUTE_LITE_MAX_TOOL_LENGTH=2000
|
||||
|
||||
# Maximum concurrent synchronous compression workers. Excess jobs wait FIFO.
|
||||
# Used by: open-sse/services/compression/compressionWorkerPool.ts. Default: 2.
|
||||
#OMNI_COMPRESSION_WORKERS=2
|
||||
@@ -1222,21 +1149,31 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
|
||||
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
|
||||
|
||||
# Removed: periodic live wal_checkpoint(TRUNCATE) could SIGBUS the process (issue
|
||||
# #13973). The variable is inert: a positive value logs a one-time deprecation warning,
|
||||
# while 0 or unset stays silent. The WAL is kept small
|
||||
# by the PASSIVE scheduler below and truncated by the shutdown checkpoint.
|
||||
# WAL truncate cadence override (ms). Set to 0 to disable. Default: 21600000 (6h).
|
||||
# Used by: src/lib/db/core.ts::getWalTruncateIntervalMs().
|
||||
#OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS=21600000
|
||||
|
||||
# Frequent wal_checkpoint(PASSIVE) cadence (ms). Set to 0 to disable. Default: 300000 (5m).
|
||||
# Used by: src/lib/db/walMaintenance.ts.
|
||||
#OMNIROUTE_WAL_PASSIVE_INTERVAL_MS=300000
|
||||
|
||||
# WAL size (MB) above which a PASSIVE tick runs wal_checkpoint(RESTART) so the
|
||||
# WAL starts over without rewriting the mapped wal-index. Default: 256.
|
||||
# WAL size (MB) above which a PASSIVE tick escalates to wal_checkpoint(TRUNCATE). Default: 256.
|
||||
# Used by: src/lib/db/walMaintenance.ts.
|
||||
#OMNIROUTE_WAL_GUARD_MAX_MB=256
|
||||
|
||||
# Minimum rows a cleanup must delete before the post-cleanup VACUUM runs. Default: 1000.
|
||||
# 0 always vacuums when rows were freed. The post-cleanup VACUUM also runs when the
|
||||
# reclaimable-space threshold below is met, whichever comes first (either signal fires it).
|
||||
# Used by: src/lib/db/cleanup.ts::shouldVacuumAfterCleanup().
|
||||
#OMNIROUTE_VACUUM_MIN_DELETED_ROWS=1000
|
||||
|
||||
# Minimum reclaimable space (MB) that alone justifies a full-database VACUUM after a
|
||||
# cleanup, even when the row-count threshold above was not met (a handful of oversized
|
||||
# blob rows can free far more space than thousands of tiny rows). VACUUM is synchronous
|
||||
# and blocks the entire process. Default: 100. 0 always vacuums after any deletion.
|
||||
# Used by: src/lib/db/cleanup.ts::getVacuumMinReclaimableBytes().
|
||||
#OMNIROUTE_VACUUM_MIN_RECLAIMABLE_MB=100
|
||||
|
||||
# Explicit path to sql-wasm.wasm for the sql.js fallback adapter. Default: auto-detect.
|
||||
# Used by: src/lib/db/adapters/sqljsAdapter.ts.
|
||||
#OMNIROUTE_SQLJS_WASM_PATH=
|
||||
@@ -1479,13 +1416,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# Override the advertised GitHub Copilot CLI version independently of
|
||||
# GITHUB_USER_AGENT. Used by: open-sse/config/providerHeaderProfiles.ts.
|
||||
# GITHUB_COPILOT_CLI_VERSION=1.0.82
|
||||
#
|
||||
# Pin the `copilot-integration-id` header sent to standard GitHub Copilot,
|
||||
# overriding the default copilot-developer-cli identity (and disabling the
|
||||
# automatic 403-identity fallback to copilot-chat). Set this only if your
|
||||
# Copilot account/org requires a specific integration id. Used by:
|
||||
# open-sse/config/providerHeaderProfiles.ts, open-sse/executors/copilotIdentityFallback.ts.
|
||||
# COPILOT_INTEGRATION_ID=copilot-chat
|
||||
|
||||
# Kill-switch to strip non-standard `codex.*` SSE events (e.g. codex.rate_limits)
|
||||
# from the Codex Responses stream. These frames break the OpenAI SDK's
|
||||
@@ -1593,7 +1523,7 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
#
|
||||
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
|
||||
# If set, it becomes the default for FETCH_TIMEOUT_MS, STREAM_IDLE_TIMEOUT_MS,
|
||||
# and STREAM_READINESS_TIMEOUT_MS. STREAM_ACTIVE_TIMEOUT_MS is independent.
|
||||
# and STREAM_READINESS_TIMEOUT_MS.
|
||||
# The fine-grained variables below override their respective defaults only when set.
|
||||
|
||||
# ── Global shortcut ──
|
||||
@@ -1614,18 +1544,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# # caller's deadline; on expiry the request retries
|
||||
# # once on a fresh no-keep-alive socket. 0 disables
|
||||
# # the bound (default: 30000 = 30s).
|
||||
# OMNIROUTE_DIRECT_RESPONSE_RETRY_TIMEOUT_MS=600000 # Ceiling (ms) for the fresh-socket
|
||||
# # RETRY attempt above (#13703). Only applies when
|
||||
# # the caller already attached its own deadline
|
||||
# # signal (the resolved connection/model/provider/
|
||||
# # FETCH_TIMEOUT_MS cascade) — that signal is the
|
||||
# # real bound and fires first in the intended path,
|
||||
# # so this is a generous backstop rather than a flat
|
||||
# # cap: without it the retry reused the same short
|
||||
# # OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS window as the
|
||||
# # pooled attempt and 504'd healthy slow-TTFB
|
||||
# # reasoning models. Never allowed below the flat
|
||||
# # floor above (default: 600000 = 10 min).
|
||||
|
||||
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
|
||||
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
|
||||
@@ -1787,8 +1705,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# ── Stream idle detection ──
|
||||
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
|
||||
# # Extended-thinking models rarely pause >90s.
|
||||
# STREAM_ACTIVE_TIMEOUT_MS=1260000 # Max total active SSE lifetime (default: 21 min = the largest registered model timeoutMs + 1 min; 0 disables)
|
||||
# # Independent of REQUEST_TIMEOUT_MS and byte activity.
|
||||
# STREAM_READINESS_TIMEOUT_MS=80000 # Time to receive the first non-ping SSE event
|
||||
# STREAM_READINESS_MAX_TIMEOUT_MS=180000 # Cap for adaptive first-event extensions
|
||||
# # (large/tool-heavy/high-reasoning requests).
|
||||
@@ -1806,8 +1722,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
|
||||
# TLS_FIRST_BYTE_WATCHDOG_MS=10000 # #12656: bounds time-to-first-byte on the wreq body (0 disables)
|
||||
# OPENCODE_RESPONSES_STALL_ROTATION=false # #13484 feature flag (Settings → Feature Flags wins): rotate once when a streamed Responses reply stalls before its first byte
|
||||
# OPENCODE_PARK_AND_RESUME=false # #13924 feature flag (Settings → Feature Flags wins): park the request with a heartbeat after repeated transient 429s, then replay one capped leg of up to 3 accounts
|
||||
#OPENCODE_POOL_STRAIN_MARKER_PATH=/tmp/opencode-pool-strain.json # #13924: pool-strain marker path (JSON {since, reason, ttl_s}); fresh marker parks without recounting
|
||||
# RESPONSES_FIRST_BYTE_TIMEOUT_MS=15000 # #13484: OpenCode Responses first-byte window, only used when the OPENCODE_RESPONSES_STALL_ROTATION flag is on (0 disables)
|
||||
|
||||
# ── API Bridge (/v1 proxy server) ──
|
||||
@@ -1897,8 +1811,8 @@ APP_LOG_TO_FILE=true
|
||||
# bodies is retained in the database.
|
||||
# Used by: open-sse/handlers/chatCore.ts — cloneBoundedChatLogPayload()
|
||||
# CHAT_LOG_TEXT_LIMIT=65536 # Max string length before truncation (default: 64 KB)
|
||||
# CHAT_LOG_ARRAY_TAIL_ITEMS=1000 # Number of array items retained from tail (default: 1000)
|
||||
# CHAT_LOG_MAX_DEPTH=20 # Max nesting depth before truncation (default: 20)
|
||||
# CHAT_LOG_ARRAY_TAIL_ITEMS=128 # Number of array items retained from tail (default: 128)
|
||||
# CHAT_LOG_MAX_DEPTH=6 # Max nesting depth before truncation (default: 6)
|
||||
# CHAT_LOG_MAX_OBJECT_KEYS=80 # Max object keys retained (default: 80, 0 = no limit)
|
||||
# CHAT_LOG_MAX_BODY_KB=1024 # Whole request/response body size before it's replaced by a bare
|
||||
# {_truncated, messageCount, ...} summary instead of the full clone
|
||||
@@ -2272,13 +2186,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: src/lib/services/bootstrap.ts, src/app/api/services/mux/_lib.ts
|
||||
# MUX_SERVICE_PORT=8322
|
||||
|
||||
# ── open-wa embedded service ──
|
||||
# Override the port where the embedded open-wa (WhatsApp Web automation)
|
||||
# daemon listens. Always bound to 127.0.0.1 — never configurable to 0.0.0.0.
|
||||
# Rarely needed — defaults to 8323.
|
||||
# Used by: src/lib/services/bootstrap.ts
|
||||
# OPENWA_SERVICE_PORT=8323
|
||||
|
||||
# ── Dario embedded service ──
|
||||
# Override the host/port the embedded Dario (Claude Code subscription proxy)
|
||||
# daemon binds to and is reached at. Always bound to 127.0.0.1 — never
|
||||
@@ -2490,22 +2397,6 @@ APP_LOG_TO_FILE=true
|
||||
# Used by: open-sse/executors/cursor.ts.
|
||||
# CURSOR_TOOL_DIRECTIVE=1
|
||||
|
||||
# Operator-defined system prompt text appended to the system message AFTER
|
||||
# translation (post-translation injection), so it reaches codex/Responses and
|
||||
# /v1/messages paths. Also used as the directive prefix stripped from echoed
|
||||
# system preamble blocks. Leave unset to disable.
|
||||
# Used by: open-sse/translator/request/claude-to-openai.ts, open-sse/translator/response/openai-to-claude.ts.
|
||||
# OMNIROUTE_SYSTEM_INSTRUCTION_APPEND=
|
||||
|
||||
# Set to "1" to also strip echoed system-prompt PREAMBLE blocks
|
||||
# (<analysis>/<system-reminder>/<summary> blocks, prose reproductions of the skill
|
||||
# section) from the start of an openai->claude stream. OFF by default: it recognises
|
||||
# constructs by English-prose heuristics and DOES mutate the response payload, so a
|
||||
# reply that genuinely opens with such a section would lose it. Turn it on only when
|
||||
# you actually hit the system-echo leak.
|
||||
# Used by: open-sse/translator/response/openai-to-claude.ts, open-sse/utils/directivePreambleStripper.ts.
|
||||
# OMNIROUTE_STRIP_SYSTEM_PREAMBLE=0
|
||||
|
||||
# Per-image fetch timeout (ms) for remote image_url vision input. Default: 15000.
|
||||
# Used by: open-sse/utils/cursorImages.ts.
|
||||
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
|
||||
@@ -2702,16 +2593,6 @@ APP_LOG_TO_FILE=true
|
||||
# for root-less / user-namespaced deployments (e.g. rootless Docker/Podman)
|
||||
# where the operator trusts the CA manually (e.g. via Node's extra-CA-certs mechanism).
|
||||
# OMNIROUTE_NO_SUDO=0
|
||||
|
||||
# ── Antigravity MITM bridge (bin/antigravity-bridge.mjs) ──
|
||||
# Local HTTPS listener that fronts the Antigravity IDE and forwards to the router.
|
||||
# BRIDGE_PORT: port the bridge listens on. Defaults to 20129.
|
||||
# ROUTER_URL: where it forwards /v1/antigravity traffic. Defaults to the local router.
|
||||
# CERT_DIR: directory holding server.key/server.crt for the bridge's TLS listener.
|
||||
# Defaults to ~/.omniroute/mitm (the MITM CA directory).
|
||||
# BRIDGE_PORT=20129
|
||||
# ROUTER_URL=http://127.0.0.1:20128/v1/antigravity
|
||||
# CERT_DIR=~/.omniroute/mitm
|
||||
# Explicit opt-out: skip provisioning /etc/hosts DNS entries for the Antigravity
|
||||
# proxy hostnames entirely (containers with no sudo/root available).
|
||||
# Used by: src/mitm/dns/provision.ts.
|
||||
@@ -2748,16 +2629,6 @@ APP_LOG_TO_FILE=true
|
||||
# When enabled, the node authenticates with the API key stored on its connection.
|
||||
# AUDIO_REMOTE_PROVIDER_NODES=false
|
||||
|
||||
# Used by: src/app/api/v1/_shared/rerankProviderNodes.ts — lets POST /v1/rerank (and
|
||||
# the memory engine's loopback rerank step) use an OpenAI-compatible provider node
|
||||
# hosted outside localhost, e.g. a LAN box or Tailscale peer running TEI/Infinity/vLLM.
|
||||
# OFF by default: routing to a remote host changes egress identity, so it must be an
|
||||
# explicit operator decision. Loopback/private nodes (localhost, 127.0.0.1,
|
||||
# 172.16-31.x) are always allowed and unaffected by this flag. Remote nodes must also
|
||||
# pass the provider outbound URL policy (see OMNIROUTE_ALLOW_LOCAL_PROVIDER_URLS);
|
||||
# cloud-metadata hosts are never routed to.
|
||||
# RERANK_REMOTE_PROVIDER_NODES=false
|
||||
|
||||
# ── Free Proxy Pool (auto-sync scheduler) ──
|
||||
# Background refresh of the free-proxy pool. Opt-in, OFF by default (parallels
|
||||
# Hard Rule #20's default-off posture for data-mutating background features).
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# OmniRoute — Self-Host env (minimal, zero-fee self-host)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# cp .env.selfhost.example .env
|
||||
# Edit only the two lines marked `# EDIT ME`. Everything else has a sane
|
||||
# default. No secrets are baked in — OmniRoute never ships credentials.
|
||||
#
|
||||
# Full variable reference: docs/guides/DOCKER_GUIDE.md and .env.example
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Ports (host-side) ──────────────────────────────────────────────────
|
||||
# Dashboard + API + Live-WS. Already match the image defaults.
|
||||
DASHBOARD_PORT=20128
|
||||
API_PORT=20129
|
||||
LIVE_WS_PORT=20132
|
||||
|
||||
# ── Bind address ───────────────────────────────────────────────────────
|
||||
# 127.0.0.1 = loopback only (safe with REQUIRE_API_KEY=false, the default).
|
||||
# Set to 0.0.0.0 ONLY when REQUIRE_API_KEY=true OR a reverse proxy
|
||||
# enforces auth upstream. Exposing an unauthenticated /v1 proxy on the
|
||||
# LAN/WAN lets anyone burn your provider quotas. # EDIT ME if you must.
|
||||
APP_BIND_HOST=127.0.0.1
|
||||
|
||||
# ── Auth ──────────────────────────────────────────────────────────────
|
||||
# false = the dashboard and /v1 proxy are open to APP_BIND_HOST's network.
|
||||
# true = every request needs an API key / dashboard login. The dashboard
|
||||
# auto-creates INITIAL_PASSWORD on first boot (read it from the logs:
|
||||
# `docker logs omniroute | grep -i password`). # EDIT ME — set true.
|
||||
REQUIRE_API_KEY=false
|
||||
# INITIAL_PASSWORD= # uncomment to pre-seed the dashboard password
|
||||
|
||||
# ── Memory ceiling (V8 old-space) ──────────────────────────────────────
|
||||
# 1024 = dashboard + light chat. Coding agents (long POST /v1/responses
|
||||
# bodies) need more — see SELF_HOST_GUIDE.md "sizing". 2048 is a safe
|
||||
# default for a single user who runs Claude Code / Codex through it.
|
||||
OMNIROUTE_MEMORY_MB=2048
|
||||
|
||||
# ── Browser-facing origin (optional) ───────────────────────────────────
|
||||
# Set ONLY if you expose OmniRoute behind a domain via a reverse proxy.
|
||||
# NEXT_PUBLIC_BASE_URL=https://your-domain.example.com
|
||||
# BASE_URL=http://omniroute:20128
|
||||
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
4
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Bug Report
|
||||
description: Report a bug or unexpected behavior in OmniRoute
|
||||
title: "fix(): "
|
||||
title: "[BUG] "
|
||||
labels: ["bug"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -8,8 +8,6 @@ body:
|
||||
value: |
|
||||
Thanks for taking the time to report a bug. Please fill out the sections below so we can reproduce and fix the issue.
|
||||
|
||||
The title is prefilled as `fix(): ` to match the [Conventional Commits](https://github.com/diegosouzapw/OmniRoute/blob/main/CONTRIBUTING.md#commit-messages) convention — pick a scope from the list documented there (e.g. `providers`, `resilience`, `dashboard`, `api`).
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
|
||||
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
4
.github/ISSUE_TEMPLATE/feature_request.yml
vendored
@@ -1,6 +1,6 @@
|
||||
name: Feature Request
|
||||
description: Suggest a new feature or improvement for OmniRoute
|
||||
title: "feat(): "
|
||||
title: "[Feature] "
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -8,8 +8,6 @@ body:
|
||||
value: |
|
||||
Thanks for suggesting a feature! Please describe the problem you're trying to solve and how you'd like it to work.
|
||||
|
||||
The title is prefilled as `feat(): ` to match the [Conventional Commits](https://github.com/diegosouzapw/OmniRoute/blob/main/CONTRIBUTING.md#commit-messages) convention — pick a scope from the list documented there (e.g. `providers`, `resilience`, `dashboard`, `api`).
|
||||
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.github/workflows/ci.yml
vendored
@@ -453,11 +453,8 @@ jobs:
|
||||
# One FS inventory of src/app/api for both anti-hallucination directions.
|
||||
- name: API docs refs (openapi + prose → routes)
|
||||
run: npm run check:api-docs-refs
|
||||
# Blocking since the 2026-09 docs re-sync: a core doc edited without `npm run i18n:run
|
||||
# --files=<doc>` (or `--adopt` for a mechanical edit) leaves 65 stale mirrors behind;
|
||||
# the run only retranslates the `## ` sections whose text changed, so it is cheap.
|
||||
- name: i18n docs drift (sources changed since their translation)
|
||||
run: node scripts/i18n/check-translation-drift.mjs
|
||||
- name: i18n translation drift (warn)
|
||||
run: node scripts/i18n/check-translation-drift.mjs --warn
|
||||
|
||||
docs-lint:
|
||||
name: Docs Lint (prose — advisory)
|
||||
|
||||
42
.github/workflows/release-acceptance.yml
vendored
42
.github/workflows/release-acceptance.yml
vendored
@@ -1,42 +0,0 @@
|
||||
name: Release acceptance
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["release/v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: release-acceptance-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
acceptance:
|
||||
name: Release acceptance
|
||||
if: github.event_name != 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: "22"
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Emit shadow acceptance report
|
||||
run: |
|
||||
node scripts/quality/validate-release-acceptance.mjs \
|
||||
--plan tests/fixtures/release-acceptance/plan-lint.json \
|
||||
--manifests tests/fixtures/release-acceptance/shadow-manifests \
|
||||
--out release-acceptance-report.json
|
||||
continue-on-error: true
|
||||
- uses: actions/upload-artifact@v4
|
||||
if: always()
|
||||
with:
|
||||
name: release-acceptance-report
|
||||
path: release-acceptance-report.json
|
||||
if-no-files-found: ignore
|
||||
retention-days: 30
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -74,7 +74,6 @@ yarn-error.log*
|
||||
# Local gitleaks artifacts (do not commit)
|
||||
gitleaks-local.json
|
||||
!.env.example
|
||||
!.env.selfhost.example
|
||||
!.env.homolog.example
|
||||
!.env.devin-bridge.example
|
||||
# Provider API keys (never commit)
|
||||
@@ -219,7 +218,6 @@ scripts/i18n/_pending-keys.json
|
||||
|
||||
# PR Reviews and local feedback files
|
||||
pr_reviews*.json
|
||||
/review/
|
||||
|
||||
#hidden local data directories (never commit)
|
||||
.local-data/
|
||||
|
||||
166418
.i18n-state.json
166418
.i18n-state.json
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,3 @@
|
||||
# wasm-bindgen glue + embedded WASM_BASE64. Prettier rewrites the generated JS
|
||||
# (quotes, wrapping) on any touch of this file; format tinycmsDomMocks.ts instead.
|
||||
open-sse/executors/tinycmsSigner.ts
|
||||
|
||||
# Long reference tables are manually aligned; formatting the whole file causes noisy diffs.
|
||||
docs/reference/ENVIRONMENT.md
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ Repository map and Reference Documentation sections below.
|
||||
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
||||
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
||||
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (178 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (176 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 110 tools (45 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
|
||||
327
CHANGELOG.md
327
CHANGELOG.md
@@ -92,31 +92,31 @@
|
||||
|
||||
## [3.8.51] — TBD
|
||||
|
||||
_Living section — reconciled 2026-09-15 from all cycle commits (`091589089c` → `c0f92ec98a`, 916 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
|
||||
_Living section — reconciled 2026-09-07 from all cycle commits (`release/v3.8.50` tip `091589089c` → `release/v3.8.51` tip `d6f315018a`, 696 non-merge commits). Bullets carry the merged PR and its author; direct pushes are listed with their commit hash. Regenerated at each `/generate-release` phase._
|
||||
|
||||
### 📊 Release by the numbers
|
||||
|
||||
| | |
|
||||
| --- | ---: |
|
||||
| 👥 People who contributed | **123** |
|
||||
| 📝 Commits in the cycle | **916** |
|
||||
| 🔀 Pull requests referenced | **892** |
|
||||
| 📋 Changelog entries | **919** |
|
||||
| 🙌 Contributors credited in entries | **121** |
|
||||
| 👥 People who contributed | **120** |
|
||||
| 📝 Commits in the cycle | **696** |
|
||||
| 🔀 Pull requests referenced | **672** |
|
||||
| 📋 Changelog entries | **685** |
|
||||
| 🙌 Contributors credited in entries | **118** |
|
||||
| 🤖 Automated dependency commits | 18 |
|
||||
|
||||
**Entries by type**
|
||||
|
||||
| Type | Count |
|
||||
| --- | ---: |
|
||||
| 🐛 Fixes | 592 |
|
||||
| ✨ Features | 131 |
|
||||
| 🧹 Chore | 93 |
|
||||
| 📚 Docs | 42 |
|
||||
| 🧪 Tests | 32 |
|
||||
| ♻️ Refactor | 8 |
|
||||
| 🏗️ Build | 7 |
|
||||
| 🐛 Fixes | 403 |
|
||||
| ✨ Features | 118 |
|
||||
| 🧹 Chore | 72 |
|
||||
| 📚 Docs | 36 |
|
||||
| 🧪 Tests | 29 |
|
||||
| ⚡ Performance | 7 |
|
||||
| ♻️ Refactor | 7 |
|
||||
| 🏗️ Build | 6 |
|
||||
| ⚙️ CI | 2 |
|
||||
| ⏪ Reverts | 2 |
|
||||
| 📦 Dependencies | 2 |
|
||||
@@ -124,35 +124,35 @@ _Living section — reconciled 2026-09-15 from all cycle commits (`091589089c`
|
||||
|
||||
### 🏆 Top 25 contributors this cycle
|
||||
|
||||
_By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
|
||||
_By commits in `release/v3.8.50..release/v3.8.51` (`091589089c..d6f315018a`), author identities consolidated via `.mailmap` and the merged PR's GitHub login. Bots excluded._
|
||||
|
||||
| # | Contributor | Commits |
|
||||
| ---: | --- | ---: |
|
||||
| 🥇 | diegosouzapw | 323 |
|
||||
| 🥈 | Bob.Hou (@HouMinXi) | 79 |
|
||||
| 🥉 | Paco Cartones (@pacocartones) | 77 |
|
||||
| 4 | Dizzle (@maxmad64bis) | 49 |
|
||||
| 5 | Koosha Paridehpour (@KooshaPari) | 43 |
|
||||
| 6 | Nguyen Thanh Dat (@ntdatt812) | 31 |
|
||||
| 7 | Ravi Tharuma (@RaviTharuma) | 26 |
|
||||
| 8 | Markus Hartung (@hartmark) | 22 |
|
||||
| 9 | Webman (@jonlwheat2-gif) | 22 |
|
||||
| 10 | backryun | 17 |
|
||||
| 11 | anhtahaylove | 15 |
|
||||
| 12 | Rafa Martins (@rafacpti23) | 10 |
|
||||
| 13 | Syed Raheemuddin (@raheemuddin786) | 10 |
|
||||
| 14 | Nguyễn Viết Tuấn (@TheDemonTuan) | 8 |
|
||||
| 15 | MumuTW | 7 |
|
||||
| 16 | Paijo (@oyi77) | 7 |
|
||||
| 17 | Tobias Andersen (@turbolego) | 6 |
|
||||
| 18 | Bl0ck (@Bl0ck154) | 5 |
|
||||
| 19 | SHANMUGAPRIYAN (@geek007git) | 5 |
|
||||
| 20 | KaspaPulse | 5 |
|
||||
| 21 | Abhishek Sharma (@abhisheksharma2411) | 4 |
|
||||
| 22 | Abhishek Divekar (@adivekar-utexas) | 4 |
|
||||
| 23 | Andrew B. (@AndrianBalanescu) | 4 |
|
||||
| 24 | NoxzRCW | 4 |
|
||||
| 25 | opensource-elearning | 4 |
|
||||
| 🥇 | diegosouzapw | 240 |
|
||||
| 🥈 | Paco Cartones (@pacocartones) | 58 |
|
||||
| 🥉 | Bob.Hou (@HouMinXi) | 57 |
|
||||
| 4 | Dizzle (@maxmad64bis) | 27 |
|
||||
| 5 | Webman (@jonlwheat2-gif) | 22 |
|
||||
| 6 | Ravi Tharuma (@RaviTharuma) | 21 |
|
||||
| 7 | backryun | 17 |
|
||||
| 8 | Markus Hartung (@hartmark) | 17 |
|
||||
| 9 | Nguyen Thanh Dat (@ntdatt812) | 16 |
|
||||
| 10 | Rafa Martins (@rafacpti23) | 10 |
|
||||
| 11 | Syed Raheemuddin (@raheemuddin786) | 10 |
|
||||
| 12 | Koosha Paridehpour (@KooshaPari) | 9 |
|
||||
| 13 | Nguyễn Viết Tuấn (@TheDemonTuan) | 8 |
|
||||
| 14 | MumuTW | 7 |
|
||||
| 15 | Paijo (@oyi77) | 7 |
|
||||
| 16 | Tobias Andersen (@turbolego) | 6 |
|
||||
| 17 | Bl0ck (@Bl0ck154) | 5 |
|
||||
| 18 | SHANMUGAPRIYAN (@geek007git) | 5 |
|
||||
| 19 | KaspaPulse | 5 |
|
||||
| 20 | Abhishek Sharma (@abhisheksharma2411) | 4 |
|
||||
| 21 | Abhishek Divekar (@adivekar-utexas) | 4 |
|
||||
| 22 | Andrew B. (@AndrianBalanescu) | 4 |
|
||||
| 23 | NoxzRCW | 4 |
|
||||
| 24 | opensource-elearning | 4 |
|
||||
| 25 | Alvin T. Veroy (@alvinveroy) | 3 |
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -278,19 +278,6 @@ _By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.ma
|
||||
- **feat(dashboard):** link a log entry's Conversation Context to its owning conversation ([#12646](https://github.com/diegosouzapw/OmniRoute/pull/12646)) — thanks @hartmark
|
||||
- **feat(catalog):** eligibility-gated free-tier bucket ([#12669](https://github.com/diegosouzapw/OmniRoute/pull/12669))
|
||||
- **feat(db):** fail-closed previous_response_id continuation for redacted video turns (#12150 P2b) ([#12707](https://github.com/diegosouzapw/OmniRoute/pull/12707))
|
||||
- **feat(gamification): pay the documented `streak_bonus` and `badge_unlock` XP rewards.** `XP_REWARDS` listed both rewards but the award pipeline never paid them: the private reward table in `events.ts` omitted them, `updateStreak()` did not report when a streak extended, and badge unlocks carried no XP. Every request that extends a daily streak now pays `streak_bonus × streak length` once per UTC day (guarded by a same-day `xp_audit_log` check), and every badge unlocked through the pipeline pays `badge_unlock` once per badge (guarded by the `user_badges` primary key; `unlockBadge()` now reports whether it inserted). Bonus XP flows through the same `addXp` + level sync + global/weekly/monthly leaderboard path as action XP, so level-ups and rankings include it. The Radar supporter recognition unlock stays XP-free. (#12522 — thanks @pacocartones)
|
||||
- **feat(providers):** advertise a `free-tier` capability in the provider plugin manifest for every provider with documented free models, so sidecars and dashboards can filter free-capable providers without reading the quota catalog ([#12786](https://github.com/diegosouzapw/OmniRoute/pull/12786)) — thanks @maxmad64bis
|
||||
- **feat(providers):** Added EURouter as an OpenAI-compatible API-key gateway (`https://api.eurouter.ai/v1`), with live model discovery via `passthroughModels`. Its copy states that models are served by third-party upstreams listed per model, so an EU-based router is not read as EU data residency for inference. ([#13025](https://github.com/diegosouzapw/OmniRoute/pull/13025)) — thanks @ntdatt812
|
||||
- **feat(providers):** Added GreenPT as an OpenAI-compatible API-key provider (`https://api.greenpt.ai/v1`), with live model discovery via `passthroughModels`. No free-inference badge: the published docs describe a free API subscription billed per token, not a free tier. ([#13024](https://github.com/diegosouzapw/OmniRoute/pull/13024)) — thanks @ntdatt812
|
||||
- **feat(i18n):** 9 new locales — Greek (`el`), Croatian (`hr`), Serbian (`sr`), Lithuanian (`lt`), Estonian (`et`), Latvian (`lv`), Slovenian (`sl`), Maltese (`mt`), Irish (`ga`) — across the dashboard, docs mirrors, CLI, README and the site; OmniRoute now ships every official EU language (51 locales). (#13044)
|
||||
- **feat(i18n):** 8 new locales — Kannada (`kn`), Malayalam (`ml`), Odia (`or`), Punjabi (`pa`), Nepali (`ne`), Sinhala (`si`), Burmese (`my`), Khmer (`km`) — across the dashboard, docs mirrors, CLI, README and the site (59 locales). The translator now restores the ICU literal escape around angle placeholders and splits oversized docs sections before translating. (#0000) ([#13660](https://github.com/diegosouzapw/OmniRoute/pull/13660))
|
||||
- feat(providers): **list Agnes 3.0 Flash as the current free chat model, drop retired 1.5 Flash, add Image 2.0/2.5 Flash plus Video 2.5/2.5 Flash, and discover the live `/v1/models` catalog (including the CN host `api.agnes-ai.cn`).** `agnes-1.5-flash` now forwards to `agnes-3.0-flash`. Video 2.5 polls `GET /v1/videos/{id}` (not the V2.0 `/agnesapi` contract). Live `/v1/models` (2026-09-09) no longer serves 1.5; the wiki marks it deprecated. 3.0 Flash is 512K context / 65,536 max output, same window as 2.5. CN-region keys use the existing per-connection base-URL field, default stays `apihub.agnes-ai.com`. ([#13120](https://github.com/diegosouzapw/OmniRoute/pull/13120)) — thanks @HouMinXi
|
||||
- **feat(sse):** Codex and OpenAI catalogs list GPT-6 Astra with effort aliases (`-low` through `-ultra`); Codex CLI identity pins `@openai/codex@0.153.4` in lockstep with the image ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) — thanks @HouMinXi
|
||||
- **feat(dashboard):** the orchestration History tab gained a "Compare runs" mode — toggling it turns each grid cell into a 2-item selection queue (a 3rd click drops the oldest pick), and picking two cells opens a side-by-side comparison panel instead of the usual detail drawer. The panel fetches both runs' detail the same way the drawer does (falling back to persisted history once a run leaves the live TTL window) and shows, per side: identity/source/state, start time, a signed `right - left` delta for duration/cost/event count, the event timeline aligned by index, and any memory hits. One side's fetch failing never blocks the other, and a delta is only ever computed when both sides have a finite value — otherwise it renders "—", never `NaN`. Comparing two runs from different sources or skills still works; a banner marks the deltas as informational rather than hiding them, since the two runs aren't a strict apples-to-apples pair. ([#12677](https://github.com/diegosouzapw/OmniRoute/pull/12677))
|
||||
- **feat(dashboard):** badge a conversation that never reached a clean stop ([#12717](https://github.com/diegosouzapw/OmniRoute/pull/12717)) — thanks @hartmark
|
||||
- **feat(release):** reconcile-changelog tool + version-anchored fragment aggregation ([#12987](https://github.com/diegosouzapw/OmniRoute/pull/12987))
|
||||
- **feat(mcp):** carry context_length through MCP list_models_catalog projection (#12776) ([#13406](https://github.com/diegosouzapw/OmniRoute/pull/13406)) — thanks @KooshaPari
|
||||
- **feat(cli):** make startup readiness budget configurable (#13369) ([#13433](https://github.com/diegosouzapw/OmniRoute/pull/13433)) — thanks @KooshaPari
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
@@ -707,194 +694,6 @@ _By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.ma
|
||||
- **fix(i18n):** quote <name> placeholder in OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES description (#12505) ([#12769](https://github.com/diegosouzapw/OmniRoute/pull/12769)) — thanks @KooshaPari
|
||||
- **fix(api/pricing):** surface validation error message as string, not raw object (#12494) ([#12771](https://github.com/diegosouzapw/OmniRoute/pull/12771)) — thanks @KooshaPari
|
||||
- **fix(sse):** re-enable prompt compression for native Codex passthrough ([#12834](https://github.com/diegosouzapw/OmniRoute/pull/12834)) — thanks @thomasmaerz
|
||||
- **fix(dashboard):** model health tests for a provider node set to the Responses API now call `/v1/responses` with a Responses-shaped body instead of `/v1/chat/completions` — those models were reported as `Provider returned HTTP 200 but no text content` even though the same model answered normally through `/v1/responses` ([#13070](https://github.com/diegosouzapw/OmniRoute/issues/13070)) ([#13087](https://github.com/diegosouzapw/OmniRoute/pull/13087)) — thanks @ntdatt812
|
||||
- fix(routing): stop a round-robin combo's "opencode" targets from collapsing onto the opencode-zen connection (#11912) ([#13283](https://github.com/diegosouzapw/OmniRoute/pull/13283))
|
||||
- fix(routing): stop the reactive-compaction debug log from lying when compression is globally disabled (#11977) ([#13276](https://github.com/diegosouzapw/OmniRoute/pull/13276))
|
||||
- fix(electron): relativize standalone-bundle symlink targets so Stage 8 manifest verification stops failing on Windows (#11979) ([#13251](https://github.com/diegosouzapw/OmniRoute/pull/13251))
|
||||
- fix(dashboard): surface a visible error when Compression Studio's combined preview run fails (#12061) ([#13288](https://github.com/diegosouzapw/OmniRoute/pull/13288))
|
||||
- fix(dashboard): make the compression "Effective pipeline" preview honor the active profile and warn when the master switch is off (#12063) ([#13287](https://github.com/diegosouzapw/OmniRoute/pull/13287))
|
||||
- fix(providers): scope TinyCMS Web signer's DOM shims to each call instead of leaking them for the process lifetime, and surface a clean HTTP status on a non-JSON interception-toggles error (#12072) ([#13254](https://github.com/diegosouzapw/OmniRoute/pull/13254))
|
||||
- fix(guardrails): stop Vision Bridge from re-selecting a model locked after a 404 (#12111) ([#13259](https://github.com/diegosouzapw/OmniRoute/pull/13259))
|
||||
- fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129) ([#13278](https://github.com/diegosouzapw/OmniRoute/pull/13278))
|
||||
- fix(providers): minimax-m3 now collapses manual thinking.type:"enabled" to adaptive, preventing upstream 400 (2013) (#12132) ([#13249](https://github.com/diegosouzapw/OmniRoute/pull/13249))
|
||||
- fix(db): scope model visibility overrides by modality so hiding a Chat model no longer hides an identically-ID'd Image/Embeddings/etc. model (#12172) ([#13258](https://github.com/diegosouzapw/OmniRoute/pull/13258))
|
||||
- fix(db): scope local-provider apiKey dedup to matching base URL so LM Studio/Ollama-style connections support multiple accounts (#12173) ([#13250](https://github.com/diegosouzapw/OmniRoute/pull/13250))
|
||||
- fix(providers): refresh Trae's stale Referer/Origin and forward user timezone so imported connections stop failing with 401 (#12190) ([#13255](https://github.com/diegosouzapw/OmniRoute/pull/13255))
|
||||
- fix(providers): route opencode-go/gpt-5.6-luna to /responses instead of /chat/completions (#12196) ([#13275](https://github.com/diegosouzapw/OmniRoute/pull/13275))
|
||||
- fix(dashboard): allow deleting the last extra-upstream-header row even when invalid (#12251) ([#13252](https://github.com/diegosouzapw/OmniRoute/pull/13252))
|
||||
- **fix(i18n):** translate pre-existing `__MISSING__:` keys for `combo.sort`, `requestLogger.detail` expand/collapse, `common.profile`, and `settings.resilienceCredentialHealth*` across 39 locales ([#12272](https://github.com/diegosouzapw/OmniRoute/issues/12272)) ([#13034](https://github.com/diegosouzapw/OmniRoute/pull/13034)) — thanks @HouMinXi
|
||||
- fix(cli): run the Node.js runtime compatibility guard before the heavy `tsx/esm` + Commander import chain so an unsupported runtime gets a clear message instead of a raw `Invalid regular expression flags` crash (#12296) ([#13253](https://github.com/diegosouzapw/OmniRoute/pull/13253))
|
||||
- fix(dashboard): refresh the providers list after deleting a compatible provider node (#12298) ([#13256](https://github.com/diegosouzapw/OmniRoute/pull/13256))
|
||||
- fix(usage): fail closed on API-key budget enforcement when a provider's `auto` routing alias has no pricing row, instead of silently counting it as $0 (#12341) ([#13257](https://github.com/diegosouzapw/OmniRoute/pull/13257))
|
||||
- **fix(providers):** include the submitted Agnes video model when polling by `video_id` ([#12356](https://github.com/diegosouzapw/OmniRoute/pull/12356)) — thanks @pacocartones
|
||||
- **fix(routing):** custom OpenAI-compatible nodes now honor the saved Chat/Responses API type after edits instead of letting the node's original ID prefix override the live connection setting ([#11884](https://github.com/diegosouzapw/OmniRoute/issues/11884)). ([#12358](https://github.com/diegosouzapw/OmniRoute/pull/12358)) — thanks @pacocartones
|
||||
- fix(sse): surface an error instead of a silent empty 200 when a Claude stream closes with zero bytes (#12398) ([#13285](https://github.com/diegosouzapw/OmniRoute/pull/13285))
|
||||
- fix(oauth): warn before the dead localhost:8080 redirect in antigravity/gemini `oauth start` (#12413) ([#13265](https://github.com/diegosouzapw/OmniRoute/pull/13265))
|
||||
- fix(providers): stop devin-cli spawn error from double-closing the SSE controller (#12517) ([#13261](https://github.com/diegosouzapw/OmniRoute/pull/13261))
|
||||
- **fix(rerank):** clamp Voyage `top_k` to the documents actually sent after empty-string filtering, and honor `return_documents: false` in the NVIDIA response adapter (#12523 — thanks @pacocartones)
|
||||
- **fix(audio):** `/v1/audio/translations` now resolves combo names the way `/v1/audio/transcriptions` already does, so a combo that `GET /v1/models` advertises is fanned out to its targets instead of being rejected with `400 Invalid translation model: <combo>. Use format: provider/model`; literal `provider/model` ids and unknown bare names behave as before (#12536 — thanks @pacocartones)
|
||||
- **fix(gemini):** strip the JSON-Schema-2020-12 `prefixItems` keyword from Gemini tool schemas at every nesting level, so Claude Code tool definitions no longer fail with `400 Unknown name "prefixItems"` on Gemini models (#12540 — thanks @pacocartones)
|
||||
- **fix(api-manager):** Expose an accessible loading status while API keys are fetched instead of an empty accessibility tree (#12541 — thanks @pacocartones)
|
||||
- **fix(video):** Clamp `estimateJpegFrameBytes` at zero for padding-only payloads and build the three encode-side frame data URIs from `JPEG_FRAME_DATA_URI_PREFIX` instead of a repeated literal (#12543 — thanks @pacocartones)
|
||||
- **fix(devin):** accept Windows `DEVIN_AGENTIC_HOME` sandbox paths (`C:\...\.sandbox\...`) in the isolated-home check so the Devin Claude Bridge no longer fails closed on Windows ([#12405](https://github.com/diegosouzapw/OmniRoute/issues/12405)) (#12545 — thanks @pacocartones)
|
||||
- **fix(db):** Add `conversation_turn_nodes` and orphaned `agentic_conversations` to the auto-cleanup cycle under the existing `retention.callLogs` window, so identity nodes whose call-log content has already been purged no longer accumulate without bound in `storage.sqlite` (#12548 — thanks @pacocartones)
|
||||
- **fix(i18n):** Wrap the `~/.claude/profiles/<name>/settings.json` placeholder in ICU single quotes in the `OMNIROUTE_AUTO_SYNC_CLAUDE_PROFILES` feature-flag description across all 42 locales and the TypeScript default, so next-intl no longer fails with `INVALID_MESSAGE: UNCLOSED_TAG` and the Feature Flags card shows the description instead of the raw key (#12549 — thanks @pacocartones)
|
||||
- **fix(orchestration):** `updateCloudAgentTask` now publishes the task's real `status` on `agent.task.updated` when an update only touches `result`, `activities` or `error`, instead of the fabricated `"updated"` state, and stays silent when no row matched the id (#12550 — thanks @pacocartones)
|
||||
- **fix(i18n):** the home "Recent Requests" panel and the Provider Topology legend are now translated instead of rendering English copies on non-English dashboards; the legend reads its own `home.topologyLegend*` labels with consistent casing rather than borrowing the memory-settings "Recent" and analytics "Error" strings (#12551 — thanks @pacocartones).
|
||||
- **docs(reference):** bring the `FEATURE_FLAGS.md` catalog back to 1:1 with `featureFlagDefinitions.ts` — 20 missing flags added, the two `*_BLOCK_THRESHOLD` env-only knobs moved out of the flag tables, category/total counts and the Live WS port corrected, guarded by a static test (#12552 — thanks @pacocartones)
|
||||
- **fix(i18n):** backfill missing `usage.kiloPass*` strings in 39 locales and restore `featureFlagOmnirouteDisableThinkingLevelVariantsDescription` in `pt.json` ([#12561](https://github.com/diegosouzapw/OmniRoute/issues/12561)) ([#13027](https://github.com/diegosouzapw/OmniRoute/pull/13027)) — thanks @HouMinXi
|
||||
- fix(docker): default docker-compose app ports (dashboard/API/live-WS) to loopback instead of `0.0.0.0`, closing the anonymous `/v1` LAN/WAN exposure gap left open by `REQUIRE_API_KEY=false` (#12568) ([#13240](https://github.com/diegosouzapw/OmniRoute/pull/13240))
|
||||
- fix(api): close DNS-rebinding SSRF gap in webhook outbound-URL guard (#12569) ([#13243](https://github.com/diegosouzapw/OmniRoute/pull/13243))
|
||||
- fix(docker): require a per-session token on the VNC browser CDP bridge and isolate it on a dedicated Docker network (#12571) ([#13241](https://github.com/diegosouzapw/OmniRoute/pull/13241))
|
||||
- fix(open-sse): write Adobe Firefly session tokens and cookie jars with 0700/0600 permissions instead of the process umask (#12572) ([#13242](https://github.com/diegosouzapw/OmniRoute/pull/13242))
|
||||
- fix(api): remove hardcoded wildcard CORS in openai-to-gemini-sse.ts so the centralized fail-closed CORS gate is the sole source of `Access-Control-Allow-Origin` (#12573) ([#13239](https://github.com/diegosouzapw/OmniRoute/pull/13239))
|
||||
- fix(api): enforce API key policy (budget/rate-limit/schedule/endpoint scoping) on the ElevenLabs speech-to-text, text-to-speech and voices proxy routes (#12574) ([#13244](https://github.com/diegosouzapw/OmniRoute/pull/13244))
|
||||
- fix(sse): cap HuggingChat NDJSON body size and bound the read loop with the fetch timeout so a stalled or hostile upstream cannot buffer unbounded memory (#12577) ([#13282](https://github.com/diegosouzapw/OmniRoute/pull/13282))
|
||||
- fix(docker): scope the cliproxyapi/qdrant/bifrost sidecars to loopback by default and forward `CLIPROXYAPI_MANAGEMENT_KEY` into the cliproxyapi container so its management API is not left both unauthenticated and LAN-published (#12578) ([#13240](https://github.com/diegosouzapw/OmniRoute/pull/13240))
|
||||
- fix(api): create DB export temp paths with `fs.mkdtempSync` instead of predictable timestamps (#12579) ([#13237](https://github.com/diegosouzapw/OmniRoute/pull/13237))
|
||||
- Cline 401 bodies that say "re-authenticate your Cline account" classify as a refreshable OAuth token, not a terminal expired key. The cooling panel no longer labels every cooldown as a 429; it shows the recorded last error instead. (#12594) ([#13060](https://github.com/diegosouzapw/OmniRoute/pull/13060)) — thanks @HouMinXi
|
||||
- **fix(catalog):** degrade unknown combo targets instead of dropping LCD modalities, and persist OpenRouter `architecture.input_modalities` into the capability snapshot ([#12613](https://github.com/diegosouzapw/OmniRoute/issues/12613)) ([#13042](https://github.com/diegosouzapw/OmniRoute/pull/13042)) — thanks @HouMinXi
|
||||
- fix(providers): send `x-api-key` instead of `Authorization: Bearer` for OpenCode Zen's `/v1/responses` endpoint (Muse Spark Contributor models), fixing a 401 on OmniRoute's auth header (#12633) ([#13247](https://github.com/diegosouzapw/OmniRoute/pull/13247))
|
||||
- fix(sse): surface the actionable "Auggie CLI not found" message when the shell reports a missing `auggie` binary via exit code instead of a spawn error (#12645) ([#13263](https://github.com/diegosouzapw/OmniRoute/pull/13263))
|
||||
- **fix(config):** Persist deletions of built-in background-degradation entries — when a stored settings record exists its `degradationMap` is now authoritative instead of being merged under the defaults, so an entry the user removed in the dashboard no longer reappears on the next apply or restart ([#12424](https://github.com/diegosouzapw/OmniRoute/issues/12424)) ([#12647](https://github.com/diegosouzapw/OmniRoute/pull/12647)) — thanks @pacocartones
|
||||
- **fix(gamification):** action-count badge milestones (First Token, Token Consumer, Token Machine, Token Whale, and the token-sharing tier) are now backed by a durable `xp_action_counts` counter incremented in `addXp()`, instead of a live `COUNT(*)` over `xp_audit_log`. The audit log is pruned by `retention.xpAuditLog` (default 30 days), so on a default install those "lifetime" milestones were really "actions in the last 30 days" and unlocked badges could stop unlocking once old rows aged out. `getActionCount()` and `checkActionCountBadges()` now read the same durable source, and a migration backfills existing totals from the surviving audit rows ([#12546](https://github.com/diegosouzapw/OmniRoute/issues/12546)) ([#12651](https://github.com/diegosouzapw/OmniRoute/pull/12651)) — thanks @pacocartones
|
||||
- **fix(images):** `/v1/images/edits` now iterates a combo's targets the same way `/v1/images/generations` does (#9239) instead of flattening a bare combo to its first target. A combo whose first target is not edit-capable — or lacks credentials — now falls through to a later edit-capable target rather than hard-erroring, and missing credentials are skipped (not a hard `401`) to match the generations path. The per-target skip/terminal classification is extracted into a shared `runImageComboTargets` loop, so generations behavior is unchanged ([#12547](https://github.com/diegosouzapw/OmniRoute/issues/12547)). ([#12653](https://github.com/diegosouzapw/OmniRoute/pull/12653)) — thanks @pacocartones
|
||||
- fix(sse): add first-byte watchdog to the TLS-fingerprint transport so a stalled wreq body falls back instead of hanging for minutes (#12656) ([#13272](https://github.com/diegosouzapw/OmniRoute/pull/13272))
|
||||
- fix(sse): exempt tiny-budget reasoning probes from combo quality failure and surface persisted-cooldown skips in ALL_TARGETS_SKIPPED diagnostics (#12659) ([#13269](https://github.com/diegosouzapw/OmniRoute/pull/13269))
|
||||
- fix(models): declare the real ~1M contextLength for OpenCode Zen's Muse Spark 1.2 models instead of falling back to the 200000 provider default (#12681) ([#13247](https://github.com/diegosouzapw/OmniRoute/pull/13247))
|
||||
- fix(oauth): align codebuddy-cn OAuth User-Agent with the chat/usage CLI version to avoid WAF false positives (#12702) ([#13264](https://github.com/diegosouzapw/OmniRoute/pull/13264))
|
||||
- fix(dashboard): surface an authentication-required banner instead of silently blanking database settings for a guest session (#12709) ([#13270](https://github.com/diegosouzapw/OmniRoute/pull/13270))
|
||||
- **fix(resilience):** stop unbounded queue that hangs 6min until Aborted — gate, provider slot, and Bottleneck queue now share a per-connection `maxWaitMs` budget; `fail-closed` on exhaust (503); execution backstop `executionMaxWaitMs` overridable per connection with upstream clamp ([#12715](https://github.com/diegosouzapw/OmniRoute/pull/12715)) — thanks @maxmad64bis (with thanks to @Tushar49 for surfacing the slow-provider need in #12635)
|
||||
- **fix(auto-combo):** every mode pack now carries `quality` and `reliability` weights (`quality` 0.02, 0.03 in `quality-first`; `reliability` 0.03, 0.04 in `reliability-first`), so selecting a pack no longer silences either signal; `DEFAULT_WEIGHTS` is unchanged ([#12731](https://github.com/diegosouzapw/OmniRoute/pull/12731)) — thanks @maxmad64bis
|
||||
- **fix(sse):** the provider execution pipeline keeps the upstream error `code`/`type` again — both were lost when the non-streaming leg was extracted, so a config-class `422` (Antigravity missing project) degraded into a generic account cooldown instead of reaching the gate that recognises it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(db):** a legacy `call_logs` table no longer aborts startup — the provider-stats index is created after the column healing runs, not before it, so an install predating `request_type` opens instead of failing with `no such column` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(catalog):** a malformed row in the operator-writable custom-models blob no longer kills every `auto/*` pool ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- **fix(tests):** realign the guards that the same day's merges left asserting the old behavior — the provider-count total after GreenPT and EURouter, the GLM stream buffer slot that `#12925` turned into a declared parameter, and the injection scan bound that `#13104` reshaped from a head-only window into head-plus-tail ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#13349](https://github.com/diegosouzapw/OmniRoute/pull/13349))
|
||||
- fix(cache): fold tool_choice/tools/response_format into the semantic cache signature so a cached tool_calls response can no longer be replayed for a request whose tool policy forbids it (#12734) ([#13267](https://github.com/diegosouzapw/OmniRoute/pull/13267))
|
||||
- **fix(models):** stop treating provider-supplied `isFree:true`/`:free`/`0/0` flags as trusted unless the provider has a documented free tier — only the shipped free-tier catalog decides otherwise; fetched `isFree:true` trusted only for a free-tier provider, custom `isFree:true` via a trusted path ([#12744](https://github.com/diegosouzapw/OmniRoute/pull/12744)) — thanks @maxmad64bis
|
||||
- fix(memory): authenticate the internal /v1/rerank loopback call so memory reranking no longer silently degrades to unranked order when REQUIRE_API_KEY=true (#12745) ([#13268](https://github.com/diegosouzapw/OmniRoute/pull/13268))
|
||||
- fix(sse): parse Ollama Cloud's current usage markup (`$X of $Y used` aria-label, nested width style) (#12749) ([#13260](https://github.com/diegosouzapw/OmniRoute/pull/13260))
|
||||
- fix(cli): setup-opencode no longer sends an active context's management token to `/v1/models` when `--api-key`/`OMNIROUTE_API_KEY` is supplied — an explicit flag or the env var now always outranks the context's token, and the flag itself is no longer swallowed by the parent program's global `--api-key` option (#12783) ([#13246](https://github.com/diegosouzapw/OmniRoute/pull/13246))
|
||||
- fix(sse): register Arcee AI in the executor provider registry so requests reach api.arcee.ai instead of silently falling back to OpenAI (#12784) ([#13277](https://github.com/diegosouzapw/OmniRoute/pull/13277))
|
||||
- **fix(quota):** share one upstream quota read when identical saturation checks arrive at the same time, so traffic bursts don't multiply provider API calls ([#12787](https://github.com/diegosouzapw/OmniRoute/pull/12787)) — thanks @maxmad64bis
|
||||
- **fix(db):** ignore expired or invalid rate-limit cooldown writes so a stale timestamp can't lock a connection that should be usable — clearing still works as before ([#12788](https://github.com/diegosouzapw/OmniRoute/pull/12788)) — thanks @maxmad64bis
|
||||
- **fix(providers):** single source for provider order with `xao` ranking alongside `xai-oauth` ([#12790](https://github.com/diegosouzapw/OmniRoute/pull/12790)) — thanks @maxmad64bis
|
||||
- **fix(routing):** off-table models check the free-model catalog before inheriting premium prices, latency bootstraps from the observed pool median, async tiers read live database pricing with a 90-day freshness gate, and the tier cache invalidates on every pricing write ([#12792](https://github.com/diegosouzapw/OmniRoute/pull/12792)) — thanks @maxmad64bis
|
||||
- **fix(auto-combo):** snapshot scoring uses the observed per-connection account tier and per-model quality instead of neutral constants, and circuit-open providers rank lower at build time ([#12794](https://github.com/diegosouzapw/OmniRoute/pull/12794)) — thanks @maxmad64bis
|
||||
- **fix(pool):** empty `auto/*` pools now say why they are empty, and the models listing uses the same paid check as routing ([#12795](https://github.com/diegosouzapw/OmniRoute/pull/12795)) — thanks @maxmad64bis
|
||||
- fix(routing): recognize CLIProxyAPI's 'unknown provider for model' 400 as fallback-worthy (#12800) ([#13284](https://github.com/diegosouzapw/OmniRoute/pull/13284))
|
||||
- **fix(sse):** translate-mode streams now emit the estimated token counts as a trailing usage-only chunk before `[DONE]` when the upstream stays silent, so metered chat clients see totals instead of nothing ([#12828](https://github.com/diegosouzapw/OmniRoute/pull/12828)) — thanks @maxmad64bis
|
||||
- **fix(db):** provider stats stay truthful on empty and legacy databases: fallback counts default to `0` instead of `null`, latency averages read `null` (not `0`) when no durations were recorded, failures logged before the error-type column existed group under `pre_migration` instead of `unclassified`, and per-provider queries use two new composite indexes ([#12832](https://github.com/diegosouzapw/OmniRoute/pull/12832)) — thanks @maxmad64bis
|
||||
- fix(nvidia): fail open when a synced model catalog goes stale instead of gating forever (#12849) ([#13248](https://github.com/diegosouzapw/OmniRoute/pull/13248))
|
||||
- **fix(db):** WAL maintenance lives in its own module: a `TRUNCATE` checkpoint that hits a busy database now warns with its streak and retries once via `PASSIVE` instead of logging success, and closing-time checkpoints no longer report success on builds without a database file, with the busy totals visible in the authenticated monitoring health payload ([#12853](https://github.com/diegosouzapw/OmniRoute/pull/12853)) — thanks @maxmad64bis
|
||||
- **fix(health):** quota with no snapshots reads empty instead of a contradicting 0% ([#12857](https://github.com/diegosouzapw/OmniRoute/pull/12857)) — thanks @maxmad64bis
|
||||
- fix(a2a): accept the dashboard's own session cookie on /a2a so "Run message/send" no longer fails with "Unauthorized: missing or invalid API key" (#12888) ([#13271](https://github.com/diegosouzapw/OmniRoute/pull/13271))
|
||||
- **fix(logs):** the Logs grid's in-memory filter pass no longer discards rows the SQL query already matched — selecting an API key from the dropdown (which sends the key's id) returns its calls again, the Combo tab shows every combo instead of only those whose name contains a "1", and the model filter and search cover the same columns as the query ([#12896](https://github.com/diegosouzapw/OmniRoute/pull/12896)) — fixes [#12873](https://github.com/diegosouzapw/OmniRoute/issues/12873) — thanks @ntdatt812
|
||||
- **fix(a2a):** `/api/a2a/status` now builds the agent card from the request that asked for it, so a gateway reached at a non-localhost host no longer advertises `http://localhost:20128` as its A2A URL ([#12918](https://github.com/diegosouzapw/OmniRoute/pull/12918)). — thanks @ntdatt812
|
||||
- **fix(compression):** progressive aging now appends its `[COMPRESSED:aging:…]` annotation after a turn's `tool_result` blocks instead of in front of them, so Anthropic no longer rejects aged conversations with "`tool_use` ids were found without `tool_result` blocks immediately after" ([#12920](https://github.com/diegosouzapw/OmniRoute/pull/12920)). — thanks @ntdatt812
|
||||
- **fix(bedrock):** model import now resolves context limits for every vendor prefix instead of only `anthropic.*`, so `global.openai.gpt-5.6-*` no longer imports with a null `inputTokenLimit` and gets rejected pre-flight at the 200k default ([#12921](https://github.com/diegosouzapw/OmniRoute/pull/12921)). — thanks @ntdatt812
|
||||
- **fix(stream):** the 64 KB stream buffer GLM asks for is honoured instead of dropped, and the type error it caused no longer fails the API Route Typecheck gate on every open PR ([#12925](https://github.com/diegosouzapw/OmniRoute/pull/12925)) — thanks @ntdatt812
|
||||
- **fix(guardrails):** mask PII inside a `tool_result`'s nested content array, which the masker walked past while redacting its sibling block ([#12930](https://github.com/diegosouzapw/OmniRoute/pull/12930)) — thanks @ntdatt812
|
||||
- **fix(dashboard):** the Radar catalog table no longer leaves absent data unexplained — empty limits, unknown context windows, and unreported capabilities each explain themselves on hover, and a new check keeps it that way ([#12937](https://github.com/diegosouzapw/OmniRoute/pull/12937)) — thanks @maxmad64bis
|
||||
- **fix(sse):** geo-blocked opencode requests rotate to the next account proxy instead of failing, so one refused egress no longer aborts the whole chain ([#12941](https://github.com/diegosouzapw/OmniRoute/pull/12941)) — thanks @maxmad64bis
|
||||
- **fix(codex):** dashboard "clear cooldown" and the CAS recovery path now drop nested `codexScopeRateLimitedUntil` maps in the same write that nulls `rate_limited_until`, and fresh quota snapshots with headroom lift fallback-sourced scope cooldowns parked by quota preflight ([#12817](https://github.com/diegosouzapw/OmniRoute/issues/12817), [#12860](https://github.com/diegosouzapw/OmniRoute/issues/12860), [#12951](https://github.com/diegosouzapw/OmniRoute/pull/12951)) The dashboard reset-credit button (`consumeCodexResetCredit`) now exercises that same snapshot path after a successful redeem, so a filled quota bar is enough to unpark a leftover fallback Codex child without another manual clear. — thanks @HouMinXi
|
||||
- **fix(db):** resolve `sql-wasm.wasm` across global npm install and hoisted layouts, ensuring OmniRoute can boot cleanly on Node 24 when native `better-sqlite3` is uncompiled. ([#13035](https://github.com/diegosouzapw/OmniRoute/pull/13035)) — thanks @HouMinXi
|
||||
- **fix(api):** `DELETE /v1/batches/delete-completed` now sweeps only the calling API key's own completed batches (batches with no owner stay out of a key-scoped sweep on purpose), with an explicit instance-wide mode reserved for authenticated dashboard sessions, a 401 for a presented key that is unknown, revoked, deactivated, banned or expired (never falling through to the session branch), audit logging of every sweep, a sanitized 500 on failure, an owner-scoped file half (a key-scoped sweep never nulls a file another tenant owns) and an atomic sweep — chunked in 200-batch transactions in instance mode — so a mid-way error never leaves a batch pointing at a nulled file (GHSA-wvxc-jp3v-5mg5) ([#12969](https://github.com/diegosouzapw/OmniRoute/pull/12969))
|
||||
- **fix(combo):** quota-weighted routing stops drawing on an out-of-credit connection — a 402 now invalidates the stored quota snapshot instead of leaving its stale remaining percentage in place, and a snapshot older than 10 minutes no longer counts as confident headroom for the primary pool ([#12972](https://github.com/diegosouzapw/OmniRoute/pull/12972)) ([#13006](https://github.com/diegosouzapw/OmniRoute/pull/13006)) — thanks @HouMinXi
|
||||
- **fix(vision):** Custom Models with "Vision capable" checked no longer have image requests swapped to `glm/glm-4.6v` when the client sends the advertised alias (`vllm/path/...`) or the bare path-shaped id — Vision Bridge now matches the stored override for all three id forms ([#12758](https://github.com/diegosouzapw/OmniRoute/issues/12758)) ([#12974](https://github.com/diegosouzapw/OmniRoute/pull/12974)) — thanks @HouMinXi
|
||||
- **fix(sse):** transient opencode upstream failures rotate to the next account proxy instead of failing, so one flapping egress no longer aborts the whole chain ([#12975](https://github.com/diegosouzapw/OmniRoute/pull/12975)) — thanks @maxmad64bis
|
||||
- **fix(azure):** Deployments from GPT-6 onward now send `max_completion_tokens` instead of `max_tokens`, which Azure rejects with HTTP 400. The rule matched a literal `gpt-5`, so each new generation arrived broken; it now matches the generation range, while `gpt-35-turbo` still keeps `max_tokens`. ([#13007](https://github.com/diegosouzapw/OmniRoute/pull/13007)) — thanks @ntdatt812
|
||||
- **fix(db):** add an opt-in self-restart circuit for sustained critical memory pressure, gate post-cleanup VACUUM behind a minimum freed-rows threshold, and checkpoint the SQLite WAL every 5 minutes with a size guard that escalates to TRUNCATE, so a growing WAL can no longer stall the event loop into a full outage. ([#13011](https://github.com/diegosouzapw/OmniRoute/pull/13011)) — thanks @HouMinXi
|
||||
- **fix(auth):** an explicit connection pin may probe a quota-disabled row once and re-enable it on success ([#12874](https://github.com/diegosouzapw/OmniRoute/issues/12874)) ([#13017](https://github.com/diegosouzapw/OmniRoute/pull/13017)) — thanks @HouMinXi
|
||||
- **fix(api):** thread `X-OmniRoute-Fallback-Attempts` through combo chat completions so streaming and non-streaming responses report how many prior legs were attempted ([#13038](https://github.com/diegosouzapw/OmniRoute/pull/13038)) — thanks @HouMinXi
|
||||
- Restore provider failure classification and credential refresh on non-streaming requests: classify non-2xx failures to lock models on per-model quota exhaustion, update connection rate limits from headers and body, and pass credential refresh handlers to pipeline execution so 401 tokens can be refreshed and retried (#13043). ([#13069](https://github.com/diegosouzapw/OmniRoute/pull/13069)) — thanks @HouMinXi
|
||||
- **fix(responses):** wrap forced-non-streaming web_search fallback JSON as Responses SSE so Codex still sees `response.completed` ([#13050](https://github.com/diegosouzapw/OmniRoute/pull/13050)) — thanks @HouMinXi
|
||||
- **fix(antigravity):** Preserve upstream thought-token usage in normalized completion and reasoning token counts ([#13055](https://github.com/diegosouzapw/OmniRoute/pull/13055)) — thanks @pacocartones
|
||||
- Fix a concurrency-slot leak in the DeepSeek PoW solver: a worker that failed to spawn (for example a missing worker script) never released its slot, so `MAX_CONCURRENT_WORKERS` failures disabled the solver until restart. ([#13097](https://github.com/diegosouzapw/OmniRoute/pull/13097)) — thanks @anhtahaylove
|
||||
- **fix(acp):** bound the ACP session output buffers — `stdoutBuffer` and `stderrBuffer` now cap at 1 MiB keeping the most recent output behind a visible `[...output truncated...]` marker, and `stderrBuffer` is reset per prompt instead of accumulating for the lifetime of the session. ([#13100](https://github.com/diegosouzapw/OmniRoute/pull/13100)) — thanks @anhtahaylove
|
||||
- **fix(acp):** release the `stdout`/`exit` listeners and the idle timer that a `sendPrompt` timeout used to leave attached to the `acpManager` singleton, and drop sessions that exited on their own from the session map instead of keeping them forever. ([#13096](https://github.com/diegosouzapw/OmniRoute/pull/13096)) — thanks @anhtahaylove
|
||||
- **fix(security):** the prompt-injection and PII scanners now read the text a `tool_result` block carries on `content` (string or nested block list), in messages and in system blocks, so tool output is judged by the same rules as user text ([#13101](https://github.com/diegosouzapw/OmniRoute/pull/13101)) — thanks @ntdatt812
|
||||
- **fix(gamification):** close the badge notification SSE stream when the request signal is already aborted before the stream starts — a client that disconnects while the route is still awaiting auth used to leave both the 2s unlock poll and the 15s heartbeat running for the lifetime of the process. ([#13106](https://github.com/diegosouzapw/OmniRoute/pull/13106)) — thanks @anhtahaylove
|
||||
- **fix(security):** the prompt-injection scan now spends its 16 KB budget on both ends of the request instead of the first 16 KB only, so `system`, `instructions`, `query`, `documents` and the newest turns are no longer hidden behind one long message ([#13104](https://github.com/diegosouzapw/OmniRoute/pull/13104)) — thanks @ntdatt812
|
||||
- **fix(dashboard):** expose the Volcano Ark console cookie on quota scraping and unwrap connect-error objects so the dashboard shows the upstream message ([#13107](https://github.com/diegosouzapw/OmniRoute/pull/13107)) — thanks @HouMinXi
|
||||
- **fix(db):** release the `beforeExit`/`SIGINT`/`SIGTERM` handlers when a `node:sqlite` adapter closes, so a closed adapter and its database handle are no longer pinned to `process` for the lifetime of the run — the same treatment #7494 gave the sql.js adapter. ([#13109](https://github.com/diegosouzapw/OmniRoute/pull/13109)) — thanks @anhtahaylove
|
||||
- **fix(translator):** `contentSchema` and `unevaluatedItems` are now treated as subschema positions by the tool-schema sanitizer, so a truncation placeholder in either is replaced with a permissive schema instead of being forwarded as a string ([#13110](https://github.com/diegosouzapw/OmniRoute/pull/13110)) — thanks @ntdatt812
|
||||
- **fix(cli-helper):** clear the `createLogStream` timeout on the abort path — `stop()` aborts the in-flight fetch and returned through the `signal.aborted` branch, which skipped `clearTimeout` and left an armed timer per stopped stream. The stream reader is now also cancelled when the read loop exits early. ([#13114](https://github.com/diegosouzapw/OmniRoute/pull/13114)) — thanks @anhtahaylove
|
||||
- **fix(docs):** drop TLS-impersonation, MITM-decrypt, supply-chain attestation, and XOR-mask writeups from the public `/docs` catalog and Docker image. Files stay in git for engineers; operators who need them open the repo, not the website. ([#13136](https://github.com/diegosouzapw/OmniRoute/pull/13136)) — thanks @HouMinXi
|
||||
- **fix(combo):** parse numeric-epoch `rate_limited_until` in the combo cooldown read path ([#13141](https://github.com/diegosouzapw/OmniRoute/pull/13141)) — thanks @maxmad64bis
|
||||
- **fix(opencode):** both OpenCode plugins now reject a gateway address typed without `http://` at configuration time, instead of publishing every model with an api url no client can call, and the v2 plugin no longer publishes a model card whose api url is blank or relative ([#13142](https://github.com/diegosouzapw/OmniRoute/pull/13142)) — thanks @maxmad64bis
|
||||
- **fix(providers):** lock opencode model on upstream 400 model-unavailable ([#13146](https://github.com/diegosouzapw/OmniRoute/pull/13146)) — thanks @maxmad64bis
|
||||
- **fix(logging):** keep the provider exchange rather than the raw client bodies when a call log exceeds its size budget, and show that recovered payload in the request-detail panel instead of replacing it with the stored response body ([#13147](https://github.com/diegosouzapw/OmniRoute/pull/13147)) — thanks @maxmad64bis
|
||||
- **fix(traffic-inspector):** the WebSocket route no longer leaks a traffic-buffer subscriber and a 30s ping timer when the client socket is already closed at handler time — listeners are attached before any resource is acquired, a destroyed socket bails out early, and the ping interval stops on a dead socket where `write()` never throws ([#13155](https://github.com/diegosouzapw/OmniRoute/pull/13155)) — thanks @anhtahaylove
|
||||
- **fix(telegram):** bound the per-user API key cache in the Telegram chat proxy so a burst of distinct chat ids can no longer grow the process heap without limit ([#13165](https://github.com/diegosouzapw/OmniRoute/issues/13165)) ([#13166](https://github.com/diegosouzapw/OmniRoute/pull/13166)) — thanks @anhtahaylove
|
||||
- **fix(stream):** cancel the upstream response body when the JSON-to-SSE sniff unwinds on a body timeout, so a stalled upstream no longer pins the connection ([#13169](https://github.com/diegosouzapw/OmniRoute/issues/13169)) ([#13171](https://github.com/diegosouzapw/OmniRoute/pull/13171)) — thanks @anhtahaylove
|
||||
- **fix(telegram):** authenticate webhook deliveries with Telegram's `secret_token` so an unauthenticated caller can no longer mint API keys or spend upstream quota ([#13172](https://github.com/diegosouzapw/OmniRoute/issues/13172)) ([#13175](https://github.com/diegosouzapw/OmniRoute/pull/13175)) — thanks @anhtahaylove
|
||||
- **fix(models):** give discoverable Gemini 3.8 Flash ids their own 65536 output spec so Antigravity no longer clamps them to 16384 ([#13195](https://github.com/diegosouzapw/OmniRoute/pull/13195)) — thanks @HouMinXi
|
||||
- **fix(dashboard):** leftover catalog-removed provider rows (gemini-cli) can be listed and purged from the providers page ([#13067](https://github.com/diegosouzapw/OmniRoute/issues/13067)) ([#13197](https://github.com/diegosouzapw/OmniRoute/pull/13197)) — thanks @HouMinXi
|
||||
- **fix(auth):** a dashboard session now requires the `authenticated: true` claim that login, OIDC and the session refresh already emit — a JWT merely signed with `JWT_SECRET` (for example the Cursor CLI passthrough token, which any API-key holder can obtain) no longer verifies as the `auth_token` cookie on any route, the WebSocket handshake or the live server; existing sessions keep working ([#13298](https://github.com/diegosouzapw/OmniRoute/issues/13298)) ([#13375](https://github.com/diegosouzapw/OmniRoute/pull/13375))
|
||||
- **fix(api):** `DELETE /v1/batches/delete-completed` now applies the caller's API-key policy (endpoint allowlist, schedule, usage cap, rate limit) like every other `/v1` route, commits the key-scoped sweep per 200-batch chunk instead of holding one write lock for the whole sweep, refuses to loop without progress, and rejects a scope that names both a key and `allTenants` ([#13684](https://github.com/diegosouzapw/OmniRoute/pull/13684))
|
||||
- Restore the API-route typecheck gate: the non-streaming leg result lost its discriminated-union narrowing after the server-owned tool loop reassignment, producing 13 new TS2339 diagnostics in `chatCore.ts`. ([#12963](https://github.com/diegosouzapw/OmniRoute/pull/12963))
|
||||
- Restore four non-streaming behaviours the server-owned tool loop refactor dropped: client aborts map to 499 with the fixed `Request aborted` message, an aborted request no longer logs a synthetic `clientResponse`, Claude prompt-cache telemetry is recorded again, and a body that cannot be canonicalized no longer throws when the tool loop is off. ([#12990](https://github.com/diegosouzapw/OmniRoute/pull/12990))
|
||||
- **fix(skills):** The CLI registry parser now reads positionals declared with `.addArgument()`, not only those written inline in `.command()`. `tunnel create [type]` was being published as `tunnel create`, so the agent-skills sync gate reported drift on every branch and regenerating would have deleted the argument. ([#13009](https://github.com/diegosouzapw/OmniRoute/pull/13009)) — thanks @ntdatt812
|
||||
- **fix(combos):** dashboard combo test uses a short prompt, serial probes, and a 60s timeout so reasoning models and rate-limited free pools do not fail the health check ([#13001](https://github.com/diegosouzapw/OmniRoute/pull/13001)) — thanks @HouMinXi
|
||||
- fix(compression): terminate idle worker threads on eviction so long-running instances stop leaking OS threads and MessagePorts ([#13091](https://github.com/diegosouzapw/OmniRoute/pull/13091)) — thanks @anhtahaylove
|
||||
- Redact Google API keys of any length in error bodies: the pattern required exactly 39 characters, so shorter or longer `AIza…` credentials passed through unredacted. ([#12964](https://github.com/diegosouzapw/OmniRoute/pull/12964))
|
||||
- **fix(grok-cli):** a 402 "Grok Build usage balance exhausted" parks that Grok login as out of credit (Grok Build CLI, grok.com cookie, and xAI OAuth share the weekly pool). Combo routing then tries the next login instead of locking the model for every account in the pool ([#13061](https://github.com/diegosouzapw/OmniRoute/pull/13061)) — thanks @HouMinXi
|
||||
- fix(compression): spawn the LLMLingua worker with a file URL object so compression actually runs instead of silently failing open on Node ([#13093](https://github.com/diegosouzapw/OmniRoute/pull/13093)) — thanks @anhtahaylove
|
||||
- **fix(test):** run the local `test` and `test:unit` scripts at concurrency 4 so a full-suite run no longer exhausts the machine's commit charge and kills unrelated processes ([#13187](https://github.com/diegosouzapw/OmniRoute/pull/13187)) — thanks @anhtahaylove
|
||||
- **fix(monitoring):** `GET /api/monitoring/health` `credentialHealth` now includes a bounded `failedConnections` list (`connectionId`, `status`, sanitized `lastError`) when the probe-cache gauge `failed>0`, plus `source: probe-cache` and a cheap `staleDbNonOkCount` for sticky SQLite `test_status` on active rows. Documents that the live gauge is not `provider_connections.test_status`. ([#12876](https://github.com/diegosouzapw/OmniRoute/pull/12876)) — thanks @RaviTharuma
|
||||
- **fix(resilience):** an openai-compatible multi-upstream gateway no longer marks the whole connection `credits_exhausted` when a single upstream (or the `/models` / representative-model health probe) returns 402. Quota failures stay model-scoped; true connection-wide auth failures are unchanged. Single-credential 402 key disable ([#5239](https://github.com/diegosouzapw/OmniRoute/issues/5239)) is preserved. ([#12875](https://github.com/diegosouzapw/OmniRoute/pull/12875)) — thanks @RaviTharuma
|
||||
- **fix(dashboard):** nine fixes on the Orchestration Canvas, all diagnosed in the Phase 2 reviews. A source that fails now keeps the timestamp of its FIRST failure instead of being re-stamped every poll — the stale line said "since the last poll" no matter how long the source had been down, and the churn also defeated the snapshot's stable identity (it serializes the source list), so the canvas re-rendered on every tick while anything was broken. A source that HAD data and only then started failing is flagged too: previously only a source with no node at all got the warning, so a source that went down mid-session kept a healthy-looking node forever. The rest are pointwise: clicking a filter chip cancels the pending search debounce (left armed, it fired ~300ms later and silently reverted the chip); the search input carries an accessible name; `?state=running, failed` parses like the unpadded form instead of dropping the padded value; the CSV toggle helper is defined once in `model/urlParams.ts` rather than twice; the agents tab tells "nothing running" apart from "the filter matched nothing", offering a clear-filters button instead of setup links that would be wrong advice there; edges stop emitting SMIL particles above 40 simultaneously active edges, keeping the colored stroke; and the drawer's error banner clears when a retried action succeeds. ([#12983](https://github.com/diegosouzapw/OmniRoute/pull/12983))
|
||||
- **fix(i18n):** the nine locales added with the EU-language batch (Greek, Estonian, Irish, Croatian, Lithuanian, Latvian, Maltese, Slovenian, Serbian) were missing the eleven Orchestration Canvas keys that Phase 3 introduced, so the compare-runs panel and the "no runs match these filters" empty state fell back to English in those languages (`deepMergeFallback` substitutes English for an absent key, so nothing rendered blank — it rendered untranslated). The coverage gate does not catch this: it enforces an 80% floor per locale, and eleven missing keys out of ~13,000 leaves coverage at 99.9%. Translated for real in each language, calibrated against the wording each file already uses for "run", "filter" and "skill". ([#13294](https://github.com/diegosouzapw/OmniRoute/pull/13294))
|
||||
- **fix(dashboard):** five follow-ups from the Phase 2 Orchestration Canvas review. The History drawer now shows the memory section for runs that already left the live TTL window: the persisted `memory_hits` event is parsed into `metadata.memoryHits` with the same defensive validation the drawer applies, and — because that event is observability rather than a state transition — it no longer leaks into the timeline, where it had been inheriting the task's state and rendering as a duplicate transition. The A2A memory recall runs against its own 1.5s deadline instead of inheriting the memory backend's 30s one; overshooting degrades exactly like any other recall failure (no hits, task proceeds), and the timer is cleared on both paths. Repeating a Conductor run carries its `requirements.cli`/`requirements.model` forward, so the new run is pinned to the same runner profile and model rather than drifting to whatever the fleet picks. A successful repeat from the Agents tab now focuses the run it created, instead of leaving the operator on the finished one. And the auth test for `POST /api/conductor/tasks` moved into the shared `ROUTES` array rather than restating the pattern. ([#12988](https://github.com/diegosouzapw/OmniRoute/pull/12988))
|
||||
- fix(plugins): stop leaking an exit listener per plugin hook timeout, which triggered MaxListenersExceededWarning on plugins that ignore SIGTERM ([#13092](https://github.com/diegosouzapw/OmniRoute/pull/13092)) — thanks @anhtahaylove
|
||||
- **fix(validation):** Provider node edits no longer fail with a generic "Invalid request" when the optional daily-quota reset fields are left blank. The dashboard sends `dailyQuotaResetTimezone` and `dailyQuotaResetHour` as `null`, and only the hour accepted it. ([#13066](https://github.com/diegosouzapw/OmniRoute/issues/13066)) ([#13083](https://github.com/diegosouzapw/OmniRoute/pull/13083)) — thanks @ntdatt812
|
||||
- **fix(combo):** quota-aware expansion drops banned, inactive, missing, and wrong-provider connections before quota fetch or model dispatch; pins and allowlists stay selectors, not a bypass. Antigravity automatic exhaustion now requires a reported zero remaining, so a positive balance below 1% stays eligible. ([#13006](https://github.com/diegosouzapw/OmniRoute/pull/13006)) — thanks @HouMinXi
|
||||
- Fix an intermittent failure in the quota-weighted routing test suite: scores are a function of `Date.now()`, so peers with identical quota scored microseconds apart never tied and swapped order. ([#12946](https://github.com/diegosouzapw/OmniRoute/pull/12946))
|
||||
- fix(volcengine): resolve quota window mapping gaps, safe multi-connection console binding, and response cookie sanitization ([#12950](https://github.com/diegosouzapw/OmniRoute/pull/12950)) — thanks @HouMinXi
|
||||
- **fix(test):** resolve the WebDAV handler path with `fileURLToPath` so the suite's 37 WebDAV tests run on Windows instead of failing with a doubled `C:\C:\` drive prefix ([#13196](https://github.com/diegosouzapw/OmniRoute/pull/13196)) — thanks @anhtahaylove
|
||||
- **fix(ui):** fall back from invalid AnySearch icon alias (direct commit `9587e07b69`)
|
||||
- **fix(sse):** stop rebuilding a truncated summary from the collector's cap-dropped event array ([#12718](https://github.com/diegosouzapw/OmniRoute/pull/12718)) — thanks @hartmark
|
||||
- **fix(conversations):** show a pending spinner for unresolved tool nodes instead of "(empty)" ([#12727](https://github.com/diegosouzapw/OmniRoute/pull/12727)) — thanks @hartmark
|
||||
- **fix(streaming):** fail fast when an upstream stream produces only lifecycle/heartbeat events, never real content ([#12741](https://github.com/diegosouzapw/OmniRoute/pull/12741)) — thanks @hartmark
|
||||
- **fix(responses-continuation):** bridge the write-in-flight window with an in-memory pending store ([#12854](https://github.com/diegosouzapw/OmniRoute/pull/12854)) — thanks @hartmark
|
||||
- **fix(translator):** drop or map agent_message on Chat Completions fallback ([#12880](https://github.com/diegosouzapw/OmniRoute/pull/12880)) — thanks @RaviTharuma
|
||||
- **fix(api):** page and stream-complete GET /v1/models for large catalogs ([#12882](https://github.com/diegosouzapw/OmniRoute/pull/12882)) — thanks @RaviTharuma
|
||||
- **fix(quota):** align AUTH window labels with usage API and clarify 503 ([#12884](https://github.com/diegosouzapw/OmniRoute/pull/12884)) — thanks @RaviTharuma
|
||||
- **fix(ci):** clear two base-reds on release/v3.8.51 — mutation-coverage gate + image-only-model guard ([#12945](https://github.com/diegosouzapw/OmniRoute/pull/12945))
|
||||
- **fix(api):** scope batch bulk-delete to the calling API key ([#13211](https://github.com/diegosouzapw/OmniRoute/pull/13211))
|
||||
- **fix(tests):** align model-sync log assertion to shipped path redaction (base-red) ([#13227](https://github.com/diegosouzapw/OmniRoute/pull/13227))
|
||||
- **fix(test):** make the ToS heading guard actually require the parentheses ([#13228](https://github.com/diegosouzapw/OmniRoute/pull/13228))
|
||||
- **fix(quality):** register 4 drifted covering tests in stryker tap.testFiles ([#13229](https://github.com/diegosouzapw/OmniRoute/pull/13229))
|
||||
- **fix(logging):** mask *-api-key headers in request-log pipeline (#13273) ([#13401](https://github.com/diegosouzapw/OmniRoute/pull/13401)) — thanks @KooshaPari
|
||||
- **fix(tests):** correct stale Jina catalog prefix in custom-models test ([#13403](https://github.com/diegosouzapw/OmniRoute/pull/13403)) — thanks @KooshaPari
|
||||
- **fix(backend):** prune old backups after health-check-repair snapshots (#13308) ([#13404](https://github.com/diegosouzapw/OmniRoute/pull/13404)) — thanks @KooshaPari
|
||||
- **fix(backend):** guard memory_fts_au trigger with WHEN clause to stop FTS bloat (#13326) ([#13405](https://github.com/diegosouzapw/OmniRoute/pull/13405)) — thanks @KooshaPari
|
||||
- **fix(free-tiers):** remove obsolete Together $25 signup credit (#12526) ([#13407](https://github.com/diegosouzapw/OmniRoute/pull/13407)) — thanks @KooshaPari
|
||||
- **fix:** skip non-chat generation models in Test all models ([#13409](https://github.com/diegosouzapw/OmniRoute/pull/13409)) — thanks @KooshaPari
|
||||
- **fix(providers):** de-duplicate agy and antigravity model catalogs ([#13410](https://github.com/diegosouzapw/OmniRoute/pull/13410)) — thanks @KooshaPari
|
||||
- **fix(dashboard):** send provider in body for mini-playground web search ([#13411](https://github.com/diegosouzapw/OmniRoute/pull/13411)) — thanks @KooshaPari
|
||||
- **fix(resilience):** add CJK quota-exhaustion patterns to 429 classifier ([#13412](https://github.com/diegosouzapw/OmniRoute/pull/13412)) — thanks @KooshaPari
|
||||
- **fix(backend):** eval runner: failed calls no longer score as passed + dotAll regex ([#13413](https://github.com/diegosouzapw/OmniRoute/pull/13413)) — thanks @KooshaPari
|
||||
- **fix(backend):** eval runner opts out of output-style and memory injection ([#13414](https://github.com/diegosouzapw/OmniRoute/pull/13414)) — thanks @KooshaPari
|
||||
- **fix:** chain serial quarantine step in npm test script (fixes #13286) ([#13418](https://github.com/diegosouzapw/OmniRoute/pull/13418)) — thanks @KooshaPari
|
||||
- **fix(antigravity):** derive stable session ID from credentials for prompt-cache reuse (#13305) ([#13424](https://github.com/diegosouzapw/OmniRoute/pull/13424)) — thanks @KooshaPari
|
||||
- **fix(memory):** merge memory into top-level system field instead of unshifting at messages[0] (#13425) ([#13427](https://github.com/diegosouzapw/OmniRoute/pull/13427)) — thanks @KooshaPari
|
||||
- **fix(compression):** preserve instruction blocks from lossy engines ([#13523](https://github.com/diegosouzapw/OmniRoute/pull/13523)) — thanks @KooshaPari
|
||||
- **fix(compression):** preserve polarity words and newlines in ultra heuristic ([#13524](https://github.com/diegosouzapw/OmniRoute/pull/13524)) — thanks @KooshaPari
|
||||
- **fix(compression):** run user preservePatterns before built-in patterns (#13457) ([#13525](https://github.com/diegosouzapw/OmniRoute/pull/13525)) — thanks @KooshaPari
|
||||
- **fix(reasoning):** expand deepseek-v4 regex to match v4.1+ and add command-code provider (#13430) ([#13528](https://github.com/diegosouzapw/OmniRoute/pull/13528)) — thanks @KooshaPari
|
||||
- **fix(providers):** add alwaysReasons metadata for reka-flash-3 ([#13531](https://github.com/diegosouzapw/OmniRoute/pull/13531)) — thanks @KooshaPari
|
||||
- **fix(playground):** allow Compare columns to scroll independently ([#13532](https://github.com/diegosouzapw/OmniRoute/pull/13532)) — thanks @KooshaPari
|
||||
- **fix(security):** empty whitelist allows through to prevent admin lockout ([#13534](https://github.com/diegosouzapw/OmniRoute/pull/13534)) — thanks @KooshaPari
|
||||
- **fix(tests):** add DISABLE_IOREG_STRATEGY env var for macOS test compatibility (fixes #13467) ([#13539](https://github.com/diegosouzapw/OmniRoute/pull/13539)) — thanks @KooshaPari
|
||||
- **fix(zed-hosted):** map developer role and Gemini enums for Zed proxy ([#13543](https://github.com/diegosouzapw/OmniRoute/pull/13543)) — thanks @KooshaPari
|
||||
- **fix(analytics):** normalize provider aliases to canonical id in usage_history ([#13545](https://github.com/diegosouzapw/OmniRoute/pull/13545)) — thanks @KooshaPari
|
||||
- **fix(combo):** use traceId for call log id to prevent UNIQUE constraint failure ([#13546](https://github.com/diegosouzapw/OmniRoute/pull/13546)) — thanks @KooshaPari
|
||||
- **fix(auth):** check model lockout before returning synthetic noauth connection ([#13547](https://github.com/diegosouzapw/OmniRoute/pull/13547)) — thanks @KooshaPari
|
||||
- **fix(guardrails):** skip credential redaction for base64 image data URLs ([#13550](https://github.com/diegosouzapw/OmniRoute/pull/13550)) — thanks @KooshaPari
|
||||
- **fix(resilience):** increase requestQueue.maxWaitMs default from 15s to 30s ([#13553](https://github.com/diegosouzapw/OmniRoute/pull/13553)) — thanks @KooshaPari
|
||||
- **fix(docs):** document OMNIROUTE_READY_TIMEOUT_MS and allowlist the test-only DISABLE_IOREG_STRATEGY ([#13692](https://github.com/diegosouzapw/OmniRoute/pull/13692))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -1050,39 +849,6 @@ _By commits in `091589089c..c0f92ec98a`, author identities consolidated via `.ma
|
||||
- **docs(security):** document socket.yml scanner config + CI workflow link (#12575) ([#12764](https://github.com/diegosouzapw/OmniRoute/pull/12764)) — thanks @KooshaPari
|
||||
- **chore(ci):** guard commit identity in pre-commit to stop author misattribution ([#12772](https://github.com/diegosouzapw/OmniRoute/pull/12772))
|
||||
- **deps:** 17 Dependabot bumps — bump electron from 43.4.0 to 43.4.1 in /electron ([#11426](https://github.com/diegosouzapw/OmniRoute/pull/11426)); bump the development group across 1 directory with 8 updates ([#11428](https://github.com/diegosouzapw/OmniRoute/pull/11428)); bump the production group across 1 directory with 11 updates ([#11440](https://github.com/diegosouzapw/OmniRoute/pull/11440)); chore(deps): bump github/codeql-action/init from 4.37.7 to 4.37.8 ([#11925](https://github.com/diegosouzapw/OmniRoute/pull/11925)); chore(deps): bump github/codeql-action from 4.37.7 to 4.37.8 ([#11926](https://github.com/diegosouzapw/OmniRoute/pull/11926)); chore(deps): bump github/codeql-action/analyze from 4.37.7 to 4.37.8 ([#11927](https://github.com/diegosouzapw/OmniRoute/pull/11927)); build(deps): bump github/codeql-action/init from 4.37.8 to 4.37.9 ([#12345](https://github.com/diegosouzapw/OmniRoute/pull/12345)); build(deps): bump github/codeql-action/analyze from 4.37.8 to 4.37.9 ([#12346](https://github.com/diegosouzapw/OmniRoute/pull/12346)); bump the development group across 1 directory with 2 updates ([#12347](https://github.com/diegosouzapw/OmniRoute/pull/12347)); build(deps): bump github/codeql-action from 4.37.8 to 4.37.9 ([#12349](https://github.com/diegosouzapw/OmniRoute/pull/12349)); bump browserslist from 4.28.2 to 4.28.8 ([#12396](https://github.com/diegosouzapw/OmniRoute/pull/12396)); bump the production group across 1 directory with 4 updates ([#12399](https://github.com/diegosouzapw/OmniRoute/pull/12399)); bump @xmldom/xmldom ([#12500](https://github.com/diegosouzapw/OmniRoute/pull/12500)); bump qs from 6.15.2 to 6.16.0 ([#12512](https://github.com/diegosouzapw/OmniRoute/pull/12512)); bump @xmldom/xmldom from 0.9.10 to 0.9.12 ([#12513](https://github.com/diegosouzapw/OmniRoute/pull/12513)); bump fast-uri from 3.1.5 to 3.1.7 ([#12514](https://github.com/diegosouzapw/OmniRoute/pull/12514)); bump @humanfs/node from 0.16.7 to 0.16.8 ([#12515](https://github.com/diegosouzapw/OmniRoute/pull/12515))
|
||||
- Clear the `release/v3.8.51` `check:agent-skills-sync` base-red: regenerate `skills/cli-tunnel/SKILL.md` so the `tunnel create [type]` positional that #13009 taught the generator to read is reflected in the committed skill. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- Clear the `release/v3.8.51` typecheck base-red from #12731: the new `Mode pack ranking gates` candidates in `open-sse/services/autoCombo/__tests__/autoCombo.test.ts` omitted the required `provider`, `model` and `errorRate` fields of `ProviderCandidate`, failing `check:open-sse-typecheck` (and with it `Fast Quality Gates`) on every open PR. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- Clear the `release/v3.8.51` docs-sync base-red: EURouter (#13025) and GreenPT (#13024) took the live provider count to 358, leaving 7 STRICT drifts (`PROVIDER_REFERENCE.md`, 4 diagrams, 2 tier-flow images) plus the `AGENTS.md` / `llm.txt` / `package.json` count claims stale. ([#13216](https://github.com/diegosouzapw/OmniRoute/pull/13216))
|
||||
- **test(models):** Pin `CATALOG_BUILD_TIMEOUT_MS` in the #12058 canonical-catalog regression so a cold catalog build racing the 8s default bound no longer flakes the suite with a 500 ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **fix(ci):** Allow `@omniroute/opencode-plugin-v2/` in the npm pack policy — [#12870](https://github.com/diegosouzapw/OmniRoute/pull/12870) shipped the v2 plugin beside its v1 sibling without widening the allowlist, so every packed file under it read as an unexpected artifact and failed `check:pack-policy` ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **fix(ci):** Point the release-green validator's pack gate at the tree it is validating — the build-provenance guard defaulted to `origin/main`, which a release branch tip can never be an ancestor of mid-cycle, so `Package artifact` reported a false red on every run and the tarball boot-smoke cascaded off it ([#12732](https://github.com/diegosouzapw/OmniRoute/issues/12732)) ([#12959](https://github.com/diegosouzapw/OmniRoute/pull/12959))
|
||||
- **docs(checks):** keep doc counts honest — headings, rankings, catalog, weights, quality gate and scoring diagram now covered ([#12507](https://github.com/diegosouzapw/OmniRoute/pull/12507)) — thanks @maxmad64bis
|
||||
- **chore(lifecycle):** `check:model-lifecycle` now also diffs `DEFAULT_DEGRADATION_MAP` (the background-task redirect table) against the vendor lifecycle snapshot, refusing a retired id as source or target, with a table-driven unit test beside it. Three rows whose source the vendor had retired — `claude-sonnet-4-20250514`, `gemini-3-pro-preview` and `gpt-5.1-codex` (whose target `gpt-5.1-codex-mini` is retired too) — were dead code, since `checkLifecycle` answers 410 before the redirect runs; they are dropped (#12535 — thanks @pacocartones)
|
||||
- **test(compression):** cover idle worker eviction at the resource level — the pool must call `terminate()` and must not retain the worker's `MessagePort`, complementing the `exit`-event assertion added with the fix ([#13371](https://github.com/diegosouzapw/OmniRoute/pull/13371))
|
||||
- **chore(quality):** raise the `imageGeneration.ts` file-size ceiling for the image-only-model guard that clears the #12945 base-red ([#13392](https://github.com/diegosouzapw/OmniRoute/pull/13392))
|
||||
- **chore(quality):** type the combo-test route's JSON response bodies instead of casting them to `any`, drop the now-empty suppression entry, and remove the `hasPerModelQuota` import `chatCore.ts` stopped using when the failure-classification helper was extracted ([#13379](https://github.com/diegosouzapw/OmniRoute/pull/13379))
|
||||
- **chore(electron):** the desktop bundle no longer ships the root-level files of every translated docs mirror (`README.md`, `llm.txt`, `CLAUDE.md`, `GEMINI.md`, `CONTRIBUTING.md`, `SECURITY.md`, `CODE_OF_CONDUCT.md`) — the packaged app only reads `docs/i18n/<locale>/docs/**`, which stays. Saves ~11 MB on top of the translated CHANGELOGs already pruned. (#0000) ([#13235](https://github.com/diegosouzapw/OmniRoute/pull/13235))
|
||||
- **chore(quality):** retighten the `src/sse/handlers/chat.ts` unused-vars suppression count to the 8 violations that actually remain ([#13385](https://github.com/diegosouzapw/OmniRoute/pull/13385))
|
||||
- Bring the documented counts back in line with the code: 171 migrations (was 169) and 20 routing strategies (was 19), plus the regenerated `cli-tunnel` skill reference. ([#12970](https://github.com/diegosouzapw/OmniRoute/pull/12970))
|
||||
- Update the documented migration count to 172 after the call-logs provider-stats indexes landed. ([#13209](https://github.com/diegosouzapw/OmniRoute/pull/13209))
|
||||
- **chore(ci):** two gates that close the blind spots behind the exclusions above. `check:vitest-exclusions` requires every Vitest exclusion to name a tracking issue and to appear in `config/quality/vitest-exclusions.json` — the previous list grew to 62 files behind a comment pointing at an issue that had been closed for a month. `check-new-key-coverage` requires a key newly added to `en.json` to reach every locale; the existing coverage gate is a percentage floor per locale, so eleven absent keys out of ~13,000 left it at 99.9% while a whole feature shipped untranslated in nine languages. Both are diff-aware, so pre-existing debt stays frozen and neither needed a migration to turn on. ([#13233](https://github.com/diegosouzapw/OmniRoute/pull/13233))
|
||||
- **build:** root layout no longer loads Inter from `next/font/google`, so a production image build does not need fonts.googleapis.com ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026)) — thanks @HouMinXi
|
||||
- **docs(i18n):** bring the eight new `llm.txt` mirrors (km, kn, ml, my, ne, or, pa, si) up to the current provider and migration counts, so `check-docs-sync` stops rejecting every commit ([#13674](https://github.com/diegosouzapw/OmniRoute/pull/13674))
|
||||
- **refactor(ui):** flow surfaces (home topology, combo live studio, compression cockpit/waterfall/nodes, token-health badge) express state through the theme-aware `--orch-status-*` tokens instead of fixed dark-mode hex, so green/red/amber/grey stay legible in the light theme; dark mode is byte-identical. Categorical palettes (routing-strategy hues, compression-layer pills, provider brand colors) deliberately stay hex ([#12378](https://github.com/diegosouzapw/OmniRoute/issues/12378)) ([#13203](https://github.com/diegosouzapw/OmniRoute/pull/13203))
|
||||
- **chore(tests):** 51 test files that had been excluded from Vitest are running again, restoring roughly 350 assertions to the blocking suite. Each was measured individually first: of the 62 files parked behind the `// #8618 — pre-existing failure` comment, 51 pass against the current tree with no source change, so the exclusions had outlived the failures they were added for. The 11 that genuinely still fail stay excluded, but now point at a live tracker (#13204) rather than at #8618, which was closed in August while the list it tracked kept growing. ([#13233](https://github.com/diegosouzapw/OmniRoute/pull/13233))
|
||||
- **chore(quality):** drop two ESLint suppression entries whose violations no longer exist, so `eslint --suppressions-location` stops rejecting every commit that touches the surrounding files ([#13384](https://github.com/diegosouzapw/OmniRoute/pull/13384))
|
||||
- **chore(release):** bring main's post-tag fixes into release/v3.8.51 — sync-back step 2/2 (direct commit `21c488f210`)
|
||||
- **chore(deps):** patch toml and esbuild in the opencode plugin lockfiles ([#12965](https://github.com/diegosouzapw/OmniRoute/pull/12965))
|
||||
- **docs(changelog):** reconcile the v3.8.51 living section — fold 366 fragments, cover every cycle commit, credit every contributor ([#12971](https://github.com/diegosouzapw/OmniRoute/pull/12971))
|
||||
- **test(plugin):** wait for the v2 lazy-refresh tier instead of sleeping past it ([#12984](https://github.com/diegosouzapw/OmniRoute/pull/12984))
|
||||
- **chore(quality):** rebaseline chatCore.ts after the non-streaming regression fixes ([#13045](https://github.com/diegosouzapw/OmniRoute/pull/13045))
|
||||
- **docs(changelog):** add the missing bullet marker to the DeepSeek PoW fragment ([#13200](https://github.com/diegosouzapw/OmniRoute/pull/13200))
|
||||
- **chore(deps):** drain the Dependabot queue — 10 of 13 alerts ([#13213](https://github.com/diegosouzapw/OmniRoute/pull/13213))
|
||||
- **chore(ci):** make worktrees carry the hooks that enforce the identity gate ([#13236](https://github.com/diegosouzapw/OmniRoute/pull/13236))
|
||||
- **chore(deps):** pin csv-parse to ^7.0.2 via overrides (#13117) ([#13300](https://github.com/diegosouzapw/OmniRoute/pull/13300)) — thanks @KooshaPari
|
||||
- **chore(deps):** bump hono from ^4.12.34 to ^4.13.7 (#13148) ([#13301](https://github.com/diegosouzapw/OmniRoute/pull/13301)) — thanks @KooshaPari
|
||||
- **chore(deps):** pin joi to ^18.2.8 via overrides (#13085) ([#13302](https://github.com/diegosouzapw/OmniRoute/pull/13302)) — thanks @KooshaPari
|
||||
- **docs(providers):** add Microsoft 365 Copilot (BizChat) provider guide (#12779) ([#13340](https://github.com/diegosouzapw/OmniRoute/pull/13340)) — thanks @KooshaPari
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
@@ -1100,7 +866,6 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@amaleta](https://github.com/amaleta) | #11906, #11934 |
|
||||
| [@amartinawi](https://github.com/amartinawi) | #11575 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #11083, #11830, #11834, #11844 |
|
||||
| [@anhtahaylove](https://github.com/anhtahaylove) | #13091, #13092, #13093, #13096, #13097, #13100, #13106, #13109, #13114, #13155, #13166, #13171, #13175, #13187, #13196 |
|
||||
| [@aniruddhaadak80](https://github.com/aniruddhaadak80) | #11760 |
|
||||
| [@app](https://github.com/app) | #12554 |
|
||||
| [@arminanton](https://github.com/arminanton) | #11461, #11513 |
|
||||
@@ -1130,10 +895,10 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@gonisulaimann](https://github.com/gonisulaimann) | #12368, #12369, #12371 |
|
||||
| [@Gorillaz322](https://github.com/Gorillaz322) | #12207 |
|
||||
| [@groovecityJO](https://github.com/groovecityJO) | #12682 |
|
||||
| [@hartmark](https://github.com/hartmark) | #11434, #11452, #11473, #11499, #11703, #11983, #11984, #11985, #11986, #11988, #11989, #11990, #11994, #12221, #12293, #12445, #12446, #12447, #12448, #12460, #12461, #12623, #12646, #12650, #12680, #12717, #12718, #12727, #12741, #12854 |
|
||||
| [@hartmark](https://github.com/hartmark) | #11434, #11452, #11473, #11499, #11703, #11983, #11984, #11985, #11986, #11988, #11989, #11990, #11994, #12221, #12293, #12445, #12446, #12447, #12448, #12460, #12461, #12623, #12646, #12650, #12680 |
|
||||
| [@hizzt](https://github.com/hizzt) | #11894 |
|
||||
| [@hongnoul](https://github.com/hongnoul) | #11484 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #11411, #11414, #11512, #11518, #11520, #11641, #11642, #11643, #11687, #11779, #11849, #11850, #11851, #11915, #11916, #11918, #11919, #11920, #12013, #12017, #12033, #12042, #12106, #12139, #12166, #12169, #12171, #12205, #12213, #12312, #12325, #12487, #12488, #12495, #12504, #12557, #12566, #12590, #12591, #12624, #12626, #12632, #12637, #12678, #12696, #12697, #12711, #12733, #12746, #12767, #12770, #12789, #12803, #12805, #12811, #12866, #12868, #12899, #12926, #12934, #12950, #12951, #12974, #13001, #13006, #13011, #13017, #13026, #13027, #13034, #13035, #13038, #13042, #13050, #13060, #13061, #13069, #13107, #13120, #13136, #13195, #13197 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #11411, #11414, #11512, #11518, #11520, #11641, #11642, #11643, #11687, #11779, #11849, #11850, #11851, #11915, #11916, #11918, #11919, #11920, #12013, #12017, #12033, #12042, #12106, #12139, #12166, #12169, #12171, #12205, #12213, #12312, #12325, #12487, #12488, #12495, #12504, #12557, #12566, #12590, #12591, #12624, #12626, #12632, #12637, #12678, #12696, #12697, #12711, #12733, #12746, #12767, #12770, #12789, #12803, #12805, #12811, #12866, #12868, #12899, #12926, #12934 |
|
||||
| [@Hsia97](https://github.com/Hsia97) | #11624 |
|
||||
| [@jacobsparts](https://github.com/jacobsparts) | #11854, #12155, #12167 |
|
||||
| [@joglomedia](https://github.com/joglomedia) | #11980 |
|
||||
@@ -1143,11 +908,11 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@KaspaPulse](https://github.com/KaspaPulse) | #11389, #11469, #11628, #11666, #11910 |
|
||||
| [@keeltrace](https://github.com/keeltrace) | #12080, #12223 |
|
||||
| [@killer30001000](https://github.com/killer30001000) | #12178 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #12470, #12592, #12667, #12699, #12703, #12706, #12764, #12769, #12771, #13300, #13301, #13302, #13340, #13401, #13403, #13404, #13405, #13406, #13407, #13409, #13410, #13411, #13412, #13413, #13414, #13418, #13424, #13427, #13433, #13523, #13524, #13525, #13528, #13531, #13532, #13534, #13539, #13543, #13545, #13546, #13547, #13550, #13553 |
|
||||
| [@KooshaPari](https://github.com/KooshaPari) | #12470, #12592, #12667, #12699, #12703, #12706, #12764, #12769, #12771 |
|
||||
| [@kriptoburak](https://github.com/kriptoburak) | #11370 |
|
||||
| [@KrzysiekSko](https://github.com/KrzysiekSko) | #12425, #12432, #12673 |
|
||||
| [@marcelokarval](https://github.com/marcelokarval) | direct commit / report |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #1622, #11435, #11437, #11441, #11537, #11550, #11553, #11555, #11557, #11812, #11842, #11843, #11903, #12151, #12214, #12215, #12218, #12226, #12314, #12316, #12317, #12318, #12319, #12320, #12321, #12507, #12715, #12731, #12744, #12786, #12787, #12788, #12790, #12792, #12794, #12795, #12828, #12832, #12853, #12857, #12870, #12937, #12941, #12975, #13141, #13142, #13146, #13147 |
|
||||
| [@maxmad64bis](https://github.com/maxmad64bis) | #1622, #11435, #11437, #11441, #11537, #11550, #11553, #11555, #11557, #11812, #11842, #11843, #11903, #12151, #12214, #12215, #12218, #12226, #12314, #12316, #12317, #12318, #12319, #12320, #12321, #12870 |
|
||||
| [@mdigitalbh81](https://github.com/mdigitalbh81) | #12240 |
|
||||
| [@Meet6338-X](https://github.com/Meet6338-X) | #11598, #11609, #12110 |
|
||||
| [@morpheus9393](https://github.com/morpheus9393) | #11943 |
|
||||
@@ -1156,11 +921,11 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@NightStalker-87](https://github.com/NightStalker-87) | #12183 |
|
||||
| [@NoxzRCW](https://github.com/NoxzRCW) | #11879, #11880, #11881, #11882, #11883, #11935 |
|
||||
| [@ntdat812](https://github.com/ntdat812) | #11585, #12095 |
|
||||
| [@ntdatt812](https://github.com/ntdatt812) | #11368, #11573, #11574, #11576, #11577, #11580, #11582, #11583, #11588, #11589, #11590, #11591, #11592, #11593, #11672, #12177, #12180, #12873, #12918, #12920, #12921, #12925, #12930, #13007, #13009, #13024, #13025, #13083, #13087, #13101, #13104, #13110 |
|
||||
| [@ntdatt812](https://github.com/ntdatt812) | #11368, #11573, #11574, #11576, #11577, #11580, #11582, #11583, #11588, #11589, #11590, #11591, #11592, #11593, #11672, #12177, #12180 |
|
||||
| [@official-burak](https://github.com/official-burak) | #11542 |
|
||||
| [@opensource-elearning](https://github.com/opensource-elearning) | #12179, #12189, #12278, #12286 |
|
||||
| [@oyi77](https://github.com/oyi77) | #11408, #11409, #11421, #11505, #11677, #12036, #12110 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #11521, #11522, #11527, #11528, #11529, #11530, #11531, #11532, #11533, #11534, #11595, #11599, #11603, #11604, #11605, #11607, #11610, #11676, #11714, #11716, #11718, #11767, #11838, #11860, #11862, #11869, #11871, #11872, #11873, #11903, #11906, #11921, #11934, #12356, #12358, #12359, #12360, #12361, #12362, #12364, #12365, #12373, #12374, #12375, #12376, #12377, #12379, #12380, #12381, #12386, #12387, #12389, #12390, #12394, #12395, #12397, #12401, #12403, #12404, #12522, #12523, #12535, #12536, #12540, #12541, #12543, #12545, #12548, #12549, #12550, #12551, #12552, #12647, #12651, #12653, #13055 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #11521, #11522, #11527, #11528, #11529, #11530, #11531, #11532, #11533, #11534, #11595, #11599, #11603, #11604, #11605, #11607, #11610, #11676, #11714, #11716, #11718, #11767, #11838, #11860, #11862, #11869, #11871, #11872, #11873, #11903, #11906, #11921, #11934, #12359, #12360, #12361, #12362, #12364, #12365, #12373, #12374, #12375, #12376, #12377, #12379, #12380, #12381, #12386, #12387, #12389, #12390, #12394, #12395, #12397, #12401, #12403, #12404 |
|
||||
| [@patrykkopycinski](https://github.com/patrykkopycinski) | #11936, #11937, #12224 |
|
||||
| [@PauloFH](https://github.com/PauloFH) | #11509 |
|
||||
| [@PauloHSOliveira](https://github.com/PauloHSOliveira) | #12241 |
|
||||
@@ -1172,7 +937,7 @@ Thanks to everyone whose work landed in v3.8.51:
|
||||
| [@raheemuddin786](https://github.com/raheemuddin786) | #11491, #11839, #11840, #11841, #12003, #12230, #12231, #12232, #12233, #12234 |
|
||||
| [@rahilmavani](https://github.com/rahilmavani) | #11761 |
|
||||
| [@Rahulsharma0810](https://github.com/Rahulsharma0810) | #11771 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #11710, #11727, #11797, #11798, #11802, #11805, #11806, #11811, #12098, #12099, #12101, #12449, #12452, #12472, #12473, #12493, #12533, #12607, #12628, #12631, #12636, #12875, #12876, #12880, #12882, #12884 |
|
||||
| [@RaviTharuma](https://github.com/RaviTharuma) | #11710, #11727, #11797, #11798, #11802, #11805, #11806, #11811, #12098, #12099, #12101, #12449, #12452, #12472, #12473, #12493, #12533, #12607, #12628, #12631, #12636 |
|
||||
| [@rezjalibd](https://github.com/rezjalibd) | #12186 |
|
||||
| [@RhianB14](https://github.com/RhianB14) | #11692 |
|
||||
| [@rifqiawl](https://github.com/rifqiawl) | #11517, #11519, #11969 |
|
||||
|
||||
32
Dockerfile
32
Dockerfile
@@ -106,8 +106,7 @@ RUN test -f package-lock.json \
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
|
||||
npm ci --include=optional --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
|
||||
&& (cd node_modules/better-sqlite3 \
|
||||
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild --force_build=1) \
|
||||
&& test -f node_modules/better-sqlite3/build/Release/better_sqlite3.node \
|
||||
&& node /usr/local/lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js rebuild) \
|
||||
&& node -e "require('better-sqlite3')(':memory:').close()" \
|
||||
&& node -e "const wreq=require('wreq-js'); if(typeof wreq.createTransport!=='function') process.exit(1)"
|
||||
|
||||
@@ -226,19 +225,7 @@ ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
|
||||
|
||||
# Data directory inside Docker — must match the volume mount in docker-compose.yml
|
||||
ENV DATA_DIR=/app/data
|
||||
RUN mkdir -p /app/data && chown node:node /app /app/data
|
||||
|
||||
# #13679: default the PUBLISHED image to requiring an API key. A bare
|
||||
# `docker run -p 20128:20128 … diegosouzapw/omniroute` (README/QUICK-START
|
||||
# one-liners) does not pass `--env-file .env`, so without this default the
|
||||
# anonymous /v1 LLM proxy would be both keyless AND world-reachable on the
|
||||
# published container. This does NOT change the npm/CLI local-dev default
|
||||
# (`REQUIRE_API_KEY` stays `"false"` in featureFlagDefinitions.ts) — only the
|
||||
# shipped deployment artifact's posture. docker-compose.yml is unaffected: it
|
||||
# loads the operator's own `.env` (env_file:) which overrides this ENV, and
|
||||
# already binds loopback-only by default (#12568). Override with
|
||||
# `-e REQUIRE_API_KEY=false` for an intentionally keyless deployment.
|
||||
ENV REQUIRE_API_KEY=true
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# `npm run build` (build-next-isolated → assembleStandalone) bundles ALL runtime
|
||||
# files into .build/next/standalone/ — .next, node_modules, migrations, scripts,
|
||||
@@ -248,24 +235,23 @@ ENV REQUIRE_API_KEY=true
|
||||
# The old per-module overrides were therefore pure duplication and were removed
|
||||
# (build-output-isolation cleanup). See scripts/build/assembleStandalone.mjs
|
||||
# (EXTRA_MODULE_ENTRIES) for the single source of truth.
|
||||
COPY --chown=node:node --from=builder /app/.build/next/standalone ./
|
||||
COPY --from=builder /app/.build/next/standalone ./
|
||||
# better-sqlite3 is the one exception still copied explicitly: assembleStandalone
|
||||
# only syncs its native build/ dir; the JS wrapper (lib/, package.json) is left to
|
||||
# Next.js tracing. bootstrap-env requires SQLite BEFORE the standalone server
|
||||
# starts, so guarantee the complete package independent of trace behaviour.
|
||||
COPY --chown=node:node --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
|
||||
RUN test -f /app/node_modules/better-sqlite3/build/Release/better_sqlite3.node
|
||||
COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
|
||||
# migrations land at <standalone>/migrations via assembleStandalone; point the runtime at them.
|
||||
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
||||
|
||||
# Docker healthcheck script — not traced by Next.js standalone output, so copy
|
||||
# it explicitly. The HEALTHCHECK CMD references it as `node healthcheck.mjs`.
|
||||
COPY --chown=node:node --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
|
||||
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
|
||||
|
||||
# Every COPY above hands its files to the baked-in `node` non-root user
|
||||
# (UID/GID 1000) at copy time. Do NOT add a `RUN chown -R node:node /app`
|
||||
# afterwards: in the overlay filesystem changing ownership rewrites every file
|
||||
# into a new layer, which stored the ~2 GB standalone build twice (#13990).
|
||||
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
|
||||
# runtime process never holds root privileges. The chown happens after all
|
||||
# COPYs so it covers files originally owned by root in the builder stage.
|
||||
RUN chown -R node:node /app
|
||||
|
||||
EXPOSE 20128
|
||||
|
||||
|
||||
30
README.md
30
README.md
@@ -7,19 +7,19 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 359 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.62B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 359 providers — 150+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 359 AI providers · 150+ free tiers · ~1.47B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 💰 ~1.62B Free Tokens / Month
|
||||
## 💰 ~1.47B Free Tokens / Month
|
||||
|
||||
</div>
|
||||
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **489 free-tier entries across 35 recurring pool keys** and computes the token headline from the **17 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
> Stacking free tiers by hand is painful — dozens of SDKs, dozens of rate limits, and no idea how much you actually have. OmniRoute catalogs **452 free-tier entries across 34 recurring pool keys** and computes the token headline from the **16 pools with a published positive monthly budget plus five per-model Groq caps**, deduplicated by shared pool. Quotas that only open after a regional identity check (today: ModelScope) are shown apart, +~6M behind regional identity verification, and never summed into the headline. The result stays visible on the dashboard (`/dashboard/free-tiers`).
|
||||
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.62B free tokens per month steady, up to ~2.22B in the first month with signup credits, from 35 documented recurring pool keys covering 489 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 17 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, xKiro 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
<img src="./docs/diagrams/free-tier-budget.svg" width="100%" alt="OmniRoute free-tier budget card: ~1.47B free tokens per month steady, up to ~2.07B in the first month with signup credits, from 34 documented recurring pool keys covering 452 cataloged free-tier entries behind one endpoint. Honest pool-deduped math — each shared pool counted once, including 16 recurring pools with a published positive monthly token budget plus five per-model Groq caps; 13 providers are marked avoid in the terms-risk catalog so you decide. Budget bar includes Mistral 1B, Nara 210M, LLM7 150M, Groq 30M (five per-model caps) and smaller pools, plus first-month signup credits and permanently-free no-token-cap providers surfaced separately so they never inflate the headline. Live used/remaining on /dashboard/free-tiers."/>
|
||||
|
||||
> Animated summary of the live `/dashboard/free-tiers` page. Full methodology (pool dedupe, credit tiers, provider terms): **[docs/reference/FREE_TIERS.md](docs/reference/FREE_TIERS.md)**.
|
||||
>
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
|
||||
| ------------------------- | :-----: | :-----------------------: | :---------: |
|
||||
| 🌐 Providers | 290 | **357** | more queued |
|
||||
| 🌐 Providers | 290 | **352** | more queued |
|
||||
| 🧠 Unique chat model IDs | 1185 | **1312** | — |
|
||||
| 🖼️ Modality Bridge | — | 🆕 vision + audio + video | — |
|
||||
| 📡 Radar free catalog | — | 🆕 opt-in | — |
|
||||
@@ -101,7 +101,7 @@
|
||||
<tr>
|
||||
<td align="right"><b>⚙️ Features</b></td>
|
||||
<td align="center"><a href="#-combos--the-flagship">🎯 Combos</a></td>
|
||||
<td align="center"><a href="#-357-ai-providers--152-catalog-marked-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-352-ai-providers--154-catalog-marked-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI & MCP</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -233,7 +233,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 54 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint and 359 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 359 providers · up to 95% token savings on eligible workloads · $0 to start with 150+ free tiers and 53 recurring/keyless free-forever providers · 36 CLI/agent integrations through one config · OpenAI, Claude, Gemini and Responses API compatibility at /v1 · production controls including circuit breakers, TLS stealth, MCP 110 tools, A2A, memory, guardrails, evals and 39,000+ static test declarations across 5,100+ tracked test files."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -542,9 +542,9 @@ Pix copia-e-cola:
|
||||
|
||||
## 📡 OmniRoute Radar
|
||||
|
||||
The main free-tier headline remains **~1.62B tokens/month** from the documented,
|
||||
The main free-tier headline remains **~1.47B tokens/month** from the documented,
|
||||
pool-deduplicated catalog above. Temporary provider signup credits can separately lift the first
|
||||
month to **~2.22B**. Radar is an optional, signed catalog overlay for people who want fresher
|
||||
month to **~2.07B**. Radar is an optional, signed catalog overlay for people who want fresher
|
||||
free-model availability between OmniRoute releases; the community catalog and every existing free
|
||||
feature remain free.
|
||||
|
||||
@@ -629,13 +629,13 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
<td align="center" width="76"><picture><source media="(prefers-color-scheme:dark)" srcset="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-png@1.91.0/dark/goose.png"/><img src="https://cdn.jsdelivr.net/npm/@lobehub/icons-static-svg@1.91.0/icons/goose.svg" width="40" alt="Goose"/></picture><br/><sub><b>Goose</b></sub><br/><sub> </sub></td>
|
||||
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Open Interpreter"/><br/><sub><b>Open Interpreter</b></sub><br/><sub> </sub></td>
|
||||
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Warp AI"/><br/><sub><b>Warp AI</b></sub><br/><sub> </sub></td>
|
||||
<td align="center" width="76"><a href="https://deyin.ai"><img src="./public/deyin.svg" width="40" alt="deyin.ai"/><br/><sub><b>deyin.ai</b></sub><br/><sub> </sub></a></td>
|
||||
<td align="center" width="76"><img src="./public/providers/cli-generic.svg" width="40" alt="Agent Deck"/><br/><sub><b>Agent Deck</b></sub><br/><sub> </sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<b>+ also works with</b> · Agent Deck · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
|
||||
<b>+ also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
|
||||
</div>
|
||||
|
||||
<sub>📖 Per-tool setup for all 36 tools (26 CLI Code's + 10 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
@@ -668,11 +668,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 357 AI Providers — 152 Catalog-Marked Free
|
||||
## 🌐 352 AI Providers — 152 Catalog-Marked Free
|
||||
|
||||
</div>
|
||||
|
||||
> **357 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **491 per-model rows**, **35 recurring pools** and **54 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
> **352 registered providers** across the canonical chat, media, search, local, cloud-agent and system collections, including **152 carrying `hasFree: true` discovery metadata**. The chat model registry covers **229 providers / 2,554 distinct provider-model pairs / 1,283 raw model IDs**; the separate free-budget catalog has **443 per-model rows**, **34 recurring pools** and **53 recurring/keyless free-forever providers**. These are different denominators by design; definitions and pool-deduped calculations live in the [Provider Reference](docs/reference/PROVIDER_REFERENCE.md) and [Free Tiers](docs/reference/FREE_TIERS.md).
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1268,7 +1268,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b>Runtime</b></td><td>Node.js 22.x / 24.x LTS — <code>>=22.22.2 <23 || >=24.0.0 <27</code></td></tr>
|
||||
<tr><td nowrap><b>Language</b></td><td>TypeScript 6.0 — <b>100% TypeScript</b> across <code>src/</code> and <code>open-sse/</code> (zero <code>any</code> in core since v2.0)</td></tr>
|
||||
<tr><td nowrap><b>Framework</b></td><td>Next.js 16 + React 19 + Tailwind CSS 4</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 178 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 122 domain modules, 176 migrations</td></tr>
|
||||
<tr><td nowrap><b>Memory</b></td><td>SQLite FTS5 full-text + int8-quantized vector embeddings, typed decay</td></tr>
|
||||
<tr><td nowrap><b>Schemas</b></td><td>Zod 4 — MCP tool I/O validation + API contracts</td></tr>
|
||||
<tr><td nowrap><b>Protocols</b></td><td>MCP (stdio / HTTP / SSE) + A2A v0.3 (JSON-RPC 2.0 + SSE)</td></tr>
|
||||
@@ -1331,7 +1331,7 @@ Métricas canônicas em 2026-08-24: **1.029 vídeos únicos** · **11.132.922 vi
|
||||
<tr><td nowrap><b><a href="docs/architecture/RESILIENCE_GUIDE.md">Resilience Guide</a></b></td><td>Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/routing/AUTO-COMBO.md">Auto-Combo Engine</a></b></td><td>16-factor scoring, mode packs, self-healing</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/ops/PROXY_GUIDE.md">Proxy Guide</a></b></td><td>3-level proxy system, 1proxy marketplace, registry CRUD</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 35 documented recurring pools / 489 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/FREE_TIERS.md">Free Tiers</a></b></td><td>Consolidated directory: 34 documented recurring pools / 452 cataloged free-tier entries</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/guides/FEATURES.md">Features Gallery</a></b></td><td>Visual dashboard tour with screenshots</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/architecture/CODEBASE_DOCUMENTATION.md">Codebase Documentation</a></b></td><td>Beginner-friendly codebase walkthrough</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -218,8 +218,6 @@ These rules are enforced by tooling and reviewers:
|
||||
|
||||
## Supply-chain scanner findings (Socket.dev / Snyk / similar)
|
||||
|
||||
> **Scope note:** `socket.yml` at the repository root only shapes `projectIgnorePaths` for Socket.dev's registry-side post-publish scan of the published npm artifact — it is not an enforced CI/PR merge gate. No workflow in `.github/workflows`, no `package.json` script, and no `Makefile` target invokes Socket.dev.
|
||||
|
||||
The published `omniroute` npm artifact bundles the Next.js `output: "standalone"`
|
||||
build, which means every route handler — including documented privileged
|
||||
features (MITM, Zed import, Cloud Sync, embedded service supervisor) — ends
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* OmniRoute Antigravity Bridge Proxy
|
||||
*
|
||||
* Intercepts Antigravity CLI and IDE requests:
|
||||
* - Directs Gemini 3.8 models directly to Google backend (100% native, untouched).
|
||||
* - Directs other models (Claude Sonnet 4.5/4.6, Opus, Gemini 3.7, GPT-OSS, etc.) to OmniRoute /v1/antigravity.
|
||||
* - Passes all non-model Google requests (auth, onboarding, telemetry) directly to Google backend.
|
||||
* - Transparently forwards all other non-target internet traffic.
|
||||
*/
|
||||
|
||||
import net from "node:net";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import tls from "node:tls";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const PORT = parseInt(process.env.BRIDGE_PORT || "20129", 10);
|
||||
const ROUTER_URL = process.env.ROUTER_URL || "http://127.0.0.1:20128/v1/antigravity";
|
||||
const ROUTER_API_KEY =
|
||||
process.env.ROUTER_API_KEY || process.env.OMNIROUTE_API_KEY || "sk-omniroute-bridge-local";
|
||||
|
||||
// Connection pool agents with TCP keep-alive
|
||||
const httpAgent = new http.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 60000,
|
||||
maxSockets: 64,
|
||||
maxFreeSockets: 16,
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
const httpsAgent = new https.Agent({
|
||||
keepAlive: true,
|
||||
keepAliveMsecs: 60000,
|
||||
maxSockets: 64,
|
||||
maxFreeSockets: 16,
|
||||
timeout: 120000,
|
||||
});
|
||||
|
||||
let cachedSslOptions = null;
|
||||
function getSslOptions() {
|
||||
if (cachedSslOptions) return cachedSslOptions;
|
||||
const certDir =
|
||||
process.env.CERT_DIR || path.join(process.env.HOME || process.cwd(), ".omniroute", "mitm");
|
||||
const serverKey = path.join(certDir, "server.key");
|
||||
const serverCrt = path.join(certDir, "server.crt");
|
||||
|
||||
if (!fs.existsSync(serverKey) || !fs.existsSync(serverCrt)) {
|
||||
console.error("❌ Certificate files not found in", certDir);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
cachedSslOptions = {
|
||||
key: fs.readFileSync(serverKey),
|
||||
cert: fs.readFileSync(serverCrt),
|
||||
};
|
||||
return cachedSslOptions;
|
||||
}
|
||||
|
||||
const TARGET_HOSTS = new Set([
|
||||
"cloudcode-pa.googleapis.com",
|
||||
"daily-cloudcode-pa.googleapis.com",
|
||||
"daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
"autopush-cloudcode-pa.sandbox.googleapis.com",
|
||||
"preprod-daily-cloudcode-pa.sandbox.googleapis.com",
|
||||
"antigravity-unleash.goog",
|
||||
]);
|
||||
|
||||
function isGenerationRequest(url) {
|
||||
if (!url) return false;
|
||||
return (
|
||||
url.includes(":generateContent") ||
|
||||
url.includes(":streamGenerateContent") ||
|
||||
url.includes("/GenerateChat") ||
|
||||
url.includes("/StreamGenerateChat") ||
|
||||
url.includes("/GenerateCode") ||
|
||||
url.includes("/CompleteCode")
|
||||
);
|
||||
}
|
||||
|
||||
function extractModel(body, url) {
|
||||
if (body && typeof body === "object") {
|
||||
if (typeof body.model === "string" && body.model) return body.model;
|
||||
if (body.request && typeof body.request.model === "string" && body.request.model) {
|
||||
return body.request.model;
|
||||
}
|
||||
}
|
||||
if (url) {
|
||||
try {
|
||||
const parsed = new URL(url, "https://cloudcode-pa.googleapis.com");
|
||||
const m = parsed.searchParams.get("model");
|
||||
if (m) return m;
|
||||
} catch {}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
const MODEL_ROUTING_MAP = {
|
||||
// Official OmniRoute Auto Groups
|
||||
"auto/best-fast": "groq/openai/gpt-oss-120b",
|
||||
"auto/best-coding": "mistral/codestral-latest",
|
||||
"auto/best-reasoning": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"auto/best-free": "groq/qwen/qwen3.8-27b",
|
||||
"auto/best-vision": "nvidia/meta/llama-3.2-90b-vision-instruct",
|
||||
"auto/coding:pro": "mistral/codestral-latest",
|
||||
"auto/coding:fast": "groq/openai/gpt-oss-120b",
|
||||
"auto/coding:free": "groq/qwen/qwen3.8-27b",
|
||||
"auto/coding:reliable": "mistral/codestral-latest",
|
||||
"auto/reasoning:pro": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"auto/smart": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"auto/claude-sonnet": "mistral/codestral-latest",
|
||||
"auto/claude-opus": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"auto/gemini": "gemini/gemini-2.5-flash",
|
||||
"auto/llama": "groq/openai/gpt-oss-120b",
|
||||
"auto/gemma": "groq/qwen/qwen3.8-27b",
|
||||
|
||||
// Human-readable Display Names (in case CLI sends displayName in envelope)
|
||||
"Auto: Best Fast (OmniRoute)": "groq/openai/gpt-oss-120b",
|
||||
"Auto: Best Coding (OmniRoute)": "mistral/codestral-latest",
|
||||
"Auto: Best Reasoning (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"Auto: Best Free (OmniRoute)": "groq/qwen/qwen3.8-27b",
|
||||
"Auto: Best Vision (OmniRoute)": "nvidia/meta/llama-3.2-90b-vision-instruct",
|
||||
"Auto: Coding Pro (OmniRoute)": "mistral/codestral-latest",
|
||||
"Auto: Coding Fast (OmniRoute)": "groq/openai/gpt-oss-120b",
|
||||
"Auto: Coding Free (OmniRoute)": "groq/qwen/qwen3.8-27b",
|
||||
"Auto: Coding Reliable (OmniRoute)": "mistral/codestral-latest",
|
||||
"Auto: Reasoning Pro (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"Auto: Smart (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"Auto: Claude Sonnet (OmniRoute)": "mistral/codestral-latest",
|
||||
"Auto: Claude Opus (OmniRoute)": "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
"Auto: Gemini (OmniRoute)": "gemini/gemini-2.5-flash",
|
||||
"Auto: Llama (OmniRoute)": "groq/openai/gpt-oss-120b",
|
||||
"Auto: Gemma (OmniRoute)": "groq/qwen/qwen3.8-27b",
|
||||
|
||||
// Fail-safe self-healing for dead/retired models
|
||||
"nvidia/deepseek-ai/deepseek-v4-pro-0813": "groq/openai/gpt-oss-120b",
|
||||
"deepseek-ai/deepseek-v4-pro-0813": "groq/openai/gpt-oss-120b",
|
||||
"NVIDIA: DeepSeek V4 Pro": "groq/openai/gpt-oss-120b",
|
||||
"nvidia/openai/gpt-oss-120b": "groq/openai/gpt-oss-120b",
|
||||
"openai/gpt-oss-120b": "groq/openai/gpt-oss-120b",
|
||||
"groq/llama-3.3-70b-versatile": "groq/openai/gpt-oss-120b",
|
||||
"llama-3.3-70b-versatile": "groq/openai/gpt-oss-120b",
|
||||
};
|
||||
|
||||
function resolveTargetModel(model) {
|
||||
if (!model) return "groq/openai/gpt-oss-120b";
|
||||
if (MODEL_ROUTING_MAP[model]) return MODEL_ROUTING_MAP[model];
|
||||
const clean = model.replace(/^models\//, "").trim();
|
||||
if (MODEL_ROUTING_MAP[clean]) return MODEL_ROUTING_MAP[clean];
|
||||
for (const [k, v] of Object.entries(MODEL_ROUTING_MAP)) {
|
||||
if (k.toLowerCase() === model.toLowerCase() || k.toLowerCase() === clean.toLowerCase()) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
if (
|
||||
clean.includes("deepseek-v4-pro") ||
|
||||
(clean.startsWith("nvidia") && clean.includes("gpt-oss-120b")) ||
|
||||
clean.includes("llama-3.3-70b-versatile")
|
||||
) {
|
||||
return "groq/openai/gpt-oss-120b";
|
||||
}
|
||||
return clean;
|
||||
}
|
||||
|
||||
const OMNIROUTE_BUILTIN_GROUPS = [
|
||||
{
|
||||
id: "auto/best-coding",
|
||||
displayName: "Auto: Best Coding (OmniRoute)",
|
||||
descriptionText:
|
||||
"OmniRoute dynamic routing to the highest benchmark coding model available (Mistral Codestral)",
|
||||
},
|
||||
{
|
||||
id: "auto/best-reasoning",
|
||||
displayName: "Auto: Best Reasoning (OmniRoute)",
|
||||
descriptionText:
|
||||
"OmniRoute dynamic routing to the highest benchmark reasoning model available (Nemotron 3 Super 120B)",
|
||||
},
|
||||
{
|
||||
id: "auto/best-fast",
|
||||
displayName: "Auto: Best Fast (OmniRoute)",
|
||||
descriptionText: "OmniRoute sub-second lowest latency high-throughput model (Groq LPUs)",
|
||||
},
|
||||
{
|
||||
id: "auto/best-vision",
|
||||
displayName: "Auto: Best Vision (OmniRoute)",
|
||||
descriptionText: "OmniRoute multimodal & computer vision routing",
|
||||
},
|
||||
{
|
||||
id: "auto/best-free",
|
||||
displayName: "Auto: Best Free (OmniRoute)",
|
||||
descriptionText: "OmniRoute 100% unmetered free tier model routing (Qwen 3.8 27B)",
|
||||
},
|
||||
{
|
||||
id: "auto/coding:pro",
|
||||
displayName: "Auto: Coding Pro (OmniRoute)",
|
||||
descriptionText: "OmniRoute frontier pro-tier coding model (Codestral)",
|
||||
},
|
||||
{
|
||||
id: "auto/coding:fast",
|
||||
displayName: "Auto: Coding Fast (OmniRoute)",
|
||||
descriptionText: "OmniRoute fast sub-second daily coding model (Groq 120B)",
|
||||
},
|
||||
{
|
||||
id: "auto/coding:free",
|
||||
displayName: "Auto: Coding Free (OmniRoute)",
|
||||
descriptionText: "OmniRoute zero-cost free coding model",
|
||||
},
|
||||
{
|
||||
id: "auto/coding:reliable",
|
||||
displayName: "Auto: Coding Reliable (OmniRoute)",
|
||||
descriptionText: "OmniRoute maximum uptime and reliability coding model",
|
||||
},
|
||||
{
|
||||
id: "auto/reasoning:pro",
|
||||
displayName: "Auto: Reasoning Pro (OmniRoute)",
|
||||
descriptionText: "OmniRoute deep reasoning frontier model",
|
||||
},
|
||||
{
|
||||
id: "auto/smart",
|
||||
displayName: "Auto: Smart (OmniRoute)",
|
||||
descriptionText: "OmniRoute highest intelligence general-purpose model",
|
||||
},
|
||||
{
|
||||
id: "auto/claude-sonnet",
|
||||
displayName: "Auto: Claude Sonnet (OmniRoute)",
|
||||
descriptionText: "OmniRoute automated routing across Claude Sonnet providers",
|
||||
},
|
||||
{
|
||||
id: "auto/claude-opus",
|
||||
displayName: "Auto: Claude Opus (OmniRoute)",
|
||||
descriptionText: "OmniRoute automated routing across Claude Opus providers",
|
||||
},
|
||||
{
|
||||
id: "auto/gemini",
|
||||
displayName: "Auto: Gemini (OmniRoute)",
|
||||
descriptionText: "OmniRoute automated routing across Gemini providers",
|
||||
},
|
||||
{
|
||||
id: "auto/llama",
|
||||
displayName: "Auto: Llama (OmniRoute)",
|
||||
descriptionText: "OmniRoute automated routing across Llama providers",
|
||||
},
|
||||
{
|
||||
id: "auto/gemma",
|
||||
displayName: "Auto: Gemma (OmniRoute)",
|
||||
descriptionText: "OmniRoute automated routing across Gemma providers",
|
||||
},
|
||||
// Active, verified provider models
|
||||
{
|
||||
id: "groq/openai/gpt-oss-120b",
|
||||
displayName: "Groq: GPT-OSS 120B (Ultra-Fast 0.02s)",
|
||||
descriptionText: "Ultra-fast inference on Groq LPUs at sub-second speeds",
|
||||
},
|
||||
{
|
||||
id: "groq/qwen/qwen3.8-27b",
|
||||
displayName: "Groq: Qwen 3.8 27B",
|
||||
descriptionText: "High-speed Qwen 3.8 27B model on Groq",
|
||||
},
|
||||
{
|
||||
id: "mistral/codestral-latest",
|
||||
displayName: "Mistral: Codestral Latest",
|
||||
descriptionText: "Mistral flagship frontier code reasoning model",
|
||||
},
|
||||
{
|
||||
id: "nvidia/nvidia/nemotron-3-super-120b-a12b",
|
||||
displayName: "NVIDIA: Nemotron 3 Super 120B",
|
||||
descriptionText: "Nemotron 3 Super 120B Deep Reasoning model on NVIDIA NIM",
|
||||
},
|
||||
{
|
||||
id: "gemini/gemini-2.5-flash",
|
||||
displayName: "Gemini: Gemini 2.5 Flash (AI Studio)",
|
||||
descriptionText: "Google AI Studio direct Gemini 2.5 Flash route",
|
||||
},
|
||||
{
|
||||
id: "gemini/gemini-2.5-pro",
|
||||
displayName: "Gemini: Gemini 2.5 Pro (AI Studio)",
|
||||
descriptionText: "Google AI Studio direct Gemini 2.5 Pro route",
|
||||
},
|
||||
];
|
||||
|
||||
const OMNIROUTE_CUSTOM_MODELS = new Set([
|
||||
...OMNIROUTE_BUILTIN_GROUPS.map((g) => g.id),
|
||||
...Object.keys(MODEL_ROUTING_MAP),
|
||||
]);
|
||||
|
||||
function shouldInterceptToOmniRoute(model, url) {
|
||||
if (!model) return false;
|
||||
|
||||
// Never intercept non-streaming unary RPCs (Antigravity expects raw JSON/Protobuf, not SSE)
|
||||
const isStreaming =
|
||||
url.includes("streamGenerateContent") ||
|
||||
url.includes("StreamGenerateChat") ||
|
||||
url.includes("alt=sse");
|
||||
if (!isStreaming) return false;
|
||||
|
||||
// Never intercept native Google/Gemini models (used by Antigravity core, subagents, websearch, grounding)
|
||||
if (model.startsWith("gemini-") || model.startsWith("models/gemini-")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Never intercept native Google CloudCode PA hosted models
|
||||
if (
|
||||
model === "claude-sonnet-4-6" ||
|
||||
model === "claude-opus-4-6" ||
|
||||
model === "gpt-oss-120b-medium"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Intercept any OmniRoute auto group, provider model, or mapped alias
|
||||
const clean = model.replace(/^models\//, "").trim();
|
||||
if (
|
||||
clean.startsWith("auto/") ||
|
||||
clean.toLowerCase().includes("omniroute") ||
|
||||
clean.includes("/") ||
|
||||
OMNIROUTE_CUSTOM_MODELS.has(model) ||
|
||||
OMNIROUTE_CUSTOM_MODELS.has(clean) ||
|
||||
Boolean(MODEL_ROUTING_MAP[model]) ||
|
||||
Boolean(MODEL_ROUTING_MAP[clean])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
const internalApp = http.createServer(async (req, res) => {
|
||||
const host = (req.headers.host || "cloudcode-pa.googleapis.com").split(":")[0];
|
||||
const url = req.url || "/";
|
||||
|
||||
// Collect request body
|
||||
const chunks = [];
|
||||
for await (const chunk of req) {
|
||||
chunks.push(chunk);
|
||||
}
|
||||
const bodyBuffer = Buffer.concat(chunks);
|
||||
|
||||
let bodyJson = null;
|
||||
if (bodyBuffer.length > 0) {
|
||||
try {
|
||||
bodyJson = JSON.parse(bodyBuffer.toString("utf-8"));
|
||||
} catch {}
|
||||
}
|
||||
|
||||
const model = extractModel(bodyJson, url);
|
||||
const shouldIntercept = shouldInterceptToOmniRoute(model, url);
|
||||
|
||||
if (shouldIntercept) {
|
||||
const resolvedModel = resolveTargetModel(model);
|
||||
console.log(
|
||||
`[Bridge] 🔀 INTERCEPTING -> OmniRoute: "${model || "default"}" => "${resolvedModel}" (${url})`
|
||||
);
|
||||
|
||||
let outgoingBuffer = bodyBuffer;
|
||||
if (bodyJson) {
|
||||
const cloned = JSON.parse(JSON.stringify(bodyJson));
|
||||
cloned.model = resolvedModel;
|
||||
if (cloned.request && typeof cloned.request === "object") {
|
||||
cloned.request.model = resolvedModel;
|
||||
}
|
||||
outgoingBuffer = Buffer.from(JSON.stringify(cloned), "utf-8");
|
||||
}
|
||||
|
||||
// Forward to OmniRoute /v1/antigravity
|
||||
try {
|
||||
const forwardHeaders = {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(outgoingBuffer),
|
||||
Authorization: `Bearer ${ROUTER_API_KEY}`,
|
||||
"x-omniroute-source": "agent-bridge",
|
||||
"x-omniroute-agent": "antigravity",
|
||||
"x-omniroute-skip-usage": "true", // Skip usage tracking for default models
|
||||
};
|
||||
|
||||
const upstreamReq = http.request(
|
||||
ROUTER_URL,
|
||||
{
|
||||
method: "POST",
|
||||
headers: forwardHeaders,
|
||||
agent: httpAgent,
|
||||
},
|
||||
(upstreamRes) => {
|
||||
res.writeHead(upstreamRes.statusCode || 200, upstreamRes.headers);
|
||||
upstreamRes.pipe(res);
|
||||
}
|
||||
);
|
||||
upstreamReq.setNoDelay(true);
|
||||
|
||||
upstreamReq.on("error", (err) => {
|
||||
console.error(`[Bridge] ❌ Error forwarding to OmniRoute: ${err.message}`);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: { message: `OmniRoute bridge error: ${err.message}` } }));
|
||||
}
|
||||
});
|
||||
|
||||
upstreamReq.write(outgoingBuffer);
|
||||
upstreamReq.end();
|
||||
return;
|
||||
} catch (err) {
|
||||
console.error(`[Bridge] ❌ Failed to invoke OmniRoute: ${err.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Otherwise: Passthrough directly to Google upstream
|
||||
console.log(`[Bridge] ⏩ PASSTHROUGH -> Google: ${model || "non-model"} (${url})`);
|
||||
|
||||
const upstreamHeaders = { ...req.headers };
|
||||
delete upstreamHeaders["host"]; // Let https.request set the correct Host
|
||||
upstreamHeaders["host"] = host;
|
||||
|
||||
if (url.includes("fetchAvailableModels")) {
|
||||
delete upstreamHeaders["accept-encoding"];
|
||||
}
|
||||
|
||||
const googleReq = https.request(
|
||||
{
|
||||
hostname: host,
|
||||
port: 443,
|
||||
path: url,
|
||||
method: req.method,
|
||||
headers: upstreamHeaders,
|
||||
agent: httpsAgent,
|
||||
},
|
||||
(googleRes) => {
|
||||
if (url.includes("fetchAvailableModels")) {
|
||||
const respChunks = [];
|
||||
googleRes.on("data", (chunk) => respChunks.push(chunk));
|
||||
googleRes.on("end", () => {
|
||||
const respBuffer = Buffer.concat(respChunks);
|
||||
let finalBuffer = respBuffer;
|
||||
try {
|
||||
const data = JSON.parse(respBuffer.toString("utf-8"));
|
||||
if (data && data.models) {
|
||||
// Inject OmniRoute built-in auto groups and models
|
||||
const baseTemplate =
|
||||
data.models["claude-sonnet-4-6"] ||
|
||||
data.models["gpt-oss-120b-medium"] ||
|
||||
Object.values(data.models)[0] ||
|
||||
{};
|
||||
|
||||
const injectedIds = [];
|
||||
for (const group of OMNIROUTE_BUILTIN_GROUPS) {
|
||||
data.models[group.id] = {
|
||||
...baseTemplate,
|
||||
id: group.id,
|
||||
name: group.id,
|
||||
displayName: group.displayName,
|
||||
descriptionText: group.descriptionText,
|
||||
};
|
||||
injectedIds.push(group.id);
|
||||
}
|
||||
|
||||
// Prepend OmniRoute groups to agentModelSorts recommended group
|
||||
if (
|
||||
Array.isArray(data.agentModelSorts) &&
|
||||
data.agentModelSorts[0]?.groups?.[0]?.modelIds
|
||||
) {
|
||||
const existing = data.agentModelSorts[0].groups[0].modelIds;
|
||||
data.agentModelSorts[0].groups[0].modelIds = [
|
||||
...injectedIds,
|
||||
...existing.filter((id) => !injectedIds.includes(id)),
|
||||
];
|
||||
}
|
||||
finalBuffer = Buffer.from(JSON.stringify(data), "utf-8");
|
||||
console.log(
|
||||
`[Bridge] 🌟 Injected custom models into fetchAvailableModels (${finalBuffer.length} bytes)`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[Bridge] ⚠️ Error modifying fetchAvailableModels: ${err.message}`);
|
||||
}
|
||||
|
||||
const headers = { ...googleRes.headers };
|
||||
delete headers["content-length"];
|
||||
delete headers["content-encoding"];
|
||||
headers["content-length"] = String(finalBuffer.length);
|
||||
res.writeHead(googleRes.statusCode || 200, headers);
|
||||
res.end(finalBuffer);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
res.writeHead(googleRes.statusCode || 200, googleRes.headers);
|
||||
googleRes.pipe(res);
|
||||
}
|
||||
);
|
||||
googleReq.setNoDelay(true);
|
||||
|
||||
googleReq.on("error", (err) => {
|
||||
console.error(`[Bridge] ❌ Google upstream error: ${err.message}`);
|
||||
if (!res.headersSent) {
|
||||
res.writeHead(502, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ error: { message: `Google upstream error: ${err.message}` } }));
|
||||
}
|
||||
});
|
||||
|
||||
if (bodyBuffer.length > 0) {
|
||||
googleReq.write(bodyBuffer);
|
||||
}
|
||||
googleReq.end();
|
||||
});
|
||||
|
||||
internalApp.keepAliveTimeout = 65000;
|
||||
internalApp.headersTimeout = 66000;
|
||||
|
||||
// Proxy server listening on HTTP port
|
||||
const proxyServer = http.createServer((req, res) => {
|
||||
// Plain HTTP request (non-CONNECT)
|
||||
res.writeHead(200, { "Content-Type": "text/plain" });
|
||||
res.end("OmniRoute Antigravity Bridge Proxy Active\n");
|
||||
});
|
||||
|
||||
proxyServer.keepAliveTimeout = 65000;
|
||||
proxyServer.headersTimeout = 66000;
|
||||
|
||||
proxyServer.on("connect", (req, clientSocket, head) => {
|
||||
clientSocket.setNoDelay(true);
|
||||
const [targetHost, targetPortStr] = (req.url || "").split(":");
|
||||
const targetPort = parseInt(targetPortStr || "443", 10);
|
||||
|
||||
if (TARGET_HOSTS.has(targetHost)) {
|
||||
// Target host: Terminate TLS locally and route via internalApp
|
||||
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
||||
|
||||
const ssl = getSslOptions();
|
||||
const tlsSocket = new tls.TLSSocket(clientSocket, {
|
||||
isServer: true,
|
||||
key: ssl.key,
|
||||
cert: ssl.cert,
|
||||
});
|
||||
tlsSocket.setNoDelay(true);
|
||||
|
||||
tlsSocket.on("error", (err) => {
|
||||
// Client closed or TLS error
|
||||
clientSocket.destroy();
|
||||
});
|
||||
|
||||
internalApp.emit("connection", tlsSocket);
|
||||
} else {
|
||||
// Non-target host: Transparent raw TCP tunnel
|
||||
const upstreamSocket = net.connect(targetPort, targetHost, () => {
|
||||
upstreamSocket.setNoDelay(true);
|
||||
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
||||
if (head && head.length > 0) {
|
||||
upstreamSocket.write(head);
|
||||
}
|
||||
upstreamSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(upstreamSocket);
|
||||
});
|
||||
|
||||
const cleanup = () => {
|
||||
clientSocket.destroy();
|
||||
upstreamSocket.destroy();
|
||||
};
|
||||
|
||||
upstreamSocket.on("error", cleanup);
|
||||
clientSocket.on("error", cleanup);
|
||||
}
|
||||
});
|
||||
|
||||
export {
|
||||
resolveTargetModel,
|
||||
MODEL_ROUTING_MAP,
|
||||
shouldInterceptToOmniRoute,
|
||||
extractModel,
|
||||
OMNIROUTE_BUILTIN_GROUPS,
|
||||
proxyServer,
|
||||
internalApp,
|
||||
};
|
||||
|
||||
const isMain = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url);
|
||||
|
||||
if (isMain) {
|
||||
proxyServer.listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`🚀 OmniRoute Antigravity Bridge listening on 127.0.0.1:${PORT}`);
|
||||
console.log(` Routing non-Gemini 3.8 model traffic -> ${ROUTER_URL}`);
|
||||
console.log(` Preserving Gemini 3.8 native traffic -> Google`);
|
||||
});
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { t } from "../i18n.mjs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
import { listManifestTargets } from "../cli-manifest.mjs";
|
||||
import { loadModelCatalog, ModelCommandError } from "./model-api.mjs";
|
||||
|
||||
// Target lists shared with `omniroute run` / `omniroute configure` — always
|
||||
// derived from the canonical manifest so the completion scripts cannot drift.
|
||||
@@ -31,14 +30,14 @@ function readCache() {
|
||||
}
|
||||
|
||||
async function refreshCache(opts = {}) {
|
||||
// Fail before replacing the cache when the selected catalog is unavailable.
|
||||
const models = (await loadModelCatalog(opts)).map((model) => model.id);
|
||||
let combos = [],
|
||||
providers = [];
|
||||
providers = [],
|
||||
models = [];
|
||||
try {
|
||||
const [cr, pr] = await Promise.allSettled([
|
||||
const [cr, pr, mr] = await Promise.allSettled([
|
||||
apiFetch("/api/combos", opts),
|
||||
apiFetch("/api/providers", opts),
|
||||
apiFetch("/api/models", opts),
|
||||
]);
|
||||
if (cr.status === "fulfilled" && cr.value.ok) {
|
||||
const j = await cr.value.json();
|
||||
@@ -48,6 +47,10 @@ async function refreshCache(opts = {}) {
|
||||
const j = await pr.value.json();
|
||||
providers = (j.providers || j.items || []).map((p) => p.id || p.name).filter(Boolean);
|
||||
}
|
||||
if (mr.status === "fulfilled" && mr.value.ok) {
|
||||
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);
|
||||
@@ -174,7 +177,6 @@ _omniroute() {
|
||||
'--max-tokens[Max tokens]:' ;;
|
||||
open)
|
||||
_arguments '1:resource:(combos providers api-manager cli-tools agents settings logs memory skills evals audit cost resilience)' ;;
|
||||
models) _arguments '1:subcommand:(manual add edit remove)' ;;
|
||||
completion) _arguments '1:subcommand:(zsh bash fish install refresh)' ;;
|
||||
config) _arguments '1:subcommand:(list get set validate contexts)' ;;
|
||||
contexts) _arguments '1:subcommand:(list add use current show remove rename export import migrate)' ;;
|
||||
@@ -229,7 +231,6 @@ _omniroute() {
|
||||
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills connect contexts configure launch launch-codex run runtime repair"
|
||||
|
||||
case "\${prev}" in
|
||||
models) COMPREPLY=($(compgen -W "manual add edit remove" -- "\${cur}")); return 0 ;;
|
||||
combo) COMPREPLY=($(compgen -W "list switch create delete show suggest" -- "\${cur}")); return 0 ;;
|
||||
keys) COMPREPLY=($(compgen -W "add list remove regenerate revoke reveal usage" -- "\${cur}")); return 0 ;;
|
||||
providers) COMPREPLY=($(compgen -W "available list test test-all validate rotate status add import auth remove edit metrics metric" -- "\${cur}")); return 0 ;;
|
||||
@@ -272,7 +273,6 @@ for cmd in $commands
|
||||
end
|
||||
|
||||
# Subcommands
|
||||
complete -c omniroute -n '__fish_seen_subcommand_from models' -a 'manual add edit remove'
|
||||
complete -c omniroute -n '__fish_seen_subcommand_from combo' -a 'list switch create delete show suggest'
|
||||
complete -c omniroute -n '__fish_seen_subcommand_from keys' -a 'add list remove regenerate revoke reveal usage'
|
||||
complete -c omniroute -n '__fish_seen_subcommand_from providers' -a 'available list test test-all validate rotate status add import auth remove edit metrics metric'
|
||||
@@ -351,18 +351,7 @@ export function registerCompletion(program) {
|
||||
.option("--quiet", "Suppress output")
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.optsWithGlobals();
|
||||
let data;
|
||||
try {
|
||||
data = await refreshCache(globalOpts);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error instanceof ModelCommandError
|
||||
? error.message
|
||||
: "Unable to refresh model completions."
|
||||
);
|
||||
process.exitCode = error.exitCode || 1;
|
||||
return;
|
||||
}
|
||||
const data = await refreshCache(globalOpts);
|
||||
if (!opts.quiet && !globalOpts.quiet) {
|
||||
process.stdout.write(
|
||||
`Cached: ${data.combos.length} combos, ${data.providers.length} providers, ${data.models.length} models\n`
|
||||
|
||||
@@ -10,7 +10,6 @@ import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
|
||||
import { printHeading } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
|
||||
import { getCrashLogPath } from "../runtime/processSupervisor.mjs";
|
||||
|
||||
const STATIC_SALT = "omniroute-field-encryption-v1";
|
||||
const KEY_LENGTH = 32;
|
||||
@@ -381,33 +380,6 @@ function checkMemory() {
|
||||
});
|
||||
}
|
||||
|
||||
// #13538: surfaces the supervisor's give-up crash record (persisted by
|
||||
// ServerSupervisor.persistCrashLog(), bin/cli/runtime/processSupervisor.mjs)
|
||||
// so a user whose `--tray` worker died silently (detached, stdio:"ignore")
|
||||
// has something concrete `doctor` can point at without needing `--log`.
|
||||
function checkCrashLog() {
|
||||
const crashLogPath = getCrashLogPath();
|
||||
if (!fs.existsSync(crashLogPath)) {
|
||||
return ok("Crash log", "No supervisor crash record found", { crashLogPath });
|
||||
}
|
||||
|
||||
try {
|
||||
const stat = fs.statSync(crashLogPath);
|
||||
const contents = fs.readFileSync(crashLogPath, "utf8");
|
||||
const lastEntry = contents.split("\n").filter(Boolean).slice(-6).join("\n");
|
||||
return warn(
|
||||
"Crash log",
|
||||
`Supervisor recorded a give-up crash at ${crashLogPath} (last modified ${stat.mtime.toISOString()})`,
|
||||
{ crashLogPath, modifiedAt: stat.mtime.toISOString(), tail: lastEntry }
|
||||
);
|
||||
} catch (error) {
|
||||
return warn("Crash log", `Crash record exists at ${crashLogPath} but could not be read`, {
|
||||
crashLogPath,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
||||
@@ -607,7 +579,6 @@ export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
checks.push(await checkNodeRuntime(rootDir));
|
||||
checks.push(await checkNativeBinary(rootDir));
|
||||
checks.push(checkMemory());
|
||||
checks.push(checkCrashLog());
|
||||
|
||||
if (!options.skipLiveness) {
|
||||
checks.push(await checkServerLiveness(options));
|
||||
|
||||
@@ -352,24 +352,10 @@ export async function runKeysRegenerateCommand(id, opts = {}) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
const encodedId = encodeURIComponent(id);
|
||||
let res = await apiFetch(`/api/v1/registered-keys/${encodedId}/regenerate`, {
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/regenerate`, {
|
||||
method: "POST",
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
// `keys` predates the split between registered keys and the dashboard's
|
||||
// ordinary API keys. IDs shown by `keys list`/the dashboard belong to
|
||||
// `/api/keys`, while deployment/registered-key IDs belong to
|
||||
// `/api/v1/registered-keys`. Try the ordinary-key route when the ID is not
|
||||
// present in the registered-key store so the command works with either ID.
|
||||
if (isRouteUnavailableStatus(res.status)) {
|
||||
res = await apiFetch(`/api/keys/${encodedId}/regenerate`, {
|
||||
method: "POST",
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
@@ -424,17 +410,9 @@ export async function runKeysRevealCommand(id, opts = {}) {
|
||||
return 1;
|
||||
}
|
||||
try {
|
||||
const encodedId = encodeURIComponent(id);
|
||||
let res = await apiFetch(`/api/v1/registered-keys/${encodedId}/reveal`, {
|
||||
const res = await apiFetch(`/api/v1/registered-keys/${encodeURIComponent(id)}/reveal`, {
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (isRouteUnavailableStatus(res.status)) {
|
||||
res = await apiFetch(`/api/keys/${encodedId}/reveal`, {
|
||||
retry: false,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
}
|
||||
if (!res.ok) {
|
||||
console.error(t("common.error", { message: `HTTP ${res.status}` }));
|
||||
return 1;
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
import { apiFetch, statusToExitCode } from "../api.mjs";
|
||||
|
||||
export class ModelCommandError extends Error {
|
||||
constructor(message, exitCode = 2) {
|
||||
super(message);
|
||||
this.exitCode = exitCode;
|
||||
}
|
||||
}
|
||||
|
||||
export async function modelRequest(path, opts, init = {}) {
|
||||
let response;
|
||||
try {
|
||||
response = await apiFetch(path, {
|
||||
...opts,
|
||||
...init,
|
||||
retry: false,
|
||||
timeout: opts.timeout ?? 30000,
|
||||
redirect: "error",
|
||||
acceptNotOk: true,
|
||||
});
|
||||
} catch (error) {
|
||||
throw new ModelCommandError(
|
||||
"Unable to reach the selected OmniRoute server.",
|
||||
error.exitCode === 124 ? 124 : 1
|
||||
);
|
||||
}
|
||||
if (!response.ok) {
|
||||
const error = new ModelCommandError(
|
||||
`Model request failed (HTTP ${response.status}).`,
|
||||
statusToExitCode(response.status)
|
||||
);
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
try {
|
||||
return await response.json();
|
||||
} catch {
|
||||
throw new ModelCommandError("The server returned an invalid model response.", 1);
|
||||
}
|
||||
}
|
||||
|
||||
export async function loadModelCatalog(opts = {}) {
|
||||
let data;
|
||||
try {
|
||||
data = await modelRequest("/api/v1/models", opts);
|
||||
} catch (error) {
|
||||
if (![404, 405, 501].includes(error.status)) throw error;
|
||||
data = await modelRequest("/api/models", opts);
|
||||
}
|
||||
const models = Array.isArray(data) ? data : (data?.data ?? data?.models);
|
||||
if (!Array.isArray(models)) throw new ModelCommandError("Invalid model catalog.", 1);
|
||||
return models.filter((model) => model && typeof model === "object").map(publicModel);
|
||||
}
|
||||
|
||||
// Public model metadata only; never forward credentials or compatibility headers.
|
||||
const PUBLIC_FIELDS = [
|
||||
"id",
|
||||
"name",
|
||||
"object",
|
||||
"created",
|
||||
"owned_by",
|
||||
"provider",
|
||||
"description",
|
||||
"source",
|
||||
"context_length",
|
||||
"contextWindow",
|
||||
"contextWindowOverride",
|
||||
"contextWindowOverrideSource",
|
||||
"max_input_tokens",
|
||||
"max_output_tokens",
|
||||
"inputTokenLimit",
|
||||
"outputTokenLimit",
|
||||
"apiFormat",
|
||||
"supportedEndpoints",
|
||||
"targetFormat",
|
||||
"supportsVision",
|
||||
"supports_vision",
|
||||
"supports_tools",
|
||||
"supports_reasoning",
|
||||
"modelType",
|
||||
"isFree",
|
||||
"dimensions",
|
||||
"root",
|
||||
"parent",
|
||||
"type",
|
||||
"free",
|
||||
"custom",
|
||||
"api_format",
|
||||
"supported_endpoints",
|
||||
"input_modalities",
|
||||
"output_modalities",
|
||||
"supported_parameters",
|
||||
"supportedInputTypes",
|
||||
];
|
||||
|
||||
export function publicModel(model) {
|
||||
const result = {};
|
||||
for (const field of PUBLIC_FIELDS) {
|
||||
const value = model?.[field];
|
||||
if (value === null || ["string", "number", "boolean"].includes(typeof value))
|
||||
result[field] = value;
|
||||
else if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
||||
result[field] = value;
|
||||
}
|
||||
if (model?.capabilities && typeof model.capabilities === "object") {
|
||||
result.capabilities = Object.fromEntries(
|
||||
Object.entries(model.capabilities).filter(
|
||||
([key, value]) =>
|
||||
[
|
||||
"vision",
|
||||
"reasoning",
|
||||
"tool_calling",
|
||||
"structured_output",
|
||||
"streaming",
|
||||
"audio",
|
||||
"video",
|
||||
].includes(key) && typeof value === "boolean"
|
||||
)
|
||||
);
|
||||
}
|
||||
result.id = String(model?.id || model?.name || "unknown");
|
||||
result.provider = String(model?.provider || model?.owned_by || "unknown");
|
||||
result.contextWindow =
|
||||
result.contextWindowOverride ??
|
||||
result.context_length ??
|
||||
result.max_input_tokens ??
|
||||
result.inputTokenLimit ??
|
||||
result.contextWindow ??
|
||||
"-";
|
||||
return result;
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { emit } from "../output.mjs";
|
||||
import { ModelCommandError, modelRequest, publicModel } from "./model-api.mjs";
|
||||
|
||||
// Runtime CLI subset of providerModelMutationSchema. The API validates again.
|
||||
const identity = z.object({
|
||||
provider: z.string().trim().min(1).max(120),
|
||||
modelId: z.string().trim().min(1).max(240),
|
||||
});
|
||||
const patchSchema = z.object({
|
||||
modelName: z.string().trim().min(1).max(240).optional(),
|
||||
apiFormat: z
|
||||
.enum([
|
||||
"chat-completions",
|
||||
"responses",
|
||||
"embeddings",
|
||||
"rerank",
|
||||
"audio-transcriptions",
|
||||
"audio-speech",
|
||||
"images-generations",
|
||||
"video",
|
||||
])
|
||||
.optional(),
|
||||
max_input_tokens: z.number().int().positive().safe().optional(),
|
||||
max_output_tokens: z.number().int().positive().safe().optional(),
|
||||
contextWindowOverride: z.number().int().positive().safe().nullable().optional(),
|
||||
});
|
||||
|
||||
const FORMAT_ENDPOINT = {
|
||||
"chat-completions": "chat",
|
||||
responses: "chat",
|
||||
embeddings: "embeddings",
|
||||
rerank: "rerank",
|
||||
"audio-transcriptions": "audio-transcriptions",
|
||||
"audio-speech": "audio-speech",
|
||||
"images-generations": "images",
|
||||
video: "videos",
|
||||
};
|
||||
|
||||
function payloadFor(action, provider, modelId, opts) {
|
||||
const id = identity.safeParse({ provider, modelId });
|
||||
const patch = {};
|
||||
if (opts.name !== undefined) patch.modelName = opts.name;
|
||||
if (opts.apiFormat !== undefined) patch.apiFormat = opts.apiFormat;
|
||||
if (opts.contextWindow !== undefined) {
|
||||
patch[action === "add" ? "max_input_tokens" : "contextWindowOverride"] = Number(
|
||||
opts.contextWindow
|
||||
);
|
||||
}
|
||||
if (opts.clearContextWindow) {
|
||||
if (opts.contextWindow !== undefined || action !== "edit")
|
||||
throw new ModelCommandError("Invalid context-window options.");
|
||||
patch.contextWindowOverride = null;
|
||||
}
|
||||
if (opts.maxOutputTokens !== undefined) patch.max_output_tokens = Number(opts.maxOutputTokens);
|
||||
const parsed = patchSchema.safeParse(patch);
|
||||
if (!id.success || !parsed.success)
|
||||
throw new ModelCommandError("Invalid model identifier or metadata.");
|
||||
if (action === "edit" && Object.keys(patch).length === 0)
|
||||
throw new ModelCommandError("Provide at least one metadata change.");
|
||||
return {
|
||||
...id.data,
|
||||
...parsed.data,
|
||||
...(parsed.data.apiFormat
|
||||
? { supportedEndpoints: [FORMAT_ENDPOINT[parsed.data.apiFormat]] }
|
||||
: {}),
|
||||
...(action === "add" ? { source: "manual" } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export async function listManualModels(provider, opts = {}) {
|
||||
const parsed = identity.shape.provider.safeParse(provider);
|
||||
if (!parsed.success) throw new ModelCommandError("Invalid provider identifier.");
|
||||
const data = await modelRequest(
|
||||
`/api/provider-models?${new URLSearchParams({ provider: parsed.data })}`,
|
||||
opts
|
||||
);
|
||||
if (!Array.isArray(data?.models)) throw new ModelCommandError("Invalid manual model catalog.", 1);
|
||||
return data.models;
|
||||
}
|
||||
|
||||
export async function modifyManualModel(action, provider, modelId, opts = {}) {
|
||||
if (!["add", "edit", "remove"].includes(action))
|
||||
throw new ModelCommandError("Invalid model operation.");
|
||||
if (action === "remove" && !opts.yes && !opts.dryRun)
|
||||
throw new ModelCommandError("Removal requires --yes (or --dry-run).");
|
||||
const body = payloadFor(action, provider, modelId, opts);
|
||||
const before = (await listManualModels(body.provider, opts)).find(
|
||||
(model) => model.id === body.modelId
|
||||
);
|
||||
if (action === "add" && before)
|
||||
throw new ModelCommandError("The custom model already exists; use edit.");
|
||||
if (action !== "add" && (!before || (before.source && before.source !== "manual"))) {
|
||||
throw new ModelCommandError("The selected model is not an existing manual model.");
|
||||
}
|
||||
if (opts.dryRun)
|
||||
return { action, dryRun: true, provider: body.provider, modelId: body.modelId, changes: body };
|
||||
const query = new URLSearchParams({
|
||||
provider: body.provider,
|
||||
model: body.modelId,
|
||||
resetOverride: "true",
|
||||
});
|
||||
await modelRequest(
|
||||
action === "remove" ? `/api/provider-models?${query}` : "/api/provider-models",
|
||||
opts,
|
||||
{
|
||||
method: { add: "POST", edit: "PUT", remove: "DELETE" }[action],
|
||||
...(action === "remove" ? {} : { body }),
|
||||
}
|
||||
);
|
||||
const after = (await listManualModels(body.provider, opts)).find(
|
||||
(model) => model.id === body.modelId
|
||||
);
|
||||
const mapping = {
|
||||
modelName: "name",
|
||||
max_input_tokens: "inputTokenLimit",
|
||||
max_output_tokens: "outputTokenLimit",
|
||||
};
|
||||
const mismatch =
|
||||
action === "remove"
|
||||
? Boolean(after)
|
||||
: !after ||
|
||||
Object.entries(body).some(([key, value]) => {
|
||||
if (["provider", "modelId"].includes(key)) return false;
|
||||
const actual = after[mapping[key] || key];
|
||||
if (Array.isArray(value)) return JSON.stringify(actual) !== JSON.stringify(value);
|
||||
return value === null ? actual != null : actual !== value;
|
||||
});
|
||||
if (mismatch)
|
||||
throw new ModelCommandError(
|
||||
"Model readback did not confirm the requested change; inspect the server before retrying.",
|
||||
1
|
||||
);
|
||||
return {
|
||||
action,
|
||||
persistenceVerified: true,
|
||||
inferenceValidation: "not-run",
|
||||
provider: body.provider,
|
||||
modelId: body.modelId,
|
||||
...(after ? { model: publicModel({ ...after, provider: body.provider }) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function modelMutationAction(action) {
|
||||
return async (provider, modelId, options, cmd) => {
|
||||
try {
|
||||
const opts = { ...cmd.optsWithGlobals(), ...options };
|
||||
emit(await modifyManualModel(action, provider, modelId, opts), {
|
||||
...opts,
|
||||
output: opts.output || "json",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(error instanceof ModelCommandError ? error.message : "Model operation failed.");
|
||||
process.exitCode = error.exitCode || 1;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export async function manualListAction(provider, options, cmd) {
|
||||
const opts = { ...cmd.optsWithGlobals(), ...options };
|
||||
try {
|
||||
emit(
|
||||
(await listManualModels(provider, opts))
|
||||
.filter((model) => !model.source || model.source === "manual")
|
||||
.map((model) => publicModel({ ...model, provider })),
|
||||
{ ...opts, output: opts.output || "json" }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
error instanceof ModelCommandError ? error.message : "Unable to list manual models."
|
||||
);
|
||||
process.exitCode = error.exitCode || 1;
|
||||
}
|
||||
}
|
||||
@@ -1,75 +1,92 @@
|
||||
import { apiFetch, isServerUp } from "../api.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
import { modelListSchema } from "../schemas/output-schemas.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { loadModelCatalog } from "./model-api.mjs";
|
||||
import { manualListAction, modelMutationAction } from "./model-crud.mjs";
|
||||
|
||||
export function registerModels(program) {
|
||||
const models = program
|
||||
program
|
||||
.command("models [provider]")
|
||||
.description(t("models.description"))
|
||||
.option("--search <query>", t("models.search"))
|
||||
.option("--json", "Output as JSON")
|
||||
.action(async (provider, opts, cmd) => {
|
||||
process.exitCode = await runModelsCommand(provider, { ...cmd.optsWithGlobals(), ...opts });
|
||||
const globalOpts = cmd.optsWithGlobals();
|
||||
const exitCode = await runModelsCommand(provider, { ...opts, output: globalOpts.output });
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
});
|
||||
models
|
||||
.command("manual <provider>")
|
||||
.description("List manual model metadata from the selected server")
|
||||
.action(manualListAction);
|
||||
models
|
||||
.command("add <provider> <model-id>")
|
||||
.description("Add an unverified manual model, then verify persistence")
|
||||
.option("--name <name>", "Display name")
|
||||
.option("--api-format <format>", "API format, e.g. chat-completions or responses")
|
||||
.option("--context-window <tokens>", "Positive integer input/context limit")
|
||||
.option("--max-output-tokens <tokens>", "Positive integer output limit")
|
||||
.option("--dry-run", "Preview without writing or inference")
|
||||
.action(modelMutationAction("add"));
|
||||
models
|
||||
.command("edit <provider> <model-id>")
|
||||
.description("Edit manual model metadata, then verify persistence")
|
||||
.option("--name <name>", "Display name")
|
||||
.option("--api-format <format>", "API format, e.g. chat-completions or responses")
|
||||
.option("--context-window <tokens>", "Positive integer context override")
|
||||
.option("--clear-context-window", "Clear the manual context-window override")
|
||||
.option("--dry-run", "Preview without writing or inference")
|
||||
.action(modelMutationAction("edit"));
|
||||
models
|
||||
.command("remove <provider> <model-id>")
|
||||
.description("Remove only a manual model override, then verify persistence")
|
||||
.option("--yes", "Confirm removal of the manual override only")
|
||||
.option("--dry-run", "Preview without writing or inference")
|
||||
.action(modelMutationAction("remove"));
|
||||
}
|
||||
|
||||
export async function runModelsCommand(provider, opts = {}) {
|
||||
try {
|
||||
let models = await loadModelCatalog(opts);
|
||||
if (provider) {
|
||||
const filter = provider.toLowerCase();
|
||||
models = models.filter(
|
||||
(model) =>
|
||||
model.provider.toLowerCase().includes(filter) || model.id.toLowerCase().startsWith(filter)
|
||||
);
|
||||
}
|
||||
if (opts.search) {
|
||||
const search = opts.search.toLowerCase();
|
||||
models = models.filter((model) =>
|
||||
[model.id, model.name, model.provider, model.description].some((value) =>
|
||||
String(value || "")
|
||||
.toLowerCase()
|
||||
.includes(search)
|
||||
)
|
||||
);
|
||||
}
|
||||
const table = opts.output === "table" || (!opts.output && !opts.json && process.stdout.isTTY);
|
||||
emit(table ? models.slice(0, 50) : models, opts, modelListSchema);
|
||||
if (table && models.length > 50)
|
||||
console.log(`... and ${models.length - 50} more. Use --output json for the full list.`);
|
||||
return 0;
|
||||
} catch (error) {
|
||||
console.error(error.exitCode ? error.message : "Unable to read the model catalog.");
|
||||
return error.exitCode || 1;
|
||||
const serverUp = await isServerUp();
|
||||
if (!serverUp) {
|
||||
console.error(t("models.noServer"));
|
||||
return 1;
|
||||
}
|
||||
|
||||
let models = [];
|
||||
|
||||
try {
|
||||
const res = await apiFetch("/api/models", { retry: false, timeout: 5000, acceptNotOk: true });
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
models = Array.isArray(data) ? data : data.models || [];
|
||||
}
|
||||
} catch {}
|
||||
|
||||
if (models.length === 0) {
|
||||
try {
|
||||
const res = await apiFetch("/api/v1/models", {
|
||||
retry: false,
|
||||
timeout: 5000,
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
models = Array.isArray(data) ? data : data.data || [];
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (provider) {
|
||||
const filter = provider.toLowerCase();
|
||||
models = models.filter(
|
||||
(m) =>
|
||||
(m.provider && m.provider.toLowerCase().includes(filter)) ||
|
||||
(m.id && m.id.toLowerCase().startsWith(filter)) ||
|
||||
(m.name && m.name.toLowerCase().includes(filter))
|
||||
);
|
||||
}
|
||||
|
||||
if (opts.search) {
|
||||
const search = opts.search.toLowerCase();
|
||||
models = models.filter(
|
||||
(m) =>
|
||||
(m.id && m.id.toLowerCase().includes(search)) ||
|
||||
(m.name && m.name.toLowerCase().includes(search)) ||
|
||||
(m.provider && m.provider.toLowerCase().includes(search)) ||
|
||||
(m.description && m.description.toLowerCase().includes(search))
|
||||
);
|
||||
}
|
||||
|
||||
if (models.length === 0) {
|
||||
console.log(t("models.noModels"));
|
||||
return 0;
|
||||
}
|
||||
|
||||
const normalized = models.map((m) => ({
|
||||
id: m.id || m.name || "unknown",
|
||||
provider: m.provider || "unknown",
|
||||
contextWindow: String(m.context_length || m.max_tokens || m.contextWindow || "-"),
|
||||
}));
|
||||
|
||||
const display = normalized.slice(0, 50);
|
||||
emit(display, opts, modelListSchema);
|
||||
|
||||
if (models.length > 50) {
|
||||
console.log(
|
||||
`\x1b[2m ... and ${models.length - 50} more. Use --output json for full list.\x1b[0m`
|
||||
);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -4,13 +4,7 @@ import { join, dirname } from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import {
|
||||
writePidFile,
|
||||
cleanupPidFile,
|
||||
waitForServer,
|
||||
findListeningPids,
|
||||
resolveReadyTimeoutMs,
|
||||
} from "../utils/pid.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer, resolveReadyTimeoutMs } from "../utils/pid.mjs";
|
||||
import {
|
||||
ServerSupervisor,
|
||||
detectMitmCrash,
|
||||
@@ -241,16 +235,6 @@ export async function runServe(opts = {}) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Refuse to start a second instance on a port something else already owns,
|
||||
// BEFORE any pid file is written or any child is spawned. Otherwise the
|
||||
// doomed child's EADDRINUSE arrives only after this process has rewritten
|
||||
// the pid files of the healthy instance that actually owns the port.
|
||||
const busyPids = await findListeningPids(dashboardPort);
|
||||
if (busyPids.length > 0) {
|
||||
reportPortInUse(dashboardPort, busyPids);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(` \x1b[2m⏳ Starting server...\x1b[0m\n`);
|
||||
|
||||
// #5172/#5160/#5152: default the V8 heap to ~35% of physical RAM (clamped
|
||||
@@ -321,29 +305,10 @@ export async function runServe(opts = {}) {
|
||||
opts.maxRestarts ?? 2,
|
||||
startedAt,
|
||||
useTray,
|
||||
{
|
||||
trayReadyPort: opts.trayReadyPort,
|
||||
trayReadyToken: opts.trayReadyToken,
|
||||
readyTimeoutMs: resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout }),
|
||||
}
|
||||
{ trayReadyPort: opts.trayReadyPort, trayReadyToken: opts.trayReadyToken }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Explain a port conflict in terms the operator can act on: who owns the port,
|
||||
* and the two ways out. Exported for unit tests.
|
||||
*/
|
||||
export function reportPortInUse(port, pids = []) {
|
||||
const owner = pids.length === 1 ? `PID ${pids[0]}` : `PIDs ${pids.join(", ")}`;
|
||||
console.error(`\n\x1b[31m✖ Port ${port} is already in use by ${owner}.\x1b[0m`);
|
||||
console.error(
|
||||
` Another OmniRoute is most likely already serving there, so open` +
|
||||
` ${urlScheme}://localhost:${port} before starting a second one.`
|
||||
);
|
||||
console.error(` To replace it: \x1b[36momniroute stop\x1b[0m, then start again`);
|
||||
console.error(` To run alongside: \x1b[36momniroute serve --port <other-port>\x1b[0m\n`);
|
||||
}
|
||||
|
||||
function runDaemon(serverJs, env, memoryLimit, dashboardPort, apiPort) {
|
||||
// #5238: skip the explicit CLI --max-old-space-size when the user pinned the
|
||||
// heap via NODE_OPTIONS (a CLI arg would shadow/override their value).
|
||||
@@ -454,7 +419,7 @@ async function runWithSupervisor(
|
||||
maxRestarts,
|
||||
startedAt,
|
||||
useTray = false,
|
||||
{ trayReadyPort, trayReadyToken, readyTimeoutMs = resolveReadyTimeoutMs() } = {}
|
||||
{ trayReadyPort, trayReadyToken } = {}
|
||||
) {
|
||||
if (showLog) process.env.OMNIROUTE_SHOW_LOG = "1";
|
||||
writePidFile("supervisor", process.pid);
|
||||
@@ -493,12 +458,8 @@ async function runWithSupervisor(
|
||||
});
|
||||
|
||||
if (!showLog) {
|
||||
let lastProbeOutcome = null;
|
||||
waitForServer(dashboardPort, readyTimeoutMs, {
|
||||
onOutcome: (outcome) => {
|
||||
lastProbeOutcome = outcome;
|
||||
},
|
||||
}).then(async (up) => {
|
||||
const readyTimeoutMs = resolveReadyTimeoutMs({ timeoutMs: opts.readyTimeout });
|
||||
waitForServer(dashboardPort, readyTimeoutMs).then(async (up) => {
|
||||
if (up) {
|
||||
if (useTray) {
|
||||
const trayReady = await maybeStartTray(dashboardPort, apiPort, supervisor);
|
||||
@@ -522,7 +483,7 @@ async function runWithSupervisor(
|
||||
}
|
||||
onReady(dashboardPort, apiPort, noOpen, startedAt);
|
||||
} else {
|
||||
reportReadinessTimeout(dashboardPort, supervisor, lastProbeOutcome);
|
||||
reportReadinessTimeout(dashboardPort, supervisor);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -534,28 +495,13 @@ async function runWithSupervisor(
|
||||
// stuck (issue reports show the server sometimes actually comes up later, or is
|
||||
// reachable directly while the CLI still looks hung). Surface a clear diagnostic
|
||||
// plus whatever stdout/stderr the child buffered instead of going silent.
|
||||
export function reportReadinessTimeout(dashboardPort, supervisor, lastProbeOutcome = null) {
|
||||
export function reportReadinessTimeout(dashboardPort, supervisor) {
|
||||
const readyTimeoutMs = resolveReadyTimeoutMs();
|
||||
const seconds = Math.round(readyTimeoutMs / 1000);
|
||||
console.error(
|
||||
`\n\x1b[33m⚠ Server did not respond within ${seconds}s.\x1b[0m It may still be starting, or may` +
|
||||
` have failed silently.`
|
||||
);
|
||||
// The last probe classification separates a real boot failure (nothing ever
|
||||
// bound the port, so the buffered output below is the reason) from a server
|
||||
// that IS listening and merely did not answer the health route in time:
|
||||
// very likely usable already, with only the readiness signal timed out.
|
||||
if (lastProbeOutcome === "hanging" || lastProbeOutcome === "fast-reject") {
|
||||
console.error(
|
||||
` Port ${dashboardPort} IS accepting connections, so the server is probably up and` +
|
||||
` still warming up. Check the dashboard before restarting it.`
|
||||
);
|
||||
} else if (lastProbeOutcome === "not-listening") {
|
||||
console.error(
|
||||
` Nothing is listening on port ${dashboardPort}, so the server never bound it and the` +
|
||||
` output below is the reason.`
|
||||
);
|
||||
}
|
||||
console.error(
|
||||
` Tip: set OMNIROUTE_READY_TIMEOUT_MS=${readyTimeoutMs * 2} or --ready-timeout ${readyTimeoutMs * 2} for slower cold starts.`
|
||||
);
|
||||
|
||||
@@ -187,9 +187,7 @@ export async function runUpdateCommand(opts = {}) {
|
||||
}
|
||||
|
||||
if (dryRun) {
|
||||
console.log(
|
||||
"\n [DRY RUN] Would run: npm install -g omniroute@latest --include=optional --legacy-peer-deps"
|
||||
);
|
||||
console.log("\n [DRY RUN] Would run: npm install -g omniroute@latest --include=optional");
|
||||
if (!skipBackup) console.log(" [DRY RUN] Would create backup in ~/.omniroute/backups/");
|
||||
return 0;
|
||||
}
|
||||
@@ -223,9 +221,7 @@ export async function runUpdateCommand(opts = {}) {
|
||||
const { execSync } = await import("child_process");
|
||||
// --include=optional keeps the optionalDependencies (better-sqlite3, keytar,
|
||||
// tls-client, llmlingua SLM stack) on update so an omit=optional config can't drop them.
|
||||
execSync("npm install -g omniroute@latest --include=optional --legacy-peer-deps", {
|
||||
stdio: "inherit",
|
||||
});
|
||||
execSync("npm install -g omniroute@latest --include=optional", { stdio: "inherit" });
|
||||
// Trust-but-verify: `npm install -g` exits 0 even when a shadowing local install
|
||||
// (e.g. ~/node_modules/omniroute ahead of the global prefix on PATH) means the
|
||||
// binary the user actually runs was not touched. Re-read the running binary's
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import { mkdirSync, appendFileSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { writePidFile, cleanupPidFile, killAllSubprocesses, isPidRunning } from "../utils/pid.mjs";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
import {
|
||||
RESTART_RESET_MS,
|
||||
DEFAULT_MAX_RESTARTS,
|
||||
@@ -21,13 +19,6 @@ import {
|
||||
|
||||
const CRASH_LOG_LINES = 50;
|
||||
|
||||
// #13538: shared path resolver so `omniroute doctor` (bin/cli/commands/doctor.mjs)
|
||||
// can surface the same file persistCrashLog() writes, without duplicating the
|
||||
// `<DATA_DIR>/server/...` convention from bin/cli/utils/pid.mjs.
|
||||
export function getCrashLogPath() {
|
||||
return join(resolveDataDir(), "server", "crash.log");
|
||||
}
|
||||
|
||||
const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
// Bun needs the Node-compat polyfill preloaded (#9761). The file ships at the
|
||||
// package root via package.json "files" (see scripts/build/pack-artifact-policy.ts)
|
||||
@@ -90,10 +81,6 @@ export class ServerSupervisor {
|
||||
cwd: dirname(this.serverPath),
|
||||
env: this.env,
|
||||
stdio: showLog ? "inherit" : ["ignore", "pipe", "pipe"],
|
||||
// Tray mode has no visible console. Keep the supervised server hidden on Windows,
|
||||
// including when it is restarted after a crash. Without this, each supervised
|
||||
// spawn can create a visible terminal window.
|
||||
windowsHide: true,
|
||||
});
|
||||
|
||||
writePidFile("server", this.child.pid);
|
||||
@@ -177,8 +164,7 @@ export class ServerSupervisor {
|
||||
if (aliveMs >= RESTART_RESET_MS) this.restartCount = 0;
|
||||
|
||||
if (this.restartCount >= this.maxRestarts) {
|
||||
const summary = `Server crashed ${this.maxRestarts} times in <30s.`;
|
||||
console.error(`\n⚠ ${summary}`);
|
||||
console.error(`\n⚠ Server crashed ${this.maxRestarts} times in <30s.`);
|
||||
if (this.onCrashCallback) {
|
||||
const action = this.onCrashCallback(this.crashLog);
|
||||
if (action === "disable-mitm-and-retry") {
|
||||
@@ -189,14 +175,6 @@ export class ServerSupervisor {
|
||||
}
|
||||
}
|
||||
this.dumpCrashLog();
|
||||
// #13538: the give-up path used to only console.error() this diagnostic.
|
||||
// In `--tray`/`--tray-worker` mode this process is launched detached with
|
||||
// stdio:"ignore" (bin/cli/tray/detachedTray.mjs buildTrayLaunch()), so
|
||||
// that console output is discarded by the OS and nothing ever explains
|
||||
// why the tray + gateway disappeared together. Best-effort persist a
|
||||
// durable record next to the existing per-service PID file convention
|
||||
// (bin/cli/utils/pid.mjs) so it survives the process exit below.
|
||||
this.persistCrashLog(summary);
|
||||
process.exit(exitCode ?? 1);
|
||||
return;
|
||||
}
|
||||
@@ -228,23 +206,6 @@ export class ServerSupervisor {
|
||||
console.error("--- End crash log ---\n");
|
||||
}
|
||||
|
||||
// #13538: best-effort append a durable crash record to
|
||||
// `<DATA_DIR>/server/crash.log`, mirroring the `<DATA_DIR>/<service>/.pid`
|
||||
// layout from bin/cli/utils/pid.mjs. Wrapped in try/catch — this diagnostic
|
||||
// write must NEVER block or fail shutdown (the give-up branch always calls
|
||||
// process.exit() right after this).
|
||||
persistCrashLog(summary) {
|
||||
try {
|
||||
const crashLogPath = getCrashLogPath();
|
||||
mkdirSync(dirname(crashLogPath), { recursive: true });
|
||||
const timestamp = new Date().toISOString();
|
||||
const body = [`[${timestamp}] ${summary}`, ...this.crashLog, ""].join("\n");
|
||||
appendFileSync(crashLogPath, body, "utf8");
|
||||
} catch {
|
||||
// Best-effort only — a diagnostic write failure must not prevent shutdown.
|
||||
}
|
||||
}
|
||||
|
||||
stop() {
|
||||
this.isShuttingDown = true;
|
||||
if (this.child?.pid) {
|
||||
|
||||
@@ -13,13 +13,11 @@ const LINUX_DESKTOP_NAME = "omniroute.desktop";
|
||||
function resolveCliPath() {
|
||||
const candidates = [];
|
||||
if (process.argv[1]) candidates.push(process.argv[1]);
|
||||
if (process.platform !== "win32") {
|
||||
try {
|
||||
const which = execSync("command -v omniroute 2>/dev/null", { encoding: "utf8" }).trim();
|
||||
if (which) candidates.push(which);
|
||||
} catch {
|
||||
// command -v unavailable
|
||||
}
|
||||
try {
|
||||
const which = execSync("command -v omniroute 2>/dev/null", { encoding: "utf8" }).trim();
|
||||
if (which) candidates.push(which);
|
||||
} catch {
|
||||
// command -v unavailable
|
||||
}
|
||||
candidates.push(join(dirname(fileURLToPath(import.meta.url)), "..", "..", "omniroute.mjs"));
|
||||
|
||||
|
||||
@@ -29,9 +29,7 @@ async function loadSystray2() {
|
||||
function getIconBase64() {
|
||||
// Icon ships at bin/cli/tray/icon.png — the previous "icons/icon.png" path
|
||||
// never existed, so the tray was created with an empty icon (#4605).
|
||||
// systray2 expects an ICO payload on Windows; the PNG asset is used elsewhere.
|
||||
// (ported from #13991, credit @prabhtheone)
|
||||
const iconPath = join(__dirname, process.platform === "win32" ? "icon.ico" : "icon.png");
|
||||
const iconPath = join(__dirname, "icon.png");
|
||||
if (existsSync(iconPath)) return readFileSync(iconPath).toString("base64");
|
||||
return "";
|
||||
}
|
||||
|
||||
@@ -1,86 +1,15 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
|
||||
// #13679 PR B: checked-in literal, used ONLY as a last-resort fallback (see
|
||||
// getActiveSalt() below) — /etc/machine-id is commonly world-readable, so relying on
|
||||
// this literal as the real default let any local user derive the same bearer token.
|
||||
const BUILTIN_DEFAULT_SALT = "omniroute-cli-auth-v1";
|
||||
const SALT_FILE_NAME = "cli-token-salt.json";
|
||||
const PERSISTED_SALT_RE = /^[0-9a-f]{64}$/;
|
||||
export const CLI_TOKEN_HEADER = "x-omniroute-cli-token";
|
||||
|
||||
let _cached = null;
|
||||
let _cachedSalt = null;
|
||||
let _cachedActiveSalt = null;
|
||||
|
||||
/** A `node --test` (or vitest) process that never opted into an explicit DATA_DIR must
|
||||
* not write a salt file into the operator's real home directory. Mirrors
|
||||
* dataPaths.ts::isTestContext() on the TS side. */
|
||||
function isTestContext() {
|
||||
return (
|
||||
process.env.NODE_ENV === "test" ||
|
||||
!!process.env.VITEST ||
|
||||
!!process.env.NODE_TEST_CONTEXT ||
|
||||
process.execArgv.includes("--test") ||
|
||||
process.argv.includes("--test")
|
||||
);
|
||||
}
|
||||
|
||||
function saltFilePath(dataDir) {
|
||||
return path.join(dataDir, SALT_FILE_NAME);
|
||||
}
|
||||
|
||||
function readPersistedSalt(filePath) {
|
||||
try {
|
||||
const raw = fs.readFileSync(filePath, "utf8");
|
||||
const parsed = JSON.parse(raw);
|
||||
const salt = parsed && typeof parsed === "object" ? parsed.salt : undefined;
|
||||
if (typeof salt === "string" && PERSISTED_SALT_RE.test(salt)) return salt;
|
||||
} catch {
|
||||
// Missing, unreadable, or corrupt — fall through to (re)generation.
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Mirrors establishPersistedSalt() in src/lib/machineToken.ts — same resolution
|
||||
* order, same salt file, same `wx`-flag create-race handling — so the CLI and the
|
||||
* server converge on the same bearer token (docs/security/CLI_TOKEN.md). */
|
||||
function establishPersistedSalt(dataDir) {
|
||||
const filePath = saltFilePath(dataDir);
|
||||
const existing = readPersistedSalt(filePath);
|
||||
if (existing) return existing;
|
||||
|
||||
const generated = crypto.randomBytes(32).toString("hex");
|
||||
try {
|
||||
fs.mkdirSync(dataDir, { recursive: true });
|
||||
fs.writeFileSync(filePath, JSON.stringify({ salt: generated }), { flag: "wx", mode: 0o600 });
|
||||
return generated;
|
||||
} catch (err) {
|
||||
if (err && err.code === "EEXIST") return readPersistedSalt(filePath);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Mirrors getActiveSalt() in src/lib/machineToken.ts so a rotated
|
||||
* OMNIROUTE_CLI_SALT reaches the CLI too (docs/security/CLI_TOKEN.md). */
|
||||
function getActiveSalt() {
|
||||
const envSalt = process.env.OMNIROUTE_CLI_SALT;
|
||||
if (envSalt) return envSalt;
|
||||
|
||||
if (_cachedActiveSalt) return _cachedActiveSalt;
|
||||
|
||||
const hasExplicitDataDir = !!(process.env.DATA_DIR && process.env.DATA_DIR.trim());
|
||||
if (!hasExplicitDataDir && isTestContext()) {
|
||||
_cachedActiveSalt = BUILTIN_DEFAULT_SALT;
|
||||
return _cachedActiveSalt;
|
||||
}
|
||||
|
||||
const dataDir = resolveDataDir();
|
||||
const persisted = establishPersistedSalt(dataDir);
|
||||
_cachedActiveSalt = persisted || BUILTIN_DEFAULT_SALT;
|
||||
return _cachedActiveSalt;
|
||||
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
|
||||
}
|
||||
|
||||
export function deriveCliToken(machineIdModule, salt) {
|
||||
@@ -90,7 +19,8 @@ export function deriveCliToken(machineIdModule, salt) {
|
||||
// undefined and calling it throws — which the catch below turned into an
|
||||
// empty token, silently disabling CLI auth for every management request.
|
||||
// Same resolution order as src/lib/machineToken.ts.
|
||||
const machineIdSync = machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
const machineIdSync =
|
||||
machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") return "";
|
||||
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
|
||||
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
|
||||
|
||||
@@ -59,81 +59,10 @@ export function isPidRunning(pid) {
|
||||
}
|
||||
}
|
||||
|
||||
// A port that is already owned must be reported, not spawned into. `omniroute
|
||||
// serve` used to hand the conflict to the child, which died with EADDRINUSE
|
||||
// twice on the supervisor's restart budget and printed three raw Node stack
|
||||
// traces without ever saying another instance owned the port. It did that
|
||||
// AFTER writing the pid files, so the doomed second instance de-registered the
|
||||
// healthy running one (supervisor/.pid left pointing at the dead starter,
|
||||
// server/.pid deleted outright).
|
||||
//
|
||||
// Discovery mirrors killByPort() in bin/cli/commands/stop.mjs (netstat on
|
||||
// win32, lsof elsewhere); the two are worth consolidating next time stop.mjs
|
||||
// is touched.
|
||||
export async function findListeningPids(port, deps = {}) {
|
||||
const platform = deps.platform || process.platform;
|
||||
let exec = deps.execFileAsync;
|
||||
if (!exec) {
|
||||
const { execFile } = await import("node:child_process");
|
||||
const { promisify } = await import("node:util");
|
||||
exec = promisify(execFile);
|
||||
}
|
||||
try {
|
||||
if (platform === "win32") {
|
||||
const { stdout } = await exec("netstat", ["-ano"]);
|
||||
return parseNetstatListeningPids(stdout, port);
|
||||
}
|
||||
const { stdout } = await exec("lsof", ["-ti", `:${port}`]);
|
||||
return stdout
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((entry) => parseInt(entry, 10))
|
||||
.filter((entry) => Number.isFinite(entry) && entry > 0);
|
||||
} catch {
|
||||
// No netstat/lsof available, or simply no listener. Report "free": a false
|
||||
// "busy" would block a legitimate start, the worse failure of the two.
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function parseNetstatListeningPids(stdout, port) {
|
||||
const portCol = `:${port}`;
|
||||
const pids = [];
|
||||
for (const line of stdout.split(/\r?\n/)) {
|
||||
const cols = line.trim().split(/\s+/);
|
||||
// Proto LocalAddress ForeignAddress State PID
|
||||
if (cols.length < 5) continue;
|
||||
if (cols[0] !== "TCP" && cols[0] !== "TCPv6") continue;
|
||||
if (!(cols[1] || "").endsWith(portCol)) continue;
|
||||
if ((cols[cols.length - 2] || "").toUpperCase() !== "LISTENING") continue;
|
||||
const pid = parseInt(cols[cols.length - 1], 10);
|
||||
if (Number.isFinite(pid) && pid > 0 && !pids.includes(pid)) pids.push(pid);
|
||||
}
|
||||
return pids;
|
||||
}
|
||||
|
||||
export function sleep(ms) {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
// A probe that times out is classified "hanging" and never counts toward
|
||||
// readiness (#6800), so a FIXED per-probe timeout puts a hard ceiling on how
|
||||
// slow a healthy first response is allowed to be. On a cold Windows boot the
|
||||
// health route resolves ~10 dynamic imports and reads the DB before it can
|
||||
// answer; when that first response lands past the ceiling the poll can never
|
||||
// succeed, because each abort discards the in-flight request before the route
|
||||
// finishes (its own 1s payload cache is never populated either) and the next
|
||||
// probe restarts the same work into the same ceiling — for the whole budget.
|
||||
// The CLI then printed "⚠ Server did not respond within 60s" over a server
|
||||
// that went on to serve traffic normally. Escalating the timeout keeps #6800's
|
||||
// guarantee (a socket that never answers still yields "hanging" forever) while
|
||||
// letting a slow-but-real response actually be observed.
|
||||
const INITIAL_PROBE_TIMEOUT_MS = 2000;
|
||||
const MAX_PROBE_TIMEOUT_MS = 15000;
|
||||
// Floor for the last probe of a budget that is nearly spent — long enough for a
|
||||
// loopback round-trip, short enough not to overrun the caller's timeout.
|
||||
const MIN_PROBE_TIMEOUT_MS = 250;
|
||||
|
||||
// #2460: Default raised from 15s to 60s so Windows users (slower Next.js
|
||||
// cold start due to filesystem watchers, antivirus, etc.) get a working
|
||||
// "server ready" signal instead of a phantom timeout while the server is
|
||||
@@ -154,24 +83,18 @@ export function resolveReadyTimeoutMs(overrides = {}) {
|
||||
if (typeof overrides.timeoutMs === "number" && overrides.timeoutMs > 0) {
|
||||
return overrides.timeoutMs;
|
||||
}
|
||||
const envValue = Number.parseInt(process.env.OMNIROUTE_READY_TIMEOUT_MS || "", 10);
|
||||
const envValue = Number.parseInt(
|
||||
process.env.OMNIROUTE_READY_TIMEOUT_MS || "",
|
||||
10
|
||||
);
|
||||
return Number.isFinite(envValue) && envValue > 0 ? envValue : DEFAULT_READY_TIMEOUT_MS;
|
||||
}
|
||||
|
||||
// `onOutcome` receives every probe classification so a caller can tell a
|
||||
// "nothing ever bound the port" timeout apart from a "port is up, the health
|
||||
// route is just still warming" one when it reports the failure.
|
||||
export async function waitForServer(port, timeout = 60000, { onOutcome } = {}) {
|
||||
export async function waitForServer(port, timeout = 60000) {
|
||||
const start = Date.now();
|
||||
let tcpListeningSince = null;
|
||||
let probeTimeout = INITIAL_PROBE_TIMEOUT_MS;
|
||||
while (Date.now() - start < timeout) {
|
||||
const remaining = timeout - (Date.now() - start);
|
||||
const outcome = await pollHealthOnce(
|
||||
port,
|
||||
Math.max(MIN_PROBE_TIMEOUT_MS, Math.min(probeTimeout, remaining))
|
||||
);
|
||||
onOutcome?.(outcome);
|
||||
const outcome = await pollHealthOnce(port);
|
||||
if (outcome === "ready") return true;
|
||||
if (outcome === "fast-reject") {
|
||||
if (tcpListeningSince === null) tcpListeningSince = Date.now();
|
||||
@@ -180,11 +103,6 @@ export async function waitForServer(port, timeout = 60000, { onOutcome } = {}) {
|
||||
// "hanging" (request timed out with no response at all) or
|
||||
// "not-listening" — neither counts toward the grace window.
|
||||
tcpListeningSince = null;
|
||||
// Only a hang says "this server may simply need longer to answer";
|
||||
// widen the next probe instead of aborting into the same ceiling again.
|
||||
if (outcome === "hanging") {
|
||||
probeTimeout = Math.min(probeTimeout * 2, MAX_PROBE_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
await sleep(500);
|
||||
}
|
||||
@@ -197,13 +115,11 @@ export async function waitForServer(port, timeout = 60000, { onOutcome } = {}) {
|
||||
// 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). The
|
||||
// caller widens `probeTimeoutMs` after a hang so a merely slow (rather
|
||||
// than dead) server is not aborted into the same ceiling on every probe.
|
||||
// process accepted the TCP connection but never answered (#6800).
|
||||
// - "not-listening": nothing is accepting connections on the port at all.
|
||||
// #11766: probe both IPv4 and IPv6 loopback to handle servers listening on
|
||||
// either family (or both).
|
||||
async function pollHealthOnce(port, probeTimeoutMs = INITIAL_PROBE_TIMEOUT_MS) {
|
||||
async function pollHealthOnce(port) {
|
||||
const hosts = ["127.0.0.1", "::1"];
|
||||
const outcomes = [];
|
||||
|
||||
@@ -212,7 +128,7 @@ async function pollHealthOnce(port, probeTimeoutMs = INITIAL_PROBE_TIMEOUT_MS) {
|
||||
hosts.map(async (host) => {
|
||||
try {
|
||||
const res = await fetch(`http://${host}:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(probeTimeoutMs),
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return { host, outcome: res.ok ? "ready" : "fast-reject" };
|
||||
} catch (err) {
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(docs):** every Markdown page under `docs/` is now mirrored in all 65 dashboard locales, not only the 22-page core set — 152 sources × 65 locales = 9,880 mirrors (6,208 new), with the 🌐 language bar of every mirror rewritten for the full locale list. The docs drift gate (`npm run i18n:check`, blocking in CI) derives its scope from the tree, so it now guards all 152 pages. Found and fixed by the run in `scripts/i18n/run-translation.mjs`: a markdown table or tight bullet list with no blank line inside it (PROVIDER_REFERENCE.md's 244-row table, FREE_TIERS.md's 71-item list) was sent as one 16–40 KB request that outlived the backend socket for verbose scripts (Greek, Amharic); oversized runs of table rows or list items are now cut at item boundaries and rejoined without a blank line, so no chunk exceeds 6 KB across the docs tree. 48 older mirrors whose tables had lost rows were retranslated with the fixed chunker.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** `openai-compatible-*` connections can now report billing/quota in Provider Limits. The connection declares its own quota endpoint, auth mode and a dot-path mapping onto `UsageQuota` in `providerSpecificData.quotaEndpoint`, so no upstream-specific code is needed per service — a mapping that resolves nothing reports no quota rather than an exhausted-looking 0/0 ([#13616](https://github.com/diegosouzapw/OmniRoute/issues/13616))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** track LLM Gateway DevPass quota — the `llmgateway` provider now reads its monthly plan-credit and weekly premium-model allowance from `GET /v1/key` and surfaces both windows in Dashboard › Limits and quota-aware preflight ([#12462](https://github.com/diegosouzapw/OmniRoute/pull/12462)).
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(gamification): pay the documented `streak_bonus` and `badge_unlock` XP rewards.** `XP_REWARDS` listed both rewards but the award pipeline never paid them: the private reward table in `events.ts` omitted them, `updateStreak()` did not report when a streak extended, and badge unlocks carried no XP. Every request that extends a daily streak now pays `streak_bonus × streak length` once per UTC day (guarded by a same-day `xp_audit_log` check), and every badge unlocked through the pipeline pays `badge_unlock` once per badge (guarded by the `user_badges` primary key; `unlockBadge()` now reports whether it inserted). Bonus XP flows through the same `addXp` + level sync + global/weekly/monthly leaderboard path as action XP, so level-ups and rankings include it. The Radar supporter recognition unlock stays XP-free. (#12522 — thanks @pacocartones)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** register `gemini-3.8-flash` ([#12638](https://github.com/diegosouzapw/OmniRoute/issues/12638)) — Gemini 3.8 Flash (DeepMind 2026-09-02) with tool calling and vision support
|
||||
@@ -0,0 +1 @@
|
||||
- **feat(providers):** advertise a `free-tier` capability in the provider plugin manifest for every provider with documented free models, so sidecars and dashboards can filter free-capable providers without reading the quota catalog ([#12786](https://github.com/diegosouzapw/OmniRoute/pull/12786)) — thanks @maxmad64bis
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(proxylogs):** proxy log columns and detail pane now show the registry proxy name instead of a bare `host:port` when several registry entries share the same gateway ([#12814](https://github.com/diegosouzapw/OmniRoute/pull/12814)) — thanks @tiangao88
|
||||
@@ -1 +0,0 @@
|
||||
- **compression:** add Hungarian Caveman language pack with Hungarian-specific rules, language detection, localized output instructions, and language-pack tests. (#12825 - thanks @botii16)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** parse/scrub DSML tool-call markers embedded in reasoning and recognize adaptive thinking on the response side — `dsmlToolCalls.ts` module + translator/stream/handler wiring ([#12905](https://github.com/diegosouzapw/OmniRoute/pull/12905)) — thanks @initguru
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(codex):** safely discover compatible models by classifying upstream models before activation to keep hidden, unsupported, retired, or newer-client models out of the active catalog, exposing candidate diagnostics while persisting only active models, adding GPT-6 Astra fallback definitions, and bumping the tested Codex CLI version to 0.153.4 ([#12933](https://github.com/diegosouzapw/OmniRoute/pull/12933)) — thanks @TheDemonTuan
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(api):** `POST /api/keys` accepts `expiresAt` (ISO datetime, nullable) with the same semantics as the key-update path, so automation can create an expiring key in one operation instead of create-then-update. Omitted/null preserves the current non-expiring behavior; enforcement reuses the existing expiry policy ([#12952](https://github.com/diegosouzapw/OmniRoute/pull/12952)) — thanks @caniko
|
||||
1
changelog.d/features/12985-eurouter-provider.md
Normal file
1
changelog.d/features/12985-eurouter-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** Added EURouter as an OpenAI-compatible API-key gateway (`https://api.eurouter.ai/v1`), with live model discovery via `passthroughModels`. Its copy states that models are served by third-party upstreams listed per model, so an EU-based router is not read as EU data residency for inference.
|
||||
1
changelog.d/features/12986-greenpt-provider.md
Normal file
1
changelog.d/features/12986-greenpt-provider.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(providers):** Added GreenPT as an OpenAI-compatible API-key provider (`https://api.greenpt.ai/v1`), with live model discovery via `passthroughModels`. No free-inference badge: the published docs describe a free API subscription billed per token, not a free tier.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(build):** add build:fast and start:fast to bypass standalone tracing ([#13021](https://github.com/diegosouzapw/OmniRoute/pull/13021)) — thanks @tuandinh0801
|
||||
1
changelog.d/features/13044-i18n-batch-eu.md
Normal file
1
changelog.d/features/13044-i18n-batch-eu.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(i18n):** 9 new locales — Greek (`el`), Croatian (`hr`), Serbian (`sr`), Lithuanian (`lt`), Estonian (`et`), Latvian (`lv`), Slovenian (`sl`), Maltese (`mt`), Irish (`ga`) — across the dashboard, docs mirrors, CLI, README and the site; OmniRoute now ships every official EU language (51 locales). (#13044)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** `OMNIROUTE_DISABLE_CONVERSATION_TRACKING=1` turns off conversation-history collection for operators who do not use the dashboard's conversation view. `resolveConversationId()` returns an untracked result before it reads SQLite or parses message history, and the switch also covers client-supplied session IDs. Routing-session handling is unchanged, tracking stays on by default, and existing records are not deleted. One reporting install held 5.97 million turn records at about 4.26 GB ([#13150](https://github.com/diegosouzapw/OmniRoute/pull/13150))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** Add `auto/kimi`, `auto/qwen`, `auto/deepseek`, `auto/gpt`, and the `auto/claude-haiku` fast variant to the built-in routing catalog, including bare `k3` models on Kimi coding and web backends (issue #13214).
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** Claude OAuth usage now shows the separate weekly Fable limit next to the shared five-hour and weekly meters. Anthropic reports it as a `weekly_scoped` entry in `limits[]`, which OmniRoute ignored, so the pool was invisible. The provider-limits cache keeps `modelQuotas` and restores it on stale-data fallback. The Fable meter is display-only and does not affect routing, account selection, or cooldowns ([#13266](https://github.com/diegosouzapw/OmniRoute/pull/13266))
|
||||
@@ -1,5 +0,0 @@
|
||||
- **feat(playground): copy an individual Compare column's response.** Each column in the Compare
|
||||
tab now has a copy button beside the remove button, reusing the existing `useCopyToClipboard`
|
||||
hook to copy that column's response text and show a checkmark while `disabled` on an empty
|
||||
response. (The independent-scrolling half of this PR was already fixed separately in #13532.)
|
||||
(#13317 — thanks @ventulus95)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(models):** add Gemini 3.8 Flash tiers to Antigravity and AGY catalogs ([#13318](https://github.com/diegosouzapw/OmniRoute/pull/13318)) — thanks @tuandinh0801, with credit to #12499 (@Abhishekchhetri020)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse): learn hard request caps stated in 429 bodies and pace under them.** Providers such as TokenRouter reject bursts with prose like `Maximum 5 requests within 1 minutes` and no rate-limit headers, so the limiter never learned the ceiling and kept racing into it; every 429 also tore the limiter down and rebuilt it with no pacing. `updateFromResponseBody` now parses that phrasing (and `N requests per minute`, `N requests per M seconds`, `N RPM`) into a per-window cap, applies it to the limiter as an empty reservoir that refills `N` every window with calls spread `window / N` apart, and records it in `learnedRateLimits`. A learned cap is reapplied whenever the limiter is rebuilt after a 429 and when limits are restored at startup, unless the connection has an explicit RPM override. Fixes [#13594](https://github.com/diegosouzapw/OmniRoute/issues/13594).
|
||||
1
changelog.d/features/13660-i18n-batch-sa.md
Normal file
1
changelog.d/features/13660-i18n-batch-sa.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(i18n):** 8 new locales — Kannada (`kn`), Malayalam (`ml`), Odia (`or`), Punjabi (`pa`), Nepali (`ne`), Sinhala (`si`), Burmese (`my`), Khmer (`km`) — across the dashboard, docs mirrors, CLI, README and the site (59 locales). The translator now restores the ICU literal escape around angle placeholders and splits oversized docs sections before translating. (#0000)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(api):** `POST /v1/rerank` (and the memory engine's loopback rerank step) can route to OpenAI-compatible provider nodes on a LAN/Tailscale host — not only loopback — behind the new `RERANK_REMOTE_PROVIDER_NODES` feature flag (default off), subject to the provider outbound URL policy; the loopback host check is consolidated into `@/shared/network/loopbackNodeHost` shared by rerank, audio, and the local health checker ([#13732](https://github.com/diegosouzapw/OmniRoute/pull/13732)) — thanks @seanford
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(security):** OmniRoute now warns at boot when the server that answers `/v1` inference is bound to a non-loopback interface while `REQUIRE_API_KEY` is disabled. The guard added in [#12568](https://github.com/diegosouzapw/OmniRoute/pull/12568) covered the API bridge (`API_HOST`, default loopback) and the live dashboard WebSocket, but not the Next server that actually serves `/v1/chat/completions` and `/v1/responses` — which binds `HOST || 0.0.0.0`, every interface by default. That matters because `GET /v1/models` follows the dashboard login posture (`requireAuthForModels`) while inference follows `REQUIRE_API_KEY`, so an instance with an admin password and `REQUIRE_API_KEY=false` answers `401` to the probe an operator naturally runs while inference stays open to anyone who can reach the port. The bound host is resolved from `OMNIROUTE_BOUND_HOST` (published by `scripts/dev/run-next.mjs`) then Next's own `HOSTNAME` (the Docker path); `HOST` is deliberately excluded because the standalone server ignores it and a warning naming the wrong interface is worse than none. New `docs/security/INFERENCE_AUTH_POSTURE.md` documents the split, how to actually probe inference, and the [#2257](https://github.com/diegosouzapw/OmniRoute/issues/2257) caveat that an invalid bearer degrades to anonymous. ([#13820](https://github.com/diegosouzapw/OmniRoute/pull/13820))
|
||||
@@ -1 +0,0 @@
|
||||
- Expose combo wall-clock timeout (`comboTimeoutMs`) next to Target timeout in the combo editor and Combo defaults. Empty keeps the 10-minute hang-stop; a positive value replaces it. ([#13857](https://github.com/diegosouzapw/OmniRoute/pull/13857))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(compression):** Lite tool-result truncation length is configurable (`lite.maxToolLength`, env `OMNIROUTE_LITE_MAX_TOOL_LENGTH`). Default stays 2000. An out-of-range step cap no longer hides a valid global cap; a toggle-only settings write keeps a stored cap; `maxToolLength: null` clears it. Dashboard copy no longer hard-codes 2,000 characters. ([#13915](https://github.com/diegosouzapw/OmniRoute/pull/13915) — refs [#13178](https://github.com/diegosouzapw/OmniRoute/issues/13178))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(proxy):** support multiple local core endpoints, one per line ([#13923](https://github.com/diegosouzapw/OmniRoute/pull/13923) — thanks @maxmad64bis)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(cli):** Add remote/context-aware manual model CRUD with metadata validation, dry-run, protected override removal and persistence readback; prefer the public model catalog and keep JSON/JSONL complete. Manual models remain inference-unverified. ([#14392](https://github.com/diegosouzapw/OmniRoute/pull/14392))
|
||||
1
changelog.d/features/agnes-30-flash-catalog.md
Normal file
1
changelog.d/features/agnes-30-flash-catalog.md
Normal file
@@ -0,0 +1 @@
|
||||
- feat(providers): **list Agnes 3.0 Flash as the current free chat model, drop retired 1.5 Flash, add Image 2.0/2.5 Flash plus Video 2.5/2.5 Flash, and discover the live `/v1/models` catalog (including the CN host `api.agnes-ai.cn`).** `agnes-1.5-flash` now forwards to `agnes-3.0-flash`. Video 2.5 polls `GET /v1/videos/{id}` (not the V2.0 `/agnesapi` contract). Live `/v1/models` (2026-09-09) no longer serves 1.5; the wiki marks it deprecated. 3.0 Flash is 512K context / 65,536 max output, same window as 2.5. CN-region keys use the existing per-connection base-URL field, default stays `apihub.agnes-ai.com`.
|
||||
@@ -1,6 +0,0 @@
|
||||
- **feat(api):** add per-key `allowAutoCombos` (default `true`) to gate the built-in `auto/*`
|
||||
combos, which previously bypassed a key's `allowedCombos`/`allowedModels`/`blockedModels`
|
||||
restrictions entirely — a restricted key could still reach any model through `auto/best-fast`.
|
||||
Also adds a per-key `catalogScope` (`all`/`combos`/`models`) to control what `/v1/models`
|
||||
advertises, and the dashboard gained an Auto Combos toggle and a catalog scope selector in the
|
||||
API key permissions UI.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** Claude OAuth connections can opt in (per account, Edit connection → Claude section) to Claude Code's lower-priority lane and once-a-week session-limit reset. After the first 5-hour usage-wall 429 carrying `anthropic-ratelimit-unified-slow-offer: treatment`, OmniRoute retries the same account with `anthropic-usage-limit: slow` and keeps sending it until the window resets — the account keeps serving past the limit instead of being cooled down (slot_busy/529 wait the server's `slow-retry-after`, bounded by `slow-max-wait`). With auto-reset on, the wall first tries `POST /api/organizations/{org}/reset_rate_limits` (`juniper_tide`) and retries at full speed when the server grants it. Both default off; nothing is sent before the limit is hit.
|
||||
1
changelog.d/features/codex-gpt-6-astra.md
Normal file
1
changelog.d/features/codex-gpt-6-astra.md
Normal file
@@ -0,0 +1 @@
|
||||
- **feat(sse):** Codex and OpenAI catalogs list GPT-6 Astra with effort aliases (`-low` through `-ultra`); Codex CLI identity pins `@openai/codex@0.153.4` in lockstep with the image ([#13026](https://github.com/diegosouzapw/OmniRoute/pull/13026))
|
||||
@@ -1 +0,0 @@
|
||||
- feat(providers): update Fish Audio for S2.1 Pro Free, validated advanced TTS controls, and provider-scoped persistent voice-clone management.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** redeem **GLM Coding Plan Reset Cards** (`glm` / `glm-cn` / `glmt` / `zai`) from the Provider Limits UI — clear an exhausted 5-hour or weekly coding-plan window before it rolls over, via the new `/api/usage/glm-reset-card` route (`GET` lists, `POST` redeems). List and redeem requests egress through the connection's proxy and honor exclusive-lease isolation; z.ai's `requestId` is reused for retries of an ambiguous (transport-failed) redemption so a lost response cannot double-consume a card (in-memory, best-effort — restart the server and a fresh key is required). Responses are validated fail-closed (HTTP 200 alone is never treated as success), unavailable/expired cards are filtered and the list is sorted by earliest expiry, and the post-redemption quota refresh is best-effort: a refresh failure still reports the successful reset.
|
||||
11
changelog.d/features/orchestration-compare-runs.md
Normal file
11
changelog.d/features/orchestration-compare-runs.md
Normal file
@@ -0,0 +1,11 @@
|
||||
- **feat(dashboard):** the orchestration History tab gained a "Compare runs" mode — toggling it
|
||||
turns each grid cell into a 2-item selection queue (a 3rd click drops the oldest pick), and
|
||||
picking two cells opens a side-by-side comparison panel instead of the usual detail drawer.
|
||||
The panel fetches both runs' detail the same way the drawer does (falling back to persisted
|
||||
history once a run leaves the live TTL window) and shows, per side: identity/source/state,
|
||||
start time, a signed `right - left` delta for duration/cost/event count, the event timeline
|
||||
aligned by index, and any memory hits. One side's fetch failing never blocks the other, and a
|
||||
delta is only ever computed when both sides have a finite value — otherwise it renders "—",
|
||||
never `NaN`. Comparing two runs from different sources or skills still works; a banner marks
|
||||
the deltas as informational rather than hiding them, since the two runs aren't a strict
|
||||
apples-to-apples pair.
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(routing): self-hosted unified OpenAI-compatible entry (`/v1/chat/completions`).** When `OMNIROUTE_SELF_HOSTED_PROVIDERS` (inline YAML) or `OMNIROUTE_SELF_HOSTED_PROVIDERS_FILE` is set, the existing `/v1/chat/completions` route diverts through the self-hosted provider adapters (`open-sse/services/providerAdapters.ts`) — OpenAI / Anthropic / local-compatible — instead of the cloud pipeline. Provider is auto-routed via the `x-omniroute-provider` header, a `provider/model` (or `provider::model`) model prefix, or the first configured provider; upstream credentials stay runtime-only and are stripped from echoed responses. Optional `OMNIROUTE_SELF_HOSTED_API_KEY` guards the entry with `Authorization: Bearer` (reserved for the D5 quota-key system); unset = open loopback/trusted-network route. Upstream failures return the standard OpenAI error shape (including a normalized 502 for unreachable providers). One OpenAI SDK snippet can now traverse multiple self-hosted providers without changing the client. (#RIC-738 / RIC-697 D4)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(routing): deterministic routing strategies for the self-hosted entry (`strategy:` block, M2/RIC-740).** The unified `/v1/chat/completions` entry (RIC-738) now accepts a declarative `strategy:` block — inline in the providers YAML or via `OMNIROUTE_SELF_HOSTED_STRATEGY` / `OMNIROUTE_SELF_HOSTED_STRATEGY_FILE` — expressing five explainable, non-predictive routing policies: blacklist / whitelist (hard filters), cooldown circuit breaker (`consecutiveFailures` + `cooldownMs`), cost-priority (cheapest `costPer1MInput` first), latency-aware (fastest recent average first), and an explicit `fallbackChain` order. The ordered candidate list is the fallback chain: a failed primary (network or non-2xx) falls through to the next candidate, and each failure feeds the breaker. Every response carries `x-omniroute-route-decision` — the one-line "why this model / why not that one" audit trail (D3). A pinned provider rejected by a hard filter returns `400` (never a silent re-route); no eligible providers returns `503` with the full explainable decision. No ML/predict dependency; malformed strategy config returns `500` rather than silently becoming a no-op. (#RIC-740 / RIC-697 D3)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(dashboard):** add sidebar pinned items shortcut section with individual item pin toggle and localStorage persistence ([#12891](https://github.com/diegosouzapw/OmniRoute/pull/12891))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(i18n):** every dashboard catalog other than `pt-BR` (64 locales) went through the same quality review `pt-BR` received in #13885 — each leaf changed by the 2026-09 retranslation was checked against its English source by the translation backend and rewritten where the meaning, placeholders, register or product terminology were off: 73,586 corrections net (75,263 applied, 1,677 that had turned a real translation into the plain English term reverted so the real-translation ratio gate stays where it was). `scripts/i18n/review-locale.mjs` now survives an upstream hiccup (per-batch retries with backoff, skipped batches listed in `_artifacts/i18n-review/<code>.skipped.json`), checkpoints the catalog every 25 batches instead of writing only at the end, and writes leaves whose own key contains a dot (`compliance.eventTypes["apiKey.ban"]`) instead of crashing.
|
||||
@@ -1 +0,0 @@
|
||||
- **Combo routing:** a context-cache-pinned model that returns `401` now falls through to the normal combo fallback loop instead of terminating the request, allowing other eligible connections or providers to serve it.
|
||||
1
changelog.d/fixes/0000-responses-node-model-test.md
Normal file
1
changelog.d/fixes/0000-responses-node-model-test.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(dashboard):** model health tests for a provider node set to the Responses API now call `/v1/responses` with a Responses-shaped body instead of `/v1/chat/completions` — those models were reported as `Provider returned HTTP 200 but no text content` even though the same model answered normally through `/v1/responses` ([#13070](https://github.com/diegosouzapw/OmniRoute/issues/13070))
|
||||
@@ -1,5 +0,0 @@
|
||||
- **fix(sse):** the Antigravity account picked for a request can now be reserved for that
|
||||
request's streaming lifecycle, so a concurrent retry or the credential handoff cannot re-pick
|
||||
an account already committed to an in-flight stream; a fully leased pool answers with a
|
||||
structured 503 `antigravity_pool_busy` carrying a bounded `Retry-After`. Opt-in behind the
|
||||
new `ANTIGRAVITY_ACCOUNT_LEASE_ENABLED` flag (default off) (#10011) — thanks @Ardem2025
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(docker):** bump the Bun image to 1.4.0, enable Turbopack on Bun, and port the node image's build memory guards so the `-bun` container builds fit the 16 GB GitHub runner instead of dying with `cannot allocate memory` ([#11719](https://github.com/diegosouzapw/OmniRoute/pull/11719)). Both images now default `OMNIROUTE_BUILD_WORKERS` to `2` (1 page-data worker) against the measured ~4.5 GB per-process RSS budget (#7518/#11663).
|
||||
@@ -1 +0,0 @@
|
||||
- **security(runtime):** fail closed on hostile thrown values and keep upstream text out of public error surfaces — the chat pipeline now reads rejection metadata through a safe accessor, sanitizes the message before it reaches call logs and console, and projects the failure-usage code onto the bounded public vocabulary; Perplexity's non-streaming quota/upstream error body sanitizes the upstream message and projects the provider-supplied error code; Arena (lmarena) maps every public failure onto a fixed vocabulary instead of echoing the upstream error; Notion's TLS transport failure sanitizes the transport error before it reaches the response body ([#11742](https://github.com/diegosouzapw/OmniRoute/pull/11742)).
|
||||
@@ -0,0 +1 @@
|
||||
- fix(routing): stop a round-robin combo's "opencode" targets from collapsing onto the opencode-zen connection (#11912)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(routing): stop the reactive-compaction debug log from lying when compression is globally disabled (#11977)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(electron): relativize standalone-bundle symlink targets so Stage 8 manifest verification stops failing on Windows (#11979)
|
||||
1
changelog.d/fixes/12061-compression-studio-run-error.md
Normal file
1
changelog.d/fixes/12061-compression-studio-run-error.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): surface a visible error when Compression Studio's combined preview run fails (#12061)
|
||||
1
changelog.d/fixes/12063-compression-profile-header.md
Normal file
1
changelog.d/fixes/12063-compression-profile-header.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): make the compression "Effective pipeline" preview honor the active profile and warn when the master switch is off (#12063)
|
||||
1
changelog.d/fixes/12072-tinycms-dom-shim-leak.md
Normal file
1
changelog.d/fixes/12072-tinycms-dom-shim-leak.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): scope TinyCMS Web signer's DOM shims to each call instead of leaking them for the process lifetime, and surface a clean HTTP status on a non-JSON interception-toggles error (#12072)
|
||||
1
changelog.d/fixes/12111-vision-bridge-model-lockout.md
Normal file
1
changelog.d/fixes/12111-vision-bridge-model-lockout.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(guardrails): stop Vision Bridge from re-selecting a model locked after a 404 (#12111)
|
||||
@@ -0,0 +1 @@
|
||||
- fix(sse): require Responses-shaped body before native OpenAI-compatible passthrough (#12129)
|
||||
1
changelog.d/fixes/12132-minimax-m3-adaptive-thinking.md
Normal file
1
changelog.d/fixes/12132-minimax-m3-adaptive-thinking.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): minimax-m3 now collapses manual thinking.type:"enabled" to adaptive, preventing upstream 400 (2013) (#12132)
|
||||
1
changelog.d/fixes/12172-model-id-collision-chat-image.md
Normal file
1
changelog.d/fixes/12172-model-id-collision-chat-image.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(db): scope model visibility overrides by modality so hiding a Chat model no longer hides an identically-ID'd Image/Embeddings/etc. model (#12172)
|
||||
1
changelog.d/fixes/12173-lmstudio-multi-account.md
Normal file
1
changelog.d/fixes/12173-lmstudio-multi-account.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(db): scope local-provider apiKey dedup to matching base URL so LM Studio/Ollama-style connections support multiple accounts (#12173)
|
||||
1
changelog.d/fixes/12190-trae-referer-401.md
Normal file
1
changelog.d/fixes/12190-trae-referer-401.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): refresh Trae's stale Referer/Origin and forward user timezone so imported connections stop failing with 401 (#12190)
|
||||
1
changelog.d/fixes/12196-opencode-go-gpt56luna.md
Normal file
1
changelog.d/fixes/12196-opencode-go-gpt56luna.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(providers): route opencode-go/gpt-5.6-luna to /responses instead of /chat/completions (#12196)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(resilience): only clear the combo-level LKGP pin when it names the target that actually failed, so an unrelated target skip under `auto`/`round-robin` no longer discards a valid pin for a healthy provider (#12235)
|
||||
1
changelog.d/fixes/12251-extra-upstream-headers-delete.md
Normal file
1
changelog.d/fixes/12251-extra-upstream-headers-delete.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): allow deleting the last extra-upstream-header row even when invalid (#12251)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** 429 bodies phrased as `N API calls / month` (Cohere trial keys) now classify as `quota_exhausted` instead of a short transient `rate_limit`, so a spent monthly allowance is no longer retried every few seconds for the rest of the billing cycle ([#12252](https://github.com/diegosouzapw/OmniRoute/pull/12252)) — thanks @brick30llc-ctrl
|
||||
1
changelog.d/fixes/12272-missing-i18n.md
Normal file
1
changelog.d/fixes/12272-missing-i18n.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(i18n):** translate pre-existing `__MISSING__:` keys for `combo.sort`, `requestLogger.detail` expand/collapse, `common.profile`, and `settings.resilienceCredentialHealth*` across 39 locales ([#12272](https://github.com/diegosouzapw/OmniRoute/issues/12272))
|
||||
1
changelog.d/fixes/12296-node-runtime-guard-early.md
Normal file
1
changelog.d/fixes/12296-node-runtime-guard-early.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(cli): run the Node.js runtime compatibility guard before the heavy `tsx/esm` + Commander import chain so an unsupported runtime gets a clear message instead of a raw `Invalid regular expression flags` crash (#12296)
|
||||
1
changelog.d/fixes/12298-provider-node-delete-refresh.md
Normal file
1
changelog.d/fixes/12298-provider-node-delete-refresh.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(dashboard): refresh the providers list after deleting a compatible provider node (#12298)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cache): fold `response_format`/Responses-API `text.format` into the semantic cache signature so a `temp=0` request can no longer be served a stored response body with a different output schema (#12307)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(gemini): preserve response-schema nullability across union flattening so a model with nothing to say returns a valid null instead of the string `"null"` or a fabricated value (#12308)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(combo):** a priority combo whose steps are different models on one Claude OAuth connection now falls through to the next step — a model-specific 404 or 5xx is scoped to the model instead of retiring the whole account, while a 429 stays account-wide ([#12334](https://github.com/diegosouzapw/OmniRoute/issues/12334))
|
||||
1
changelog.d/fixes/12341-budget-alias-auto.md
Normal file
1
changelog.d/fixes/12341-budget-alias-auto.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(usage): fail closed on API-key budget enforcement when a provider's `auto` routing alias has no pricing row, instead of silently counting it as $0 (#12341)
|
||||
1
changelog.d/fixes/12356-agnes-video-poll-model-name.md
Normal file
1
changelog.d/fixes/12356-agnes-video-poll-model-name.md
Normal file
@@ -0,0 +1 @@
|
||||
- **fix(providers):** include the submitted Agnes video model when polling by `video_id`
|
||||
@@ -0,0 +1 @@
|
||||
- **fix(routing):** custom OpenAI-compatible nodes now honor the saved Chat/Responses API type after edits instead of letting the node's original ID prefix override the live connection setting ([#11884](https://github.com/diegosouzapw/OmniRoute/issues/11884)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(memory):** extracted facts and oversized extraction input are now truncated at a word or sentence boundary instead of at a hard character offset. `sanitizeMatch()` (500-char fact cap) and `capExtractionText()` (64KB extraction-input cap) previously sliced at the exact limit, which could cut a fact mid-word or mid-clause; both now back the cut index off within an 80-char lookback window, preferring sentence-ending punctuation (`. ! ?`), then a plain word boundary, and only falling back to the original hard cut when neither is found — the same pattern already used for `compressToolResults` (#8169) — thanks @LeMonBLOCK ([#12383](https://github.com/diegosouzapw/OmniRoute/pull/12383))
|
||||
1
changelog.d/fixes/12398-claude-truly-empty-stream.md
Normal file
1
changelog.d/fixes/12398-claude-truly-empty-stream.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): surface an error instead of a silent empty 200 when a Claude stream closes with zero bytes (#12398)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(chatCore):** stop `executeWithUpstreamStartTimeout` leaking its abortPromise listener onto the long-lived client/stream signal, and stop `mergeAbortSignals` leaking per-attempt abort listeners, so a later hedge cancellation or client disconnect cannot reject an orphaned promise and take the process down (`Error [AbortError]: hedge-cancelled`). The crash guard also absorbs combo abort reasons (`hedge-cancelled`, `combo-per-model-timeout`) and raw string disconnect reasons as a last-resort net ([#12406](https://github.com/diegosouzapw/OmniRoute/pull/12406) — thanks @Beexly)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user