mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-22 06:42:19 +03:00
Compare commits
3 Commits
dependabot
...
fix/13131-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc27bbb0fd | ||
|
|
f121836fd0 | ||
|
|
bf0b618e94 |
140
.env.example
140
.env.example
@@ -541,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)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -664,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
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -763,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.
|
||||
@@ -1156,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
|
||||
@@ -1217,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=
|
||||
@@ -1474,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
|
||||
@@ -1609,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).
|
||||
@@ -2483,15 +2406,6 @@ APP_LOG_TO_FILE=true
|
||||
# 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
|
||||
@@ -2724,16 +2638,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:
|
||||
|
||||
2
.github/workflows/codeql.yml
vendored
2
.github/workflows/codeql.yml
vendored
@@ -22,7 +22,7 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4.38.0
|
||||
- uses: github/codeql-action/init@cdf488f595d80d6e07e03d4674febd5ab45fa938 # v4.37.9
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
queries: security-extended
|
||||
|
||||
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
|
||||
1
.gitignore
vendored
1
.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)
|
||||
|
||||
140966
.i18n-state.json
140966
.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 |
|
||||
|
||||
20
Dockerfile
20
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,7 +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
|
||||
RUN mkdir -p /app/data
|
||||
|
||||
# #13679: default the PUBLISHED image to requiring an API key. A bare
|
||||
# `docker run -p 20128:20128 … diegosouzapw/omniroute` (README/QUICK-START
|
||||
@@ -248,24 +247,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
|
||||
|
||||
|
||||
34
README.md
34
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 → 358 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. 358 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 358 providers. Automatic fallback keeps routing while another healthy target is available. Six pillars: resilient fallback across 358 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/>
|
||||
@@ -486,7 +486,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 359 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — a dated feature snapshot vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 358 providers, 150+ free tiers built in, 19 routing strategies, 12-engine token compression, built-in MCP server with 110 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA and 42 i18n UI locales. OmniRoute is MIT-licensed and self-hostable. Competitor capabilities and counts may change; see the linked methodology."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -725,7 +725,7 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
<td align="center" width="150"><img src="./public/providers/pollinations.svg" width="42" alt="Pollinations"/><br/><b>Pollinations</b><br/><sub>GPT, Llama, Claude<br/>No key needed</sub></td>
|
||||
<td align="center" width="150"><img src="./public/providers/cloudflare.svg" width="42" alt="Cloudflare AI"/><br/><b>Cloudflare AI</b><br/><sub>50+ models<br/>10K neurons/day</sub></td>
|
||||
<td align="center" width="150"><img src="./public/providers/nvidia.svg" width="42" alt="NVIDIA NIM"/><br/><b>NVIDIA NIM</b><br/><sub>GLM, MiniMax<br/>~40 RPM free</sub></td>
|
||||
<td align="center" width="150"><img src="./public/openference.svg" width="42" alt="Openference"/><br/><b>Openference</b><br/><sub>Qwen3.8 27B, Llama 3.2<br/>Free tier</sub></td>
|
||||
<td align="center" width="150"><img src="./public/providers/cerebras.svg" width="42" alt="Cerebras"/><br/><b>Cerebras</b><br/><sub>GLM 4.7, GPT-OSS<br/>1M tokens/day</sub></td>
|
||||
<td align="center" width="150"><img src="./public/providers/openrouter.svg" width="42" alt="OpenRouter"/><br/><b>OpenRouter</b><br/><sub>:free models<br/>+$10 → higher RPM</sub></td>
|
||||
</tr>
|
||||
</table>
|
||||
@@ -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`);
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -90,10 +90,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);
|
||||
|
||||
@@ -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 "";
|
||||
}
|
||||
|
||||
@@ -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)).
|
||||
@@ -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
|
||||
@@ -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(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(build):** add build:fast and start:fast to bypass standalone tracing ([#13021](https://github.com/diegosouzapw/OmniRoute/pull/13021)) — thanks @tuandinh0801
|
||||
@@ -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(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,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 +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.
|
||||
@@ -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,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)).
|
||||
@@ -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 +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 +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 +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)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(usage):** Render OpenRouter PAYG account credits as a metered quota when no per-key spending limit is set ([#12468](https://github.com/diegosouzapw/OmniRoute/pull/12468))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** `omniroute serve` no longer reports "Server did not respond within 60s" for a server that is actually up: the readiness probe's per-attempt timeout now escalates (2s, 4s, 8s, 15s, clamped to the time left in the budget) instead of aborting every attempt at a fixed 2s, so a health route that needs more than 2s for its first response is observed rather than repeatedly torn down. The timeout diagnostic now also states whether the port was accepting connections. ([#12484](https://github.com/diegosouzapw/OmniRoute/pull/12484))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** `omniroute serve` now checks whether the port is already owned before spawning anything, and reports the conflict with the owning PID plus the two ways out (`omniroute stop`, or `--port`). Previously it handed the conflict to the child process, which died with `EADDRINUSE` and was retried twice on the supervisor's restart budget, printing three identical raw Node stack traces without ever saying that another instance held the port. Because that happened after the pid files were written, the doomed second instance also de-registered the healthy running one, leaving `supervisor/.pid` pointing at the dead starter and `server/.pid` deleted. ([#12485](https://github.com/diegosouzapw/OmniRoute/pull/12485))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(devin):** treat Devin CLI model ids as literal — never strip or synthesize effort suffixes ([#12492](https://github.com/diegosouzapw/OmniRoute/pull/12492) — thanks @Neuron-Mr-White)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(command-code):** floor a tiny caller-set `max_tokens` (e.g. `64`) to `MUSE_SPARK_MIN_OUTPUT_TOKENS = 512` for muse-spark ids, detected through the prefix-aware `MUSE_SPARK_PATTERN` so provider-prefixed forms (`meta/muse-spark-1.2-contributor`, `cmd/meta/muse-…`) are covered in both `buildOpenAiBody` (the `/provider/v1/chat/completions` path from #12130) and `buildCommandCodeCliBody` (the `/alpha/generate` fallback) — the hidden server-side reasoning phase can no longer consume the whole output budget and answer HTTP 200 with null content (`out=64, reasoning=61`), mirroring the #11214 mitigation already shipped for opencode-go; a caller that sent no budget is left without one and budgets at or above the floor pass through untouched ([#12497](https://github.com/diegosouzapw/OmniRoute/pull/12497)) — thanks @Stazyu
|
||||
@@ -1 +0,0 @@
|
||||
- Fix `keys regenerate`/`keys reveal` in the CLI to fall back to the dashboard `/api/keys` route when an ID from `keys list` does not exist in the registered-keys store, closing an ID-namespace drift between the two API key families.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** Windows dashboard no longer reports Claude Code as `settings_found_binary_unresolved` when npm-global detection fails inside Electron. A failed `npm config get prefix` is no longer cached as permanent `""` (which deleted every npm-derived candidate for the process lifetime), Windows lookup PATH is enriched with npm-prefix / `%APPDATA%\npm` / nvm / `%ProgramFiles%\nodejs`, and stock Node MSI `.cmd` shims under Program Files remain an explicit safety net. Separate from the #7831 `.ps1` / known-path fix for #7774. ([#12563](https://github.com/diegosouzapw/OmniRoute/issues/12563))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** strip `temperature`/`top_p` on native Codex Responses passthrough so combo `codex-review` traffic no longer 400s with `Unsupported parameter: temperature` ([#12585](https://github.com/diegosouzapw/OmniRoute/pull/12585))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(pricing):** saving model pricing from the dashboard no longer fails with a 400 / `[object Object]` — sync-written pricing fields round-trip through PATCH and validation errors surface actionable details ([#12629](https://github.com/diegosouzapw/OmniRoute/pull/12629)) — thanks @wofiporia
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** Claude-native context handoffs now land in Anthropic's top-level `system` parameter instead of a leading `role: "system"` message, and the final Claude executor dispatch hoists any remaining leading prompt system/developer messages and relocates directive-only `output_config` envelopes away from `messages[0]`, preventing the `messages.0: use the top-level 'system' parameter` HTTP 400 on model switches ([#12668](https://github.com/diegosouzapw/OmniRoute/pull/12668)).
|
||||
@@ -1 +0,0 @@
|
||||
- Honor a model's declared `reasoning_efforts` vocabulary in the reasoning-routing rule gate: a model-scoped or connection-scoped rule forcing `max`/`ultra` is now treated as supported when the model's resolved capabilities list that tier (operator overrides apply to models without a static registry declaration), instead of being rejected by the hardcoded `gpt-5.6-*` regex. Custom OpenAI-compatible providers whose models accept `max` natively (for example Merge Gateway `zai/glm-5.3-flash`, which accepts `low|high|max`) can now use forced-max rules without the request failing with `Reasoning effort 'max' is not supported by the configured target`.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dashboard):** the Modal provider connection form now shows a Base URL field (placeholder `https://<workspace>--<app>.modal.run/v1`), so bring-your-own-deploy Modal connections can be validated and saved instead of failing outright — the server-side validator already required `providerSpecificData.baseUrl` ([#12704](https://github.com/diegosouzapw/OmniRoute/issues/12704))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** route the `dario` and `9router` request bodies through the internal-marker strip before they are serialized upstream — both executors override `transformRequest()` without calling the base implementation, so the internal context-relay / universal-handoff markers (`_omnirouteSkipContextRelay`, `_omnirouteInternalRequest`, `_omnirouteSkipUniversalHandoff`) reached strict OpenAI-compatible gateways and got the call rejected with HTTP 400 "Unsupported parameter(s)" ([#12729](https://github.com/diegosouzapw/OmniRoute/issues/12729))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** OpenAI-compatible model discovery now parses per-vendor-route `effort_values` (nested under `vendors.<vendor>.capabilities.reasoning` in `/v1/models`), intersected across vendor routes so a synced level is always honored on every route the model can land on; re-syncing a connection whose catalog declares this shape no longer silently resets the synced `supportedThinkingEfforts`/`defaultThinkingEffort` data ([#12730](https://github.com/diegosouzapw/OmniRoute/pull/12730))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(codex):** the Codex WebSocket transport now emits a terminal `response.failed` (code `upstream_websocket_closed`) when the upstream socket closes before a terminal response event, instead of ending the client stream as if it had completed normally — preventing silent output truncation and allowing fallback/retry to trigger ([#12737](https://github.com/diegosouzapw/OmniRoute/pull/12737)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(models):** preserve free-model metadata (`isFree`) discovered live from a provider through synced-model normalization, so free models no longer lose that flag before reaching the UI/consumers ([#12763](https://github.com/diegosouzapw/OmniRoute/pull/12763)) — thanks @keeltrace
|
||||
@@ -1 +0,0 @@
|
||||
- `/v1/models` combos whose merged `capabilities.vision` is `true` now also advertise `input_modalities: ["text","image"]` / `output_modalities: ["text"]` (synced modality intersections keep precedence), so models.dev-shaped clients no longer see a vision combo as text-only. (#12799 — thanks @aref-alapour)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** the background cleanup scheduler no longer runs a blocking full `VACUUM` after pruning rows (it froze every route, `/healthz` included, for minutes on large databases — 30 s after every start and every 6 h); freed pages are now reclaimed with paced `PRAGMA incremental_vacuum` batches plus a WAL checkpoint, and on `auto_vacuum=NONE` a full VACUUM is deferred to the Storage page's scheduled window via `vacuumScheduler.requestFullVacuum()` ([#12821](https://github.com/diegosouzapw/OmniRoute/issues/12821))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** a recoverable direct-fetch response-start timeout (`DIRECT_RESPONSE_START_TIMEOUT`) could, in a narrow timer/promise-settlement race, escape as an `unhandledRejection` → `uncaughtException` and kill the server process — even though `proxyFetch` already retries this exact condition on a fresh socket. Guarded the timer callback so it can no longer fire against an already-settled attempt, and extended the process-level crash guard (already used by the WS/API-bridge servers) to recognize and swallow this code if it ever escapes anyway. Also installs that same guard in the production server entrypoint (`dist/server-ws.mjs`), which never had it even though the dev server already did ([#12861](https://github.com/diegosouzapw/OmniRoute/issues/12861)) — thanks @insoln
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** Gemini to Claude usage no longer double-counts the cached prompt prefix — `input_tokens` now excludes `cache_read_input_tokens`, matching the Anthropic Messages semantics ([#12863](https://github.com/diegosouzapw/OmniRoute/pull/12863))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** An Anthropic OAuth `403 "Request not allowed"` no longer bans the Claude connection on the first response — it is a per-request refusal on an otherwise healthy token, so it is now classified as the non-terminal `request_rejected` type, the connection is excluded for a growing cooldown (5 min, then 15 min) and only three consecutive refusals with no success in between escalate to `banned`; previously a single such response flipped the only Claude connection to `banned` and every later request was short-circuited with "All 1 connection(s) banned by upstream" until an operator reconnected ([#12859](https://github.com/diegosouzapw/OmniRoute/issues/12859), [#12864](https://github.com/diegosouzapw/OmniRoute/pull/12864) — thanks @insoln)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** vLLM connections now advertise the real context window: model discovery reads `max_model_len` instead of falling back to the 128K default ([#12897](https://github.com/diegosouzapw/OmniRoute/pull/12897), closes [#12858](https://github.com/diegosouzapw/OmniRoute/issues/12858))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(combos):** A combo's visibility can be changed through the API again: `updateComboSchema` accepts `isHidden`, so a visibility-only update is no longer rejected as empty and a mixed update no longer drops it ([#12898](https://github.com/diegosouzapw/OmniRoute/pull/12898), closes [#12836](https://github.com/diegosouzapw/OmniRoute/issues/12836))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(chatcore):** block a client's own duplicate retry (same idempotency key) from opening a second upstream turn while the first is still in flight, returning `409 turn_in_progress` instead of wasting quota on a redundant execution
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(combo):** A weighted combo whose every target was excluded before dispatch by a resilience timer (model lockout, open circuit breaker, provider cooldown) now answers `503` `all_targets_cooling_down` with `Retry-After` set to the earliest exclusion to lapse, the excluded targets and reasons in `diagnostics.excluded`, a `wait` recovery hint, and a `[COMBO]` warning naming the reasons; previously the pool was dropped silently and the host answered `404 "Combo has no executable targets"` (recovery hint "switch combo / reconnect the missing providers") for a pool that was configured, connected and merely cooling down — which clients such as Claude Code render as "this model may not exist". A pool with nothing to run keeps its `404` ([#12954](https://github.com/diegosouzapw/OmniRoute/issues/12954), [#12956](https://github.com/diegosouzapw/OmniRoute/pull/12956) — thanks @insoln)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** A `5xx` model-lockout failure — a transport error (`terminated`, `EHOSTUNREACH`, connect timeout), an upstream server error, or OmniRoute's own synthesized `502` from quality validation — now locks only the exact provider/connection/model tuple instead of the quota family; previously one empty response on a single `gpt-5.6-*` model removed every `gpt-5*` model of the codex connection from routing for 2–30 min (escalating) while its quota was untouched. Quota statuses (`429`/`403`/`402`) keep the family scope; success-decay and the Model Cooldowns card now handle exact-scope locks too ([#12955](https://github.com/diegosouzapw/OmniRoute/issues/12955), [#12957](https://github.com/diegosouzapw/OmniRoute/pull/12957) — thanks @insoln)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(claude):** forward client-negotiated `thinking-binding-controls-2026-08-01` and `thinking-display-updates-2026-08-18` betas so Fable 5.1 `thinking.block_binding` / `thinking.display` requests are no longer rejected upstream with `Extra inputs are not permitted` ([#12989](https://github.com/diegosouzapw/OmniRoute/pull/12989))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** skip the POSIX CLI path lookup during Windows autostart setup, preventing a bogus path error before successful enablement ([#12993](https://github.com/diegosouzapw/OmniRoute/pull/12993))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(streaming):** allow a per-provider override of the fetch-start (headers-wait) timeout cap so providers that buffer the full generation before the first byte (e.g. `command-code`, `opencode-go`) are not cut off at the global 110s cap; the same two entries also gain a reasoning-safe `requestDefaults.maxTokens` of 16384 so thinking models such as `z-ai/glm-5.3-flash` are not cut off mid-reasoning ([#13002](https://github.com/diegosouzapw/OmniRoute/pull/13002)) — thanks @alvinveroy
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(mitm):** add catch-all (*) model mapping fallback for Agent Bridge ([#13013](https://github.com/diegosouzapw/OmniRoute/pull/13013)) — thanks @tuandinh0801
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** Antigravity connection Retest probes Cloud Code envelope ([#13015](https://github.com/diegosouzapw/OmniRoute/pull/13015)) — thanks @tuandinh0801
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(dev):** allow Ctrl+C to promptly kill dev server by closing active connections ([#13020](https://github.com/diegosouzapw/OmniRoute/pull/13020)) — thanks @tuandinh0801
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** reasoning replay now works for Chat Completions and Anthropic Messages clients on Responses-API reasoning targets such as `opencode-go/deepseek-v4-flash`: plain (non-tool-call) assistant turns are captured against the same normalized transcript the read side digests (the Responses body carries `input`, not `messages`, so the write side digested only the assistant message instead of the full transcript and every replay missed), and the replay pass runs on the OpenAI pivot for every source format, so Anthropic Messages clients are replayed too. Fixes the intermittent `400 The reasoning_text in the thinking mode must be passed back to the API` from Console Go for clients that drop `reasoning_content` ([#13031](https://github.com/diegosouzapw/OmniRoute/pull/13031)) — thanks @jmche
|
||||
@@ -1 +0,0 @@
|
||||
- fix(oauth): kimi-coding/github device-flow `pollToken` no longer rejects with `TypeError: Body is unusable` when the token endpoint returns a non-JSON error page (CDN/anti-bot/proxy interstitial) — the body is now read once and parsed, preserving the graceful `invalid_response` fallback instead of a generic 500 (#13046 — thanks @ysntony)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(quota): keep Kiro active while any _freetrial pool has quota (#13088)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** recognize `tool_choice.type: "custom"` in Responses→Chat translation and propagate custom tool names (including namespace-flattened ones) across both the streaming and non-streaming provider legs, so non-streaming Responses clients get `custom_tool_call`/raw `input` instead of `function_call`/JSON arguments ([#13128](https://github.com/diegosouzapw/OmniRoute/pull/13128)) — thanks @ducphamtien-fonos
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** authenticated `GET /api/db/health` polls no longer run a SQLite `quick_check`. The health dashboard polls every 15 seconds, and that scan ran synchronously on the request-serving event loop, blocking it for the length of the scan. Reference and state checks still run, and explicit repair requests keep integrity checks unless `OMNIROUTE_SKIP_DB_HEALTHCHECK=1` is set ([#13149](https://github.com/diegosouzapw/OmniRoute/pull/13149))
|
||||
@@ -1 +0,0 @@
|
||||
- **resilience:** a Cloudflare managed challenge (`cf-mitigated: challenge` / challenge HTML on 403) is classified as a fingerprint rejection and retried on another account/transport instead of banning the connection (#13161 — thanks @anhtran-ai)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** OpenCode config generator preserves catalog display names — custom names win, then `display_name`/native `name` (with the `owned_by/` prefix stripped once), then a readable label for `auto/*` ids, instead of always showing the raw model id ([#13168](https://github.com/diegosouzapw/OmniRoute/pull/13168)) — thanks @domenicomassafra
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** Fable 5 and 5.1 keep their prompt-cache prefix across mid-conversation system messages. OmniRoute treated only Opus as capable, so every Fable system turn was hoisted into the top-level system prompt and moved the cached prefix, which reported `system_changed` from turn 2 on. Fable now has its own mid-conversation-system capability path, and `context-1m` stays limited to Opus so Fable is never sent an unrelated beta header ([#13173](https://github.com/diegosouzapw/OmniRoute/pull/13173))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(evals):** an eval case whose model call errored is no longer scored as passed — a case that never reached a model has no measured behaviour to grade ([#13201](https://github.com/diegosouzapw/OmniRoute/pull/13201)) — thanks @aaustinhuang
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(evals):** the eval runner now sends `x-omniroute-compression: off` and `x-omniroute-no-memory: true` on every case, so a graded case measures the model instead of the operator's injected output style, retrieved memory and `memory_*` tools ([#13139](https://github.com/diegosouzapw/OmniRoute/issues/13139), [#13206](https://github.com/diegosouzapw/OmniRoute/pull/13206)) — thanks @aaustinhuang
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(vertex):** preserve Claude prompt-cache breakpoints for Vertex and Vertex Partner, use the documented five-minute ephemeral TTL by default, and forward cache usage metadata through streaming responses ([#13220](https://github.com/diegosouzapw/OmniRoute/pull/13220)) — fixes #13219
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(mcp):** load the audit `better-sqlite3` driver via the shared `runtimeRequire()` helper instead of `createRequire(import.meta.url)`, which broke when the Next.js standalone build emits the module as a CommonJS chunk ([#13223](https://github.com/diegosouzapw/OmniRoute/pull/13223)) — thanks @chatchawan-simplewish
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user