mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-21 14:42:20 +03:00
Compare commits
4 Commits
fix/7346-m
...
refactor/e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7d418889d | ||
|
|
0310e49a99 | ||
|
|
9ced57bc08 | ||
|
|
a459dfd221 |
87
.env.example
87
.env.example
@@ -128,11 +128,6 @@ PORT=20128
|
||||
# Optional: set the public origin *with* the same path so OAuth and display URLs
|
||||
# stay consistent without relying on window.location.origin alone:
|
||||
# NEXT_PUBLIC_BASE_URL=https://host/omniroute
|
||||
#
|
||||
# Explicit path probed by the container health check. Unset, the probe derives it
|
||||
# from OMNIROUTE_BASE_PATH; setting it opts back into the deep monitoring endpoint.
|
||||
# Used by: scripts/dev/healthcheck.mjs
|
||||
# OMNIROUTE_HEALTHCHECK_PATH=/api/monitoring/health
|
||||
|
||||
# Opt-in iframe embedding of the OmniRoute HTML pages (issue #10273). Off by default:
|
||||
# every route ships `frame-ancestors 'none'` + `X-Frame-Options: DENY`, which is why the
|
||||
@@ -143,8 +138,6 @@ PORT=20128
|
||||
# (/api, /v1, /v1beta, /a2a, /healthz and the root-level aliases) keeps the strict
|
||||
# headers regardless. Only `vscode` is recognised; `1`/`true` do NOT enable it.
|
||||
# Used by: next.config.mjs via scripts/build/dashboardEmbed.mjs — build-time, rebuild after changing.
|
||||
# Docker: pass it as a build arg (`docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode`);
|
||||
# setting it on an already-built server or image does nothing.
|
||||
# DASHBOARD_ALLOW_EMBED=vscode
|
||||
|
||||
# Split-port mode: serve Dashboard and API on separate ports for network isolation.
|
||||
@@ -229,15 +222,6 @@ PORT=20128
|
||||
# unaffected by this dev-only flag).
|
||||
OMNIROUTE_USE_TURBOPACK=1
|
||||
|
||||
# Disable systemd sd_notify (Type=notify / WatchdogSec=) even when running
|
||||
# under a systemd unit with NOTIFY_SOCKET set.
|
||||
# Used by: scripts/dev/systemd-notify.mjs. Set to 1 to disable.
|
||||
# OMNIROUTE_DISABLE_SD_NOTIFY=1
|
||||
|
||||
# Injected by systemd when running under a service unit (sd_notify protocol).
|
||||
# Read by scripts/dev/systemd-notify.mjs — never set this yourself.
|
||||
# NOTIFY_SOCKET=/run/systemd/notify
|
||||
|
||||
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
|
||||
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
|
||||
# OMNIROUTE_SKIP_DB_HEALTHCHECK=1
|
||||
@@ -273,11 +257,6 @@ OMNIROUTE_USE_TURBOPACK=1
|
||||
# so a missing/corrupt cache never breaks tab-completion.
|
||||
# OMNIROUTE_DEBUG_COMPLETION=1
|
||||
|
||||
# Set to 1 to print per-request timing diagnostics from the CLI quota commands
|
||||
# to stderr (`[omniroute] GET <path> completed in Nms`).
|
||||
# Used by: bin/cli/commands/quota.mjs
|
||||
# OMNIROUTE_DEBUG=1
|
||||
|
||||
# Docker production port mappings (docker-compose.prod.yml only).
|
||||
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
|
||||
# PROD_DASHBOARD_PORT=20130
|
||||
@@ -782,25 +761,11 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_CURSOR_BIN=agent
|
||||
# CLI_CLINE_BIN=cline
|
||||
# CLI_CONTINUE_BIN=cn
|
||||
# CLI_QODER_BIN=qodercli
|
||||
# CLI_QODER_BIN=qoder
|
||||
# CLI_QWEN_BIN=qwen
|
||||
# CLI_AIDER_BIN=aider
|
||||
# CLI_GOOSE_BIN=goose
|
||||
# CLI_GEMINI_BIN=gemini
|
||||
# CLI_KILO_BIN=kilocode
|
||||
# CLI_OPENCODE_BIN=opencode
|
||||
# CLI_HERMES_BIN=hermes
|
||||
# CLI_FORGE_BIN=forge
|
||||
# CLI_JCODE_BIN=jcode
|
||||
# CLI_DEEPSEEK_TUI_BIN=deepseek-tui
|
||||
# CLI_CODEWHALE_BIN=codewhale
|
||||
# CLI_SMELT_BIN=smelt
|
||||
# CLI_PI_BIN=pi
|
||||
# CLI_CRUSH_BIN=crush
|
||||
# CLI_OMP_BIN=omp
|
||||
# CLI_LETTA_BIN=letta
|
||||
# Windsurf has no default binary — set this to enable binary detection for it.
|
||||
# CLI_WINDSURF_BIN=windsurf
|
||||
# CLI_AUGGIE_BIN=auggie
|
||||
# AUGGIE_BIN=auggie
|
||||
|
||||
@@ -891,7 +856,7 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# MODEL_SYNC_INTERVAL_HOURS=24
|
||||
|
||||
# Provider limits sync interval in minutes (rate limit windows, quotas).
|
||||
# Used by: src/lib/usage/providerLimits.ts — polls provider health endpoints.
|
||||
# Used by: src/server-init.ts — polls provider health endpoints.
|
||||
# Default: 70
|
||||
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
|
||||
|
||||
@@ -1042,10 +1007,6 @@ PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
||||
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
|
||||
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
|
||||
|
||||
# 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
|
||||
|
||||
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
|
||||
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
|
||||
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
|
||||
@@ -1361,14 +1322,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body
|
||||
# FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s)
|
||||
# FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s)
|
||||
# OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS=30000 # Bounded response-start window per direct
|
||||
# # (no-proxy) attempt (#10214). A silently-dropped
|
||||
# # pooled keep-alive socket surfaces no transport
|
||||
# # error, so without this bound a direct request can
|
||||
# # stall until undici's headersTimeout (600s) or the
|
||||
# # caller's deadline; on expiry the request retries
|
||||
# # once on a fresh no-keep-alive socket. 0 disables
|
||||
# # the bound (default: 30000 = 30s).
|
||||
|
||||
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
|
||||
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
|
||||
@@ -1423,14 +1376,6 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
|
||||
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
|
||||
|
||||
# ── Perplexity web: built-in-search hint ──
|
||||
# Used by: open-sse/executors/perplexity-web/protocol.ts — appends "You have
|
||||
# built-in web search. Answer questions directly using search results." to the
|
||||
# caller's system message. Off by default: Perplexity's answer engine searches
|
||||
# anyway, and for coding clients the sentence leaks into replies as
|
||||
# meta-commentary. Set to 1/true/yes/on to restore the old behavior.
|
||||
# OMNIROUTE_PPLX_SEARCH_HINT=0
|
||||
|
||||
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
|
||||
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
|
||||
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
|
||||
@@ -2004,16 +1949,6 @@ APP_LOG_TO_FILE=true
|
||||
# Reachability probe target for the scheduler and the auto-test endpoint.
|
||||
# Point it at an internal/self-hosted URL to avoid the public default.
|
||||
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
|
||||
# Probes started at once per batch, for the scheduler and the auto-test endpoint.
|
||||
# Floored at 1 and capped at 50. Default: 10.
|
||||
# PROXY_HEALTH_TEST_CONCURRENCY=10
|
||||
# Delay in ms between two probe departures inside a batch. Without it the whole batch
|
||||
# leaves at once and a shared egress IP can trip a rate-limited target. 0 disables the
|
||||
# spacing; capped at 5000. Default: 100.
|
||||
# PROXY_HEALTH_TEST_STAGGER_MS=100
|
||||
# Set "false" to stop probing the real host of a proxy's assigned provider (GET /models,
|
||||
# no API key) and always use the generic target above instead. Default: enabled.
|
||||
# PROXY_HEALTH_USE_PROVIDER_TARGET=true
|
||||
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
|
||||
# PROXY_AUTO_REMOVE=false
|
||||
# Consecutive failures before an auto-remove fires. Default: 3.
|
||||
@@ -2882,14 +2817,18 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# Minimum spacing between submissions and the extra pause after every third success.
|
||||
# ADOBE_FIREFLY_MIN_SUBMIT_GAP_MS=12000
|
||||
# ADOBE_FIREFLY_BATCH_EXTRA_GAP_MS=15000
|
||||
# Browser used by Adobe Firefly renewal. True headless is debug-only: Adobe
|
||||
# colligo normally rejects risk tokens minted without a headed browser.
|
||||
# Used by: open-sse/services/adobeFireflyBrowserLogin.ts
|
||||
# Chrome CDP runtime used by Adobe Firefly renewal. True headless is debug-only:
|
||||
# Adobe colligo normally rejects risk tokens minted without a headed browser.
|
||||
# ADOBE_FIREFLY_CHROME_CDP_PORT=9334
|
||||
# ADOBE_FIREFLY_CHROME_VISIBLE=0
|
||||
# ADOBE_FIREFLY_CHROME_HEADED=0 # Legacy alias for ADOBE_FIREFLY_CHROME_VISIBLE=1
|
||||
# ADOBE_FIREFLY_CHROME_HEADLESS=0
|
||||
# The CDP-attached Chrome runtime (adobeFireflyChromeRuntime.ts) was removed in
|
||||
# #9255 along with its knobs — ADOBE_FIREFLY_CHROME_CDP_PORT, _VISIBLE, _HEADED,
|
||||
# _PING, _FORCE_RESTART, ADOBE_FIREFLY_LOGIN_WAIT_MS and _FORTER_WAIT_MS are read
|
||||
# nowhere and have no effect.
|
||||
# ADOBE_FIREFLY_CHROME_FORCE_RESTART=0
|
||||
# ADOBE_FIREFLY_CHROME_PING=auto
|
||||
# ADOBE_FIREFLY_LOGIN_WAIT_MS=0
|
||||
# ADOBE_FIREFLY_FORTER_WAIT_MS=45000
|
||||
# Optional absolute Chrome executable; auto-detected when unset.
|
||||
# CHROME_PATH=
|
||||
|
||||
# Telegram Mini App bridge. The update endpoint remains disabled while the bot
|
||||
# token is unset. Used by: src/lib/telegram/* and src/app/api/telegram/update/route.ts.
|
||||
|
||||
64
.github/workflows/radar-export.yml
vendored
64
.github/workflows/radar-export.yml
vendored
@@ -1,64 +0,0 @@
|
||||
# Publica o export estável do catálogo consumido pelo OmniRoute Radar numa URL
|
||||
# fixa (asset de release `radar-export-latest`), para o servidor privado do Radar
|
||||
# (1 GB RAM, nunca clona/builda o OmniRoute) baixá-lo via `RADAR_EXPORT_URL` em
|
||||
# vez de depender do snapshot gravado no deploy. Fonte: scripts/release/radar-export.mjs.
|
||||
#
|
||||
# A URL estável resultante (definir em RADAR_EXPORT_URL no .env do radar-server):
|
||||
# https://github.com/diegosouzapw/OmniRoute/releases/download/radar-export-latest/export-omniroute.json
|
||||
name: Radar Export
|
||||
|
||||
on:
|
||||
workflow_dispatch: # o operador pode publicar sob demanda (de qualquer ref)
|
||||
push:
|
||||
branches: [main] # produção: só o catálogo do main clobra o asset estável
|
||||
paths:
|
||||
- open-sse/config/freeModelCatalog.data.ts
|
||||
- open-sse/config/freeModelCatalog.ts
|
||||
- open-sse/config/providerRegistry.ts
|
||||
- open-sse/config/providers/**
|
||||
- scripts/release/radar-export.mjs
|
||||
- .github/workflows/radar-export.yml
|
||||
schedule:
|
||||
- cron: "17 6 * * 1" # semanal (segunda 06:17 UTC): mantém geradoEm/proveniência frescos
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: radar-export-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
CI_NODE_VERSION: "24"
|
||||
|
||||
jobs:
|
||||
publish-export:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # gh release upload — clobra o asset estável do export
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false # publish usa GH_TOKEN via gh release, não a credencial do checkout
|
||||
- uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- name: Generate catalog export with provenance
|
||||
run: node --import tsx/esm scripts/release/radar-export.mjs "$RUNNER_TEMP/export-omniroute.json"
|
||||
- name: Publish to the stable release asset
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG="radar-export-latest"
|
||||
# Cria o release estável na primeira vez; nas seguintes só re-anexa o asset.
|
||||
if ! gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1; then
|
||||
gh release create "$TAG" \
|
||||
--repo "$GITHUB_REPOSITORY" \
|
||||
--title "Radar catalog export (rolling)" \
|
||||
--notes "Export estável do catálogo OmniRoute para o Radar. Atualizado automaticamente; NÃO é um release de versão do produto." \
|
||||
--latest=false
|
||||
fi
|
||||
gh release upload "$TAG" "$RUNNER_TEMP/export-omniroute.json" --repo "$GITHUB_REPOSITORY" --clobber
|
||||
@@ -1294,15 +1294,10 @@ export function mapRawModelToModelV2(
|
||||
// `(providerID, modelID)`. If the raw id is already provider-prefixed
|
||||
// (e.g. `cc/claude-opus-4-7` from the `cc` Claude Code alias, or
|
||||
// `nvidia/llama-3-70b` from a provider that ships prefixed ids), leave
|
||||
// it as-is — double-prefixing breaks OC's lookup. Bare **combo** ids
|
||||
// (`owned_by: "combo"`, e.g. `gpt-5.6-sol`) must also stay unprefixed:
|
||||
// OpenCode looks up `-m <plugin>/<combo>` as model id `<combo>` under
|
||||
// the plugin provider (#10345). Other bare ids still prefix with
|
||||
// `providerId` so credentials resolve as `(omniroute, model)`.
|
||||
id:
|
||||
raw.id.includes("/") || raw.owned_by === "combo"
|
||||
? raw.id
|
||||
: `${ctx.providerId}/${raw.id}`,
|
||||
// it as-is — double-prefixing breaks OC's lookup. Otherwise prefix with
|
||||
// the resolved `providerId` so a bare key like `claude-opus-4` parses as
|
||||
// `(omniroute, claude-opus-4)` and the credentials resolve correctly.
|
||||
id: raw.id.includes("/") ? raw.id : `${ctx.providerId}/${raw.id}`,
|
||||
/**
|
||||
* Display name. Falls back to raw.id when no enrichment is available;
|
||||
* the caller (`createOmniRouteProviderHook`) overlays
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { mapRawModelToModelV2 } from "../src/index.ts";
|
||||
|
||||
test("mapRawModelToModelV2: bare combo ids stay unprefixed (#10345)", () => {
|
||||
const combo = mapRawModelToModelV2(
|
||||
{
|
||||
id: "gpt-5.6-sol",
|
||||
owned_by: "combo",
|
||||
context_length: 272000,
|
||||
max_output_tokens: 8192,
|
||||
},
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
|
||||
);
|
||||
assert.equal(combo.id, "gpt-5.6-sol");
|
||||
assert.equal(combo.providerID, "omniroute");
|
||||
|
||||
const slashed = mapRawModelToModelV2(
|
||||
{
|
||||
id: "cx/gpt-5.6-sol",
|
||||
owned_by: "combo",
|
||||
context_length: 272000,
|
||||
},
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
|
||||
);
|
||||
assert.equal(slashed.id, "cx/gpt-5.6-sol");
|
||||
|
||||
const ordinary = mapRawModelToModelV2(
|
||||
{ id: "claude-primary", context_length: 200000 },
|
||||
{ providerId: "omniroute", baseURL: "https://or.example.com/v1" }
|
||||
);
|
||||
assert.equal(ordinary.id, "omniroute/claude-primary");
|
||||
});
|
||||
@@ -46,7 +46,7 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 343 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 340 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -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 (154 migrations) |
|
||||
| Database | `src/lib/db/` | SQLite domain modules (153 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 109 tools (44 canonical + memory/skill/GitHub/pool/gamification/plugin/Notion/Obsidian/local-corpus/RTK modules), 3 transports (stdio / SSE / Streamable HTTP), 33 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
|
||||
@@ -167,7 +167,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **security(search)**: block SSRF via `/v1/search` `provider_options.baseUrl` for the Firecrawl search provider — the client-controlled override is now validated as a public URL before it is used to build the server-side fetch target, so a caller with a valid API key can no longer redirect search requests at loopback, RFC1918, or cloud-metadata hosts — thanks @zmf963
|
||||
- **providers**: honor `PATCH /api/providers/[id]` so `omniroute providers rotate` stops 405ing (the OpenAPI spec and CLI already use PATCH) (PR #10366)
|
||||
- **executors**: fix internal timeout misclassified as client disconnect (499) for 7 niche executors — pass TimeoutError reason to controller.abort() (#8197 side-finding)
|
||||
- test(combo): guard auto/best-free never leaks the combo name as a model (#7754)
|
||||
|
||||
15
CLAUDE.md
15
CLAUDE.md
@@ -47,21 +47,6 @@ rewrite it to the `_tasks/…` equivalent before writing:
|
||||
|
||||
Commit those artifacts inside the `_tasks/` repo (`git -C _tasks …`), never in the main repo.
|
||||
|
||||
## Scratch / temporary files — use `_artifacts/`, not `/tmp`
|
||||
|
||||
This project overrides the harness's default session scratchpad (`/tmp/claude-*/…`). Write
|
||||
temporary/working files — exports, generated zips, one-off intermediate outputs, anything you'd
|
||||
otherwise put in `/tmp` — to `/home/diegosouzapw/dev/proxys/OmniRoute/_artifacts/` instead.
|
||||
|
||||
- `_artifacts/` is a root `_*` path: already gitignored (`AGENTS.md` → "Root `_*` paths"), lives
|
||||
on disk only, never tracked.
|
||||
- Reason: keeping scratch output inside the project (vs `/tmp`) makes it trivial for the operator
|
||||
to find and delete everything temporary in one place, instead of hunting across ephemeral
|
||||
session-specific `/tmp` directories that vanish or accumulate untracked.
|
||||
- Do **not** confuse this with `_tasks/` (Hard Rule #23, its own private git repo for durable
|
||||
plans/specs/research/hand-offs) — `_artifacts/` is for disposable working files only, nothing
|
||||
here needs to survive or be versioned.
|
||||
|
||||
## Base-green before opening PRs
|
||||
|
||||
Before cutting a branch or opening a PR, run the base-green check (`AGENTS.md` → Git Workflow →
|
||||
|
||||
12
Dockerfile
12
Dockerfile
@@ -140,18 +140,6 @@ ENV OMNIROUTE_USE_TURBOPACK="${OMNIROUTE_USE_TURBOPACK}"
|
||||
ARG OMNIROUTE_BASE_PATH=""
|
||||
ENV OMNIROUTE_BASE_PATH=$OMNIROUTE_BASE_PATH
|
||||
|
||||
# #10273: the dashboard's `frame-ancestors` policy is compiled into the route
|
||||
# manifest by next.config.mjs (via scripts/build/dashboardEmbed.mjs), so it is
|
||||
# fixed when the image is built and cannot be flipped with `-e` on a running
|
||||
# container. Build with `--build-arg DASHBOARD_ALLOW_EMBED=vscode` to produce an
|
||||
# image whose HTML pages may be framed by the VS Code Simple Browser
|
||||
# (OmniCopilot's `dashboardOpen: "editor"`). Unset — the default — keeps every
|
||||
# route on `frame-ancestors 'none'` + X-Frame-Options: DENY. Builder-stage only:
|
||||
# the runner stage deliberately does not carry it, because a runtime value would
|
||||
# suggest an effect it cannot have.
|
||||
ARG DASHBOARD_ALLOW_EMBED=""
|
||||
ENV DASHBOARD_ALLOW_EMBED=$DASHBOARD_ALLOW_EMBED
|
||||
|
||||
# Docker containers cannot run the MITM/Agent-Bridge stack (no host DNS/cert
|
||||
# access), so keep @/mitm/manager on the graceful stub (#3390). This flag is
|
||||
# Docker-only: npm/Electron/VPS builds must bundle the REAL manager (#6344).
|
||||
|
||||
24
README.md
24
README.md
@@ -7,7 +7,7 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 343 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 343 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
<img src="./docs/diagrams/readme-hero.svg" width="100%" alt="OmniRoute — Never stop coding. Every AI tool → 340 providers — 90+ free — through one endpoint. Claude Code, Codex, Cursor, Cline, Copilot & Antigravity into FREE Claude / GPT / Gemini with auto-fallback. RTK + Caveman stacked compression saves 15–95% tokens (~89% avg) — never hit limits. 340 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
|
||||
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
|
||||
| ------------------------- | :-----: | :---------: | :---------: |
|
||||
| 🌐 Providers | 290 | **342** | more queued |
|
||||
| 🌐 Providers | 290 | **340** | more queued |
|
||||
| 🧠 Documented models | 1185 | **1202** | — |
|
||||
| 🖼️ Modality Bridge | — | 🆕 vision | 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="#-343-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-340-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-full-cli--a2a--mcp">🔌 CLI & MCP</a></td>
|
||||
</tr>
|
||||
<tr>
|
||||
@@ -210,7 +210,7 @@ curl http://localhost:20128/v1/chat/completions \
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 343 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 343 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
<img src="./docs/diagrams/promise-pillars.svg" width="100%" alt="The Promise — One endpoint. 340 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 340 providers in milliseconds, zero downtime) · Save up to 95% tokens (RTK + Caveman stacked compression cuts 15–95%, ~89% avg on tool-heavy sessions) · $0 to start (90+ free tiers, 56 free forever — no card needed) · Every tool works (33 coding agents through one config) · One endpoint (OpenAI ↔ Claude ↔ Gemini ↔ Responses API at /v1) · Production-grade (circuit breakers, TLS stealth, MCP 109 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -461,7 +461,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
</div>
|
||||
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 343 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
<img src="./docs/diagrams/comparison-table.svg" width="100%" alt="What sets OmniRoute apart — comparison table vs 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute: 340 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 109 tools, A2A agent protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, Desktop/Termux/PWA, 43 i18n UI locales, 100% MIT self-hosted. OmniRoute is the only one with the full set; competitors show a mix of checks, partials and crosses. Verified from each project's docs."/>
|
||||
|
||||
<sub>📊 Full methodology & per-feature detail vs 9router, OpenRouter, CLIProxyAPI & LiteLLM → [`docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md`](docs/comparison/OMNIROUTE_VS_ALTERNATIVES.md)</sub>
|
||||
|
||||
@@ -557,9 +557,9 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🧠 Memory you control** — off by default, opt-in int8 vector quantization + typed decay, per-request `x-omniroute-no-memory`. → [Memory](docs/frameworks/MEMORY.md)
|
||||
- **🛡️ Security** — prompt-injection guard on every LLM route (red-team suite), opt-in credential-masking guardrail (redacts leaked API keys/secrets in both directions), free DuckDuckGo last-resort web search, and an optional OIDC login gate for the dashboard (password login always stays available). → [Guardrails](docs/security/GUARDRAILS.md)
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, Freepik, Adobe Firefly, Microsoft Designer, Google Imagen, Segmind, EdgeTTS. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🌍 Deployment & ops** — reverse-proxy `basePath`, browser-language auto-detect, per-key device tracking, root-less MITM trust, zh-TW localization. → [Environment](docs/reference/ENVIRONMENT.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **343-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **🤝 More providers & agents** — Cursor Cloud Agent, Grok Build (xAI) with browser + OAuth login, Ollama first-class card, Claude Opus 5 & Sonnet 5, Kimi official partnership (Code/Web/Moonshot), Zed, Requesty, SenseNova, Yuanbao, Agnes AI… and a refreshed **340-provider catalog**. → [Providers](docs/reference/PROVIDER_REFERENCE.md)
|
||||
- **📡 Routing transparency** — every response carries an `X-OmniRoute-Decision` header naming the strategy/provider/latency that served it, a new `cache-optimized` combo strategy + Auto-Combo `cacheAffinity` factor route repeat requests back to the connection holding the cached prefix, and a read-only `/v1/auto-combo/{channel}/candidates` endpoint exposes an `auto/*` channel's live candidate pool. → [Auto-Combo](docs/routing/AUTO-COMBO.md)
|
||||
- **⚡ Local performance & infra** — one-click local Redis, Cloudflare Workers / Deno Deploy relay deployers, Bifrost & Mux as supervised embedded services. → [Embedded Services](docs/frameworks/EMBEDDED-SERVICES.md)
|
||||
|
||||
@@ -642,11 +642,11 @@ of your shell history. → [CLI Integrations](docs/guides/CLI-INTEGRATIONS.md)
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 343 AI Providers — 90+ Free
|
||||
## 🌐 340 AI Providers — 90+ Free
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **343 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
> The most complete catalog of any open-source router: **340 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -877,7 +877,7 @@ Engines run in pipeline order; each is independently toggleable and configurable
|
||||
<tr><td align="center" nowrap>9</td><td align="left" nowrap><b>Aggressive</b></td><td align="left">Summarization + progressive aging of old turns</td></tr>
|
||||
<tr><td align="center" nowrap>10</td><td align="left" nowrap><b>LLMLingua-2</b></td><td align="left">ML semantic pruning via MobileBERT ONNX — code-safe, async</td></tr>
|
||||
<tr><td align="center" nowrap>11</td><td align="left" nowrap><b>Ultra</b></td><td align="left">Heuristic token pruning with an optional small-model (SLM) tier</td></tr>
|
||||
<tr><td align="center" nowrap>12</td><td align="left" nowrap><b>OmniGlyph</b></td><td align="left">Experimental context-as-image encoding for measured Claude Fable 5 on the direct Anthropic wire; GPT 5.6 transformers remain fail-closed pending provider receipts. Four compression profiles (aggressive default, balanced, coding-safe, passthrough) (most aggressive; opt-in)</td></tr>
|
||||
<tr><td align="center" nowrap>12</td><td align="left" nowrap><b>OmniGlyph</b></td><td align="left">Experimental context-as-image encoding routed to Claude Fable 5 (most aggressive; opt-in)</td></tr>
|
||||
</table>
|
||||
|
||||
Code blocks, URLs and structured data are **always preserved** byte-perfect. **One-click presets** combine the engines:
|
||||
@@ -988,8 +988,6 @@ docker run -d --name omniroute --restart unless-stopped --stop-timeout 40 \
|
||||
-p 127.0.0.1:20128:20128 -v omniroute-data:/app/data diegosouzapw/omniroute:latest
|
||||
```
|
||||
|
||||
`:latest` follows the highest **published** stable SemVer. It does not track git `main`. Pin `:X.Y.Z` for GitOps. See [Docker Release Channels](docs/guides/DOCKER_GUIDE.md#release-channels).
|
||||
|
||||
> **Pre-release Docker channel:** `diegosouzapw/omniroute:next` and
|
||||
> `diegosouzapw/omniroute:next-web` follow the current default `release/v*`
|
||||
> branch. These mutable tags are intended only for testing unreleased fixes and
|
||||
@@ -1174,7 +1172,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
|
||||
<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) — 120 domain modules, 154 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 120 domain modules, 153 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>
|
||||
|
||||
@@ -52,19 +52,6 @@ function resolveUrl(path, opts) {
|
||||
return `${getBaseUrl(opts)}${path.startsWith("/") ? path : `/${path}`}`;
|
||||
}
|
||||
|
||||
/** The machine-derived token is valid only for the local loopback server. */
|
||||
export function isLoopbackUrl(value) {
|
||||
try {
|
||||
const hostname = new URL(value).hostname.replace(/^\[|\]$/g, "").toLowerCase();
|
||||
if (hostname === "localhost" || hostname === "::1") return true;
|
||||
if (/^127(?:\.[0-9]{1,3}){3}$/.test(hostname)) return true;
|
||||
if (/^::ffff:(?:127\.|7f[0-9a-f]{2}:)/i.test(hostname)) return true;
|
||||
return false;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export async function buildHeaders(opts) {
|
||||
const headers = new Headers(opts.headers || {});
|
||||
if (!headers.has("accept")) headers.set("accept", "application/json");
|
||||
@@ -100,17 +87,10 @@ export async function buildHeaders(opts) {
|
||||
if (auth && !headers.has("authorization")) {
|
||||
headers.set("authorization", `Bearer ${auth}`);
|
||||
}
|
||||
// Inject the machine-derived credential only for an explicit local loopback
|
||||
// destination. Remote contexts and absolute remote URLs use scoped access
|
||||
// tokens and must never receive this machine-bound local credential.
|
||||
const destinationUrl = opts.destinationUrl ?? getBaseUrl(opts);
|
||||
if (!isLoopbackUrl(destinationUrl)) {
|
||||
headers.delete(CLI_TOKEN_HEADER);
|
||||
} else {
|
||||
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
|
||||
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
|
||||
headers.set(CLI_TOKEN_HEADER, cliToken);
|
||||
}
|
||||
// Inject machine-id derived CLI token; env var override for testing.
|
||||
const cliToken = opts.cliToken ?? process.env.OMNIROUTE_CLI_TOKEN ?? (await getCliToken());
|
||||
if (cliToken && !headers.has(CLI_TOKEN_HEADER)) {
|
||||
headers.set(CLI_TOKEN_HEADER, cliToken);
|
||||
}
|
||||
if (opts.idempotencyKey && !headers.has("idempotency-key")) {
|
||||
headers.set("idempotency-key", opts.idempotencyKey);
|
||||
@@ -215,12 +195,8 @@ function fetchOnce(url, init, timeoutMs) {
|
||||
export async function apiFetch(path, opts = {}) {
|
||||
const method = String(opts.method || "GET").toUpperCase();
|
||||
const url = resolveUrl(path, opts);
|
||||
const headers = await buildHeaders({ ...opts, destinationUrl: url });
|
||||
const headers = await buildHeaders(opts);
|
||||
const body = serializeBody(opts.body, headers);
|
||||
// Undici preserves custom headers across cross-origin redirects. A local server
|
||||
// redirect must never turn the loopback machine credential into an outbound
|
||||
// secret, so fail redirects whenever this header is present.
|
||||
const redirect = headers.has(CLI_TOKEN_HEADER) ? "error" : opts.redirect;
|
||||
const timeout =
|
||||
opts.timeout ?? (Number.parseInt(process.env.OMNIROUTE_HTTP_TIMEOUT_MS || "", 10) || 30000);
|
||||
const maxAttempts = opts.retry === false ? 1 : (opts.retryMax ?? RETRY_DEFAULTS.maxAttempts);
|
||||
@@ -229,7 +205,7 @@ export async function apiFetch(path, opts = {}) {
|
||||
let lastErr;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
const res = await fetchOnce(url, { method, headers, body, redirect }, timeout);
|
||||
const res = await fetchOnce(url, { method, headers, body }, timeout);
|
||||
if (res.ok) return enrichResponse(res, opts);
|
||||
if (attempt < maxAttempts && shouldRetryStatus(res.status, method, opts)) {
|
||||
const delay = computeBackoff(attempt, res.headers.get("retry-after"));
|
||||
|
||||
@@ -4,9 +4,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createDecipheriv, scryptSync } from "node:crypto";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { isLoopbackUrl } from "../api.mjs";
|
||||
import { resolveDataDir, resolveStoragePath } from "../data-dir.mjs";
|
||||
import { getCliToken, CLI_TOKEN_HEADER } from "../utils/cliToken.mjs";
|
||||
import { printHeading } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { readDatabaseHealth, readEncryptedCredentialSamples } from "../sqlite.mjs";
|
||||
@@ -380,11 +378,11 @@ function checkMemory() {
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchWithTimeout(url, options = {}) {
|
||||
async function fetchWithTimeout(url) {
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), CHECK_TIMEOUT_MS);
|
||||
try {
|
||||
return await fetch(url, { ...options, signal: controller.signal });
|
||||
return await fetch(url, { signal: controller.signal });
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
@@ -473,98 +471,6 @@ async function checkServerLiveness(options = {}) {
|
||||
);
|
||||
}
|
||||
|
||||
export async function checkMachineTokenAuth(options = {}) {
|
||||
if (process.env.OMNIROUTE_DISABLE_CLI_TOKEN === "true") {
|
||||
return warn("CLI machine token", "CLI machine-token authentication is disabled", {
|
||||
derived: false,
|
||||
accepted: false,
|
||||
disabled: true,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
const parsed = new URL(resolveLivenessUrl(options));
|
||||
if (
|
||||
!["http:", "https:"].includes(parsed.protocol) ||
|
||||
parsed.username ||
|
||||
parsed.password ||
|
||||
!isLoopbackUrl(parsed.toString())
|
||||
) {
|
||||
return warn(
|
||||
"CLI machine token",
|
||||
"Machine-token probes are limited to HTTP(S) loopback endpoints",
|
||||
{ derived: false, accepted: false, tokenExposed: false }
|
||||
);
|
||||
}
|
||||
parsed.pathname = "/api/cli/whoami";
|
||||
parsed.search = "";
|
||||
parsed.hash = "";
|
||||
url = parsed.toString();
|
||||
} catch {
|
||||
return warn("CLI machine token", "Could not resolve the management endpoint", {
|
||||
derived: false,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
|
||||
const token = await getCliToken();
|
||||
if (!token) {
|
||||
return fail(
|
||||
"CLI machine token",
|
||||
"Could not derive a machine token; verify the node-machine-id runtime is installed",
|
||||
{ derived: false, accepted: false, tokenExposed: false }
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetchWithTimeout(url, {
|
||||
headers: { [CLI_TOKEN_HEADER]: token },
|
||||
redirect: "error",
|
||||
});
|
||||
if (response.ok) {
|
||||
return ok("CLI machine token", "Server accepted the local machine token", {
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: true,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return warn(
|
||||
"CLI machine token",
|
||||
"Server rejected the local machine token; if the CLI and server are on different hosts or container boundaries, run `omniroute connect <host> --key <oma_live_...>`",
|
||||
{
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
containerBoundaryLikely: true,
|
||||
tokenExposed: false,
|
||||
}
|
||||
);
|
||||
}
|
||||
return warn("CLI machine token", `Machine-token probe returned HTTP ${response.status}`, {
|
||||
url,
|
||||
status: response.status,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
} catch {
|
||||
return warn("CLI machine token", "Machine-token endpoint could not be reached", {
|
||||
url,
|
||||
status: 0,
|
||||
derived: true,
|
||||
accepted: false,
|
||||
tokenExposed: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
const rootDir =
|
||||
context.rootDir || path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "..");
|
||||
@@ -582,7 +488,6 @@ export async function collectDoctorChecks(context = {}, options = {}) {
|
||||
|
||||
if (!options.skipLiveness) {
|
||||
checks.push(await checkServerLiveness(options));
|
||||
checks.push(await checkMachineTokenAuth(options));
|
||||
}
|
||||
|
||||
// CLI tool health checks
|
||||
|
||||
@@ -118,11 +118,6 @@ export function npmInstallRuntime(pkgs, opts = {}) {
|
||||
// install of a sibling runtime dep (e.g. systray2 from trayRuntime.ts, which writes to the
|
||||
// same runtime dir) does not prune this package as "extraneous" — that pruning otherwise
|
||||
// reproduces "No SQLite driver available" after a tray install removes better-sqlite3.
|
||||
// npm 12+ defaults `allowScripts` to off, silently skipping lifecycle/install
|
||||
// scripts (e.g. better-sqlite3's node-gyp/prebuild-install rebuild) unless the
|
||||
// package has a matching `allowScripts` entry — and still exits 0, masking the
|
||||
// failure (#10713). The runtime dir is a CLI-owned, non-user package.json, so
|
||||
// explicitly allowing scripts for the packages we are installing here is safe.
|
||||
const npmArgs = [
|
||||
"install",
|
||||
...pkgs,
|
||||
@@ -130,7 +125,6 @@ export function npmInstallRuntime(pkgs, opts = {}) {
|
||||
"--no-fund",
|
||||
"--prefer-online",
|
||||
"--save-exact",
|
||||
...pkgs.map((pkg) => `--allow-scripts=${pkg}`),
|
||||
];
|
||||
// On Windows .cmd files cannot be executed without a shell; use cmd.exe /c explicitly
|
||||
// so we never set shell:true (which would propagate env and enable injection).
|
||||
|
||||
@@ -121,16 +121,7 @@ function writeLinuxSystemdUnit(cliPath) {
|
||||
"Wants=network-online.target",
|
||||
"",
|
||||
"[Service]",
|
||||
// Type=notify + WatchdogSec: the server sends READY=1 once listening and
|
||||
// WATCHDOG=1 every 60s; if its event loop ever blocks (frozen process),
|
||||
// the pings stop and systemd kills+restarts the service. NotifyAccess=all
|
||||
// because the pings come from the server child, not the serve supervisor.
|
||||
// Foreground serve only: `--daemon` escapes the cgroup and would break
|
||||
// the notify handshake.
|
||||
"Type=notify",
|
||||
"NotifyAccess=all",
|
||||
"WatchdogSec=180",
|
||||
"TimeoutStartSec=300",
|
||||
"Type=simple",
|
||||
`ExecStart=${buildServeExecLine(cliPath, { tray: false })}`,
|
||||
"Restart=on-failure",
|
||||
"RestartSec=5",
|
||||
|
||||
@@ -12,39 +12,25 @@ function getActiveSalt() {
|
||||
return process.env.OMNIROUTE_CLI_SALT || BUILTIN_DEFAULT_SALT;
|
||||
}
|
||||
|
||||
export function deriveCliToken(machineIdModule, salt) {
|
||||
export async function getCliToken() {
|
||||
const salt = getActiveSalt();
|
||||
if (_cached !== null && _cachedSalt === salt) return _cached;
|
||||
try {
|
||||
// node-machine-id is CommonJS: under `await import()` its exports land on
|
||||
// `.default`, so destructuring `machineIdSync` off the namespace yields
|
||||
// undefined and calling it throws — which the catch below turned into an
|
||||
// empty token, silently disabling CLI auth for every management request.
|
||||
// Same resolution order as src/lib/machineToken.ts.
|
||||
const machineIdSync =
|
||||
machineIdModule?.machineIdSync || machineIdModule?.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") return "";
|
||||
const mod = await import("node-machine-id");
|
||||
const machineIdSync = mod.machineIdSync ?? mod.default?.machineIdSync;
|
||||
if (typeof machineIdSync !== "function") throw new Error("machine-id API unavailable");
|
||||
// machineIdSync(true) returns the original unhashed hardware ID — mirrors
|
||||
// getMachineTokenSync() in src/lib/machineToken.ts (#10148 cliToken hardening).
|
||||
const rawId = machineIdSync(true);
|
||||
if (!rawId) return "";
|
||||
return crypto.createHmac("sha256", rawId).update(salt).digest("hex");
|
||||
} catch {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCliToken() {
|
||||
const salt = getActiveSalt();
|
||||
if (_cached !== null && _cachedSalt === salt) return _cached;
|
||||
try {
|
||||
const imported = await import("node-machine-id");
|
||||
const token = deriveCliToken(imported, salt);
|
||||
if (!token) {
|
||||
// Swallowing here changes control flow (every management call goes out
|
||||
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
|
||||
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled");
|
||||
}
|
||||
_cached = token;
|
||||
const mid = machineIdSync(true);
|
||||
_cached = crypto.createHmac("sha256", mid).update(salt).digest("hex");
|
||||
} catch (e) {
|
||||
// Swallowing here changes control flow (every management call goes out
|
||||
// unauthenticated and 401s), so leave a breadcrumb rather than failing mute.
|
||||
console.debug("[CLI_TOKEN] machine-id resolution failed, CLI auth disabled:", e);
|
||||
_cached = "";
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ export async function waitForServer(port, timeout = 60000) {
|
||||
// - "not-listening": nothing is accepting connections on the port at all.
|
||||
async function pollHealthOnce(port) {
|
||||
try {
|
||||
const res = await fetch(`http://127.0.0.1:${port}/api/monitoring/health`, {
|
||||
const res = await fetch(`http://localhost:${port}/api/monitoring/health`, {
|
||||
signal: AbortSignal.timeout(2000),
|
||||
});
|
||||
return res.ok ? "ready" : "fast-reject";
|
||||
|
||||
@@ -119,9 +119,6 @@ function loadEnvFile() {
|
||||
addEnvPath(join(ROOT, ".env"));
|
||||
}
|
||||
|
||||
const keyOrigin = new Map();
|
||||
const shadowed = new Map();
|
||||
|
||||
for (const envPath of envPaths) {
|
||||
try {
|
||||
if (existsSync(envPath)) {
|
||||
@@ -134,31 +131,19 @@ function loadEnvFile() {
|
||||
const key = trimmed.slice(0, eqIdx).trim();
|
||||
if (process.env[key] === undefined) {
|
||||
process.env[key] = parseEnvValue(trimmed.slice(eqIdx + 1));
|
||||
keyOrigin.set(key, envPath);
|
||||
} else if (!shadowed.has(key)) {
|
||||
// The line is inert: something set this key first. Report it once
|
||||
// per key, whether the winner was an earlier file or the process
|
||||
// environment (#6194: a shell's own HOSTNAME beat the .env and the
|
||||
// server bound to the wrong address in silence).
|
||||
shadowed.set(key, { winner: keyOrigin.get(key) ?? null, loser: envPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
loadedEnvPaths.push(envPath);
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn(` \x1b[33m⚠ Could not read ${envPath}: ${err?.message ?? err}\x1b[0m`);
|
||||
} catch {
|
||||
// Ignore errors reading env files.
|
||||
}
|
||||
}
|
||||
|
||||
for (const envPath of loadedEnvPaths) {
|
||||
console.log(` \x1b[2m📋 Loaded env from ${envPath}\x1b[0m`);
|
||||
}
|
||||
|
||||
for (const [key, { winner, loser }] of shadowed) {
|
||||
const setter = winner ? winner : "the environment";
|
||||
console.warn(` \x1b[33m⚠ ${key} in ${loser} is ignored, ${setter} set it first\x1b[0m`);
|
||||
}
|
||||
}
|
||||
|
||||
loadEnvFile();
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(resilience):** warn when `/healthz` is served under event-loop lag ≥200ms so a slow 200 is visible as sick, not healthy ([#10303](https://github.com/diegosouzapw/OmniRoute/issues/10303))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(docker):** add `GET`/`HEAD` `/livez` as a process-alive probe, distinct from `/healthz` readiness ([#10316](https://github.com/diegosouzapw/OmniRoute/issues/10316))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** accept `response_format=ogg` on `/v1/audio/speech` as an alias for the existing Opus/Ogg encoder ([#10587](https://github.com/diegosouzapw/OmniRoute/issues/10587))
|
||||
@@ -1 +0,0 @@
|
||||
- feat(server): emit systemd sd_notify READY/WATCHDOG/STOPPING (generated unit becomes Type=notify with WatchdogSec=180) so a frozen server process is killed and restarted by systemd instead of lingering undetected
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(call_logs):** persist the per-call error family in `call_logs.error_type` and expose a failure breakdown (`errorBreakdown`) in the usage analytics endpoint, reusing the existing production classifier ([#10670](https://github.com/diegosouzapw/OmniRoute/issues/10670))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(proxy):** the proxy-health sweep and `GET /api/settings/proxies/egress` now report an anonymous summary of egress-IP sharing — how many rotation groups share an egress IP and the largest number of accounts behind one IP — computed from persisted `proxy_logs` over a 24h window. No IPs and no account identities by default; `PROXY_LOG_INCLUDE_IPS=true` restores raw details. ([#10677](https://github.com/diegosouzapw/OmniRoute/issues/10677))
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(guides):** OmniRoute now serves VS Code's **native Copilot Chat model picker** through the [OmniCopilot](https://github.com/diegosouzapw/OmniCopilot) extension ([Marketplace](https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot) · [Open VSX](https://open-vsx.org/extension/diegosouzapw/omnicopilot) — Cursor, Windsurf, VSCodium, Theia…) — no Copilot subscription needed since VS Code 1.122. New [`docs/guides/VSCODE-COPILOT.md`](docs/guides/VSCODE-COPILOT.md) covers setup, how the picker collapses the `dual`-prefix catalog via `GET /v1/models?prefix=alias`, and the **build-time** `DASHBOARD_ALLOW_EMBED=vscode` flag that renders the dashboard in an editor tab ([#10697](https://github.com/diegosouzapw/OmniRoute/pull/10697))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(docker):** `DASHBOARD_ALLOW_EMBED` is now a Docker build argument — `docker build --build-arg DASHBOARD_ALLOW_EMBED=vscode` produces an image whose dashboard renders inside the VS Code Simple Browser (OmniCopilot's `dashboardOpen: "editor"`). Previously the flag was only reachable from a source build: Docker silently drops a `--build-arg` with no matching `ARG`, so the operator got the default image and no error. Builder-stage only and empty by default — the runtime stages deliberately do not carry it, and the unframable default posture is unchanged ([#10701](https://github.com/diegosouzapw/OmniRoute/pull/10701))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(providers):** new `cursor-api` provider (card "Cursor API", alias `cua`): connect a Cursor user API key (`crsr_…`) and route `cursor-api/<model>` through the existing Cursor agent executor (the key is exchanged for a 1h session token and cached), plus a `/api/cursor-cli/*` passthrough so the Cursor CLI itself runs through OmniRoute (`CURSOR_API_ENDPOINT=http://<omniroute>/api/cursor-cli`, `CURSOR_API_KEY=<OmniRoute key>`) with every RPC attributed and logged. The IDE `cursor` provider is unchanged. (#10729)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(api):** `GET /api/health` now answers `{ status, timestamp }` without a key. Until now the path had no route, so the management-auth boundary answered first with a 401 — indistinguishable from a wrong key or an unknown route, which left Docker HEALTHCHECKs and Kubernetes probes unable to tell "down" from "misconfigured". Kept deliberately minimal: version, uptime and memory stay behind the authenticated `/api/monitoring/health` ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10771)).
|
||||
@@ -1 +0,0 @@
|
||||
- feat(routing): make Task-Aware Smart Routing's detection patterns operator-configurable via `settings.taskRouting.patternOverrides` (`PUT /api/settings/task-routing`) — the built-in patterns are English-only, so a non-English dashboard had no recourse short of turning detection off entirely; an override now replaces the pattern list for one task type without touching the rest (#10783)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(api): accept PATCH on /api/combos/[id], the verb the OpenAPI spec already documents (#10869)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(sse):** add GLM-5.3 support (`glm-5.3`, `glm-5.3-high`, `glm-5.3-low`) across the z.ai first-party providers, mapping the upstream `reasoning_effort` request parameter to the existing 5.2 tier UX ([#10896](https://github.com/diegosouzapw/OmniRoute/pull/10896)) — thanks @phuongddx
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(home):** add a live **Recent Requests** panel beside the home Provider Topology (polls `GET /api/usage/call-logs?excludeTests=1` every ~3s, gated by the topology appearance toggle + page visibility). `excludeTests` is now an allowlist of real provider inference (`/v1/%` or `/api/v1/%`), applied before `LIMIT`, so connection-test/model-sync/management rows can never leak into the feed ([#10897](https://github.com/diegosouzapw/OmniRoute/pull/10897), extracted from [#8450](https://github.com/diegosouzapw/OmniRoute/pull/8450)) — thanks @nguyenha935
|
||||
@@ -1,2 +0,0 @@
|
||||
- **feat(credential-health):** pace the credential health sweep per connection via `provider_connections.healthCheckInterval` (minutes, 0 = never), with `CREDENTIAL_HEALTH_CHECK_INTERVAL` as the global default ([#8443](https://github.com/diegosouzapw/OmniRoute/issues/8443))
|
||||
- **behavior change:** `healthCheckInterval` is a shared column — it paces both the OAuth token refresh and the credential health sweep, and `0` disables both. The connection editor defaults it to 60, so configured OAuth connections are now credential-checked at 60min instead of the previous ~10min (aligned with the probe-volume goal of #8443)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(sse): add Cursor plan image generation via Agent CLI (`IMAGE_PROVIDERS.cursor`, format `cursor-agent-image`), reusing the chat Cursor OAuth connection
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(usage):** show Kimi Coding's fixed-order Code 5-hour/7-day quota windows plus Extra Usage status, balance, monthly spend/limit, and the official Additional Credits link on Dashboard → Quota cards.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(routing):** keep approximate Combo context estimates advisory so requests reach concrete targets instead of returning a pre-dispatch 400 ([#10162](https://github.com/diegosouzapw/OmniRoute/pull/10162)) — thanks @xz-dev
|
||||
@@ -1 +0,0 @@
|
||||
- fix(video): stop advertising the googleflow (Veo) video provider as working and fail fast with a clear diagnostic — its submit/poll endpoints 404 and no server-side OAuth transport can satisfy the working endpoint (#10285)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): hash the API key before using it as the model-catalog cache Map key (no raw credentials in process heap) (#10313)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(opencode-plugin):** publish bare combo model ids without the plugin provider prefix so OpenCode can select them ([#10345](https://github.com/diegosouzapw/OmniRoute/issues/10345))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(backend):** log `auto/<family> matched no connected models` once per process per label instead of every minute ([#10346](https://github.com/diegosouzapw/OmniRoute/issues/10346))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(docker):** warn at boot when `OMNIROUTE_MEMORY_MB` disagrees with `NODE_OPTIONS --max-old-space-size`, and document that the standalone/Docker launcher appends `OMNIROUTE_MEMORY_MB` last ([#10353](https://github.com/diegosouzapw/OmniRoute/issues/10353))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): fail over combo streaming responses that reach `finish_reason` with zero content, reasoning, or tool_calls instead of forwarding a terminated-but-empty completion (#10404)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(antigravity):** automatically rotate to a sibling account when one is BYOP (GCP Project ID required, `gcp_project_required` 422) — the account is excluded from selection for 24h and the request succeeds via another account instead of failing fast; the actionable 422 is surfaced only when no sibling exists (follow-up to the #10424 BYOP fast-fail) ([#10470](https://github.com/diegosouzapw/OmniRoute/pull/10470)) — thanks @rqzbeh
|
||||
@@ -1 +0,0 @@
|
||||
- fix(mitm): forward passthrough traffic to the actual requested Host instead of misrouting every non-TARGET_HOSTS request to the hardcoded Antigravity sandbox host (#10479)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): use 127.0.0.1 for the readiness health-check poll instead of localhost, avoiding Windows DNS-resolution delays that made a healthy server report as never-ready (#10508)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): register a real Firefly auth probe under both the `firefly` alias and the `adobe-firefly` canonical id, and normalize the provider id before the generic web-cookie fallback, so a Firefly connection stops always reporting "Provider validation not supported" (#10522)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): auto-replay a bounded multi-turn trajectory in the DeepSeek Web prompt builder for clients that never send `tools[]`, so agentic clients like Cline stop losing the original task after a couple of turns (#10527)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(network):** direct (no-proxy) egress now bounds each attempt's response-start window (default 30s, `OMNIROUTE_DIRECT_HEADERS_TIMEOUT_MS`) and retries once on a fresh no-keep-alive socket, so a silently-dropped pooled keep-alive connection can no longer stall direct providers (opencode-go, command-code) until a service restart ([#10214](https://github.com/diegosouzapw/OmniRoute/issues/10214))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(dashboard): show the real model count on the "List Models" endpoint card instead of a permanent "—" (#10553)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): remove 10 retired model ids from the crof seed catalog so /v1/models stops advertising models crof.ai no longer serves (#10577)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): resolve the short provider-alias prefix (e.g. `el/`) advertised by GET /v1/models for audio speech, transcription and translation model ids (#10586)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): map OpenAI-compat voice names to real ElevenLabs voice_ids in direct TTS (#10589)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(dashboard): make /api/models agree with /v1/models on synced-catalog coverage instead of reporting stale models as available (#10615)
|
||||
@@ -1 +0,0 @@
|
||||
- **Combo routing:** await each connection's token limit before reserving quota. The old lookup treated the `Promise` as a connection and dropped `rateLimitOverrides.tpm` ([#10686](https://github.com/diegosouzapw/OmniRoute/pull/10686)).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(guardrails): resolve the public provider alias before querying credentials in the Vision Bridge router, so command-code/opencode (and any alias!=id provider) are no longer reported as "unusable" despite active connections (#10702)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(dashboard): filter the Modality Bridge Vision model picker to vision-capable models, matching the sibling Video/Audio tabs (#10703)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(usage): repair provider-reported input_tokens: 0 on non-trivial requests instead of passing it through unrepaired (#10705)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): distinguish a CLI-probe timeout from a genuinely absent binary in locateCommand, and resolve the Hermes Agent Apply flow's `keyId` server-side instead of writing the `YOUR_OMNIROUTE_API_KEY_HERE` placeholder (#10710, #10711)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): pass --allow-scripts for the runtime's own npm-installed dependencies, so npm 12+'s default install-scripts block no longer silently skips better-sqlite3's native build (#10713)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(db): filter `getProviderMetrics()` to providers with a live `provider_connections` row so a deleted provider stops permanently ghost-haunting the Home "Provider Topology" widget (#10714)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(proxy): keep password-only proxy credentials instead of dropping them when no username is set (#10720)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** copilot-m365-web chat turns no longer surface as `(empty response)` — the type:4 invocation is aligned with the 2026-08 wire shape and now carries its type:1 Metrics follow-up in the same socket write, and the access token pre-flight-refreshes from a stored refresh_token instead of requiring a DevTools re-capture every ~75 minutes ([#10732](https://github.com/diegosouzapw/OmniRoute/pull/10732) — thanks @acc0mplish)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(catalog):** stop counting `getTokenLimit()`'s generic 128k catch-all as a known combo window, so `/v1/models` advertises the min of sourced member contexts instead of collapsing a 500k combo to 128k ([#10734](https://github.com/diegosouzapw/OmniRoute/issues/10734))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(search):** name `/v1/search` 502s with provider id and sanitized Node cause code, without hostnames ([#10735](https://github.com/diegosouzapw/OmniRoute/issues/10735))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api):** `/api/cache/stats` reported the prompt-cache LRU, which no request path ever writes to — it answered `0 hit / 0 miss, size 0` while the semantic cache served real traffic, and the Health and Usage dashboards rendered that as fact. It now reports the semantic cache's in-memory entries, with the same response shape ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10769)) — thanks @Poid-ZA, who first fixed this in #9446.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(logging):** the app log is filterable and readable again. Entries from the tagged logger (`[LEVEL] [TAG] message`) were filed under the level instead of the component, and printf format strings were never applied, so `%s`/`%d` stayed literal with the values trailing behind them unlabelled — including every LiveWS connection line, where the format is deliberate hardening against injected format specifiers ([#PRNUM](https://github.com/diegosouzapw/OmniRoute/pull/10770)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(analytics):** Claude Code (`claude`/`cc`) is a flat-rate subscription, so cost analytics reports `$0` for it instead of estimating Anthropic list prices — the metered `anthropic` API keeps its real cost, and budget/quota/routing still estimate as before ([#10774](https://github.com/diegosouzapw/OmniRoute/pull/10774)) — thanks @electrumguy
|
||||
@@ -1 +0,0 @@
|
||||
- fix(db): periodically run `wal_checkpoint(TRUNCATE)` so the SQLite WAL file shrinks on long-running servers (default 6h, override with `OMNIROUTE_WAL_TRUNCATE_INTERVAL_MS`, `0` disables) (#10781)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): replace LiveWS's application-only liveness check with a protocol-level `ws.ping()`/`pong` heartbeat (RFC 6455 §5.5.2) alongside the existing one, so a read-only dashboard subscriber that never sends anything survives the connection timeout — a socket that stops reading frames entirely is still reaped exactly as before (#10782)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** scope the same-account transport retry (#9708) out of emergency-fallback and combo hops — it was retrying the free fallback model and combo targets too, doubling upstream calls and corrupting the terminal error status on those paths.
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** Keep NVIDIA timeout probes and generic Antigravity/AGY HTTP 400 probes from poisoning credential health while preserving explicit Google geo-block handling ([#10799](https://github.com/diegosouzapw/OmniRoute/pull/10799)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(images):** register OpenAI `dall-e-3` in the image registry so unprefixed `dall-e-3` (and `openai/dall-e-3`) route to OpenAI Images instead of Microsoft Designer Web, and so the chat catalog no longer lists `openai/dall-e-3` as a 128k chat model ([#10832](https://github.com/diegosouzapw/OmniRoute/issues/10832))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(security):** Outbound URL guard now resolves IPv4-mapped IPv6 literals to their embedded address, so `[::ffff:169.254.169.254]` is refused by the unconditional cloud-metadata block like its dotted spelling; `[::]` is refused alongside `0.0.0.0` ([#10843](https://github.com/diegosouzapw/OmniRoute/pull/10843)) — thanks @ntdat812
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(i18n):** The "Disabled" status no longer renders as the noun for a person with a disability in Japanese, Spanish, Hindi, Polish, Telugu, Urdu and both Chinese locales — 24 strings now use each catalog's existing wording (ja 無効, es Deshabilitado, hi अक्षम, pl Wyłączone, te నిలిపివేయబడింది, ur غیر فعال, zh-CN 已禁用, zh-TW 已停用) ([#10812](https://github.com/diegosouzapw/OmniRoute/issues/10812), [#10853](https://github.com/diegosouzapw/OmniRoute/pull/10853)) — thanks @ntdat812
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(catalog):** `/v1/models` no longer advertises the built-in `auto/*` ids while auto routing is disabled — they were listed but rejected at request time with `Auto routing is disabled` ([#10831](https://github.com/diegosouzapw/OmniRoute/issues/10831), [#10857](https://github.com/diegosouzapw/OmniRoute/pull/10857)) — thanks @ntdat812
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(context):** Base64 file payloads (OpenAI `file` parts, Responses `input_file`, Claude `document` blocks) are budgeted like the Gemini `inlineData` path instead of being counted as prompt text — a ~1MB PDF estimated at 350k tokens and was rejected on the context limit before reaching the provider's document pipeline ([#10840](https://github.com/diegosouzapw/OmniRoute/issues/10840), [#10858](https://github.com/diegosouzapw/OmniRoute/pull/10858)) — thanks @ntdat812
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(mcp):** MCP tool calls that wait on a model provider no longer abort after 10 seconds. `omniRouteFetch` applied a single hardcoded `AbortSignal.timeout(10000)` to every internal hop, and `omniroute_route_request` — which posts to `/v1/chat/completions` and waits on the upstream provider, plus auto-combo candidate probing before a provider is even chosen — passed no signal of its own, so it inherited it. Any route slower than 10s failed from the MCP side while the identical request succeeded through the REST API. `omniroute_web_search` and `omniroute_web_fetch` in the same file already carried an explicit 60s signal, so that value is now shared by all three provider-bound calls instead of being repeated as a literal, while management reads (health, resilience, rate limits, combos, quota, usage) keep their fast-fail 10s budget so a stalled local endpoint still cannot hold a tool call open. Both budgets are overridable through `OMNIROUTE_MCP_FETCH_TIMEOUT_MS` and `OMNIROUTE_MCP_UPSTREAM_TIMEOUT_MS`, replacing the reported workaround of patching the compiled `dist/.build/next/server/chunks/*.js`; a malformed or non-positive override falls back to the default rather than disabling the timeout
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** importing models with an expired API key now surfaces the credential error instead of reporting "No new models were added". The Import button posts to `/api/providers/{id}/sync-models`, which self-fetches the models route; that route does not fail on an upstream 401 but degrades to a catalog it already has, preferring the cache and using the local catalog only when there is no cache. A provider that imported successfully once therefore has a cache, so an expired key produced `{ source: "cache", warning: "Models probe failed (401) — using cached catalog" }` with HTTP 200 — and the #5460/#5465 degradation guard only recognised the `local_catalog` branch, so model-sync accepted it as a successful discovery, found every cached model already imported, and returned the empty-diff result. Retest does not go through this path, which is why it failed correctly and made the import look like a genuine "nothing to do". The existing rule — a degraded discovery must not be persisted as the synced catalog — is now applied to the branch it missed rather than special-casing 401/403, discriminating on the warning the fallback builder always attaches (an ordinary non-refresh cache hit attaches none, and model-sync always requests `refresh=true`). `isDegradedLocalCatalog` keeps its exact meaning and its existing tests
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): reject a combo update that removes every model, and store the copilot's combo targets where the router reads them (#10866)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(proxy):** proxy "Test connection" no longer reports an IPv4-only SOCKS5/SSH proxy as dead. #1255 moved every egress probe from `api.ipify.org` to `api64.ipify.org` so proxies with IPv6 egress could be tested, but `api64` is IPv6-first: a tunnel with no IPv6 route has nothing to connect to, so the probe hung until the caller's deadline and a proxy that was carrying live LLM traffic came back as a failure. Swapping the target to `api4` fixes that case and re-breaks the one #1255 fixed, so the probe now tries the targets in order instead — `api64` first, so a proxy with working IPv6 answers on the first attempt and keeps the exact behaviour #1255 introduced, including which of its addresses is reported (the egress IP is used as an identity to detect accounts of one rotation group sharing an address, so the attempts are sequential rather than raced). The attempts split the budget each call site already enforced, so no probe can take longer than it could before, and each attempt gets its own `AbortController` so exhausting the budget on an unreachable target does not abort the next one. `OMNIROUTE_PROXY_ECHO_URL` pins a single target — including a self-hosted echo — replacing the workaround of rewriting the compiled bundle after every upgrade. The relay branch of the test route still targets `api64` through `x-relay-target`, since that request egresses from the relay worker rather than the operator's tunnel
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): warn when a .env line never takes effect, and stop swallowing an unreadable .env (#10870)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** Remove stale MiMoCode provider configuration, including the legacy `mcode` alias, left after provider retirement while preserving historical usage and call logs ([#10873](https://github.com/diegosouzapw/OmniRoute/pull/10873)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(provider-health):** Keep unsupported 404/405 validation probes neutral so they do not poison stored credential health or scheduler failure state, while still honoring per-connection health-check pacing ([#10878](https://github.com/diegosouzapw/OmniRoute/pull/10878)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(antigravity):** map Gemini 3.7 Flash tier ids (`gemini-3.7-flash-high/medium/low`, bare `gemini-3.7-flash`) to the upstream `gemini-3.7-flash-tiered` model id Google's Cloud Code endpoint expects, and configure per-tier thinking budgets ([#10882](https://github.com/diegosouzapw/OmniRoute/pull/10882)) — thanks @adevwithpurpose
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(memory):** enable agent memory save/update via MCP tools (`memory_save`/`update`/`search`/`delete` builtins with per-provider schemas, `apiKeyId` optional with caller-principal fallback) and gate server-side memory builtin injection to non-stream requests only ([#10887](https://github.com/diegosouzapw/OmniRoute/pull/10887)) — thanks @Egorich-print
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(perplexity-web):** make the built-in-search hint appended to every system message opt-in via `OMNIROUTE_PPLX_SEARCH_HINT` (off by default) — Perplexity's answer engine searches anyway, and the hint leaked into replies as meta-commentary for coding clients ([#10902](https://github.com/diegosouzapw/OmniRoute/pull/10902), extracted from [#8634](https://github.com/diegosouzapw/OmniRoute/pull/8634)) — thanks @danscMax
|
||||
@@ -1 +0,0 @@
|
||||
- fix(cli): repair hollow externalized package dirs in the nested `<distDir>/node_modules` bundle location too, not just the top-level one, fixing macOS/Linux Electron `ERR_MODULE_NOT_FOUND` on Turbopack-externalized packages (#7346)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(images):** retry Codex image generation on a sibling ChatGPT account when the requested model isn't entitled on the current account, instead of failing the request outright ([#8307](https://github.com/diegosouzapw/OmniRoute/pull/8307)).
|
||||
@@ -1 +0,0 @@
|
||||
- fix(compression): preserve unfenced raw code (e.g. Copilot #file references) from Caveman's prose recapitalization/whitespace cleanup, which was corrupting keyword casing and indentation (#9144)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(api): yield the event loop during catalog builds and bulk-load override/hidden-model tables (#9147)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** convert OpenAI `image_url` blocks nested in `role: "tool"` / `tool_result` content to Claude `image` source blocks so OpenAI-compatible clients (Kimi Code CLI `ReadMediaFile`, and any other tool that returns media) no longer 400 the next Claude-format upstream turn ([#9692](https://github.com/diegosouzapw/OmniRoute/issues/9692))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(resilience):** retry a retryable Codex pre-output 502/503/504/507 once on the same account (2–3s jitter) before cooling the connection, and stop translating that mixed pool into an all-accounts quota `429` ([#9708](https://github.com/diegosouzapw/OmniRoute/issues/9708))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(electron):** desktop window stays hidden on Windows because the embedded Next.js server binds to the machine hostname instead of loopback ([#PENDING](https://github.com/diegosouzapw/OmniRoute/pull/PENDING))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(build): tolerate a same-realpath symlink or stale-typed dest in the standalone bundle assembler, fixing non-deterministic `ERR_FS_CP_EINVAL`/`ERR_FS_CP_DIR_TO_NON_DIR` crashes under heavy concurrent build I/O
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(translator):** merge consecutive same-role contents in direct Claude to Gemini request translation to prevent upstream HTTP 400 errors
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(catalog):** derive combo reasoning-effort tiers from the exact runtime-selectable connection scope, intersecting dynamic, pinned, allowlisted, and compatible provider-node evidence while failing closed on unknown capabilities.
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(docker):** spell out that `:latest` tracks the highest **published** stable SemVer (not git `main`), and that GitOps should pin `X.Y.Z` ([#10317](https://github.com/diegosouzapw/OmniRoute/issues/10317))
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(backend):** document that memory extraction, skills injection, and token refresh share the request event loop, plus dashboard kill switches ([#10349](https://github.com/diegosouzapw/OmniRoute/issues/10349))
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(docker):** document default SQLite as single-replica / HA-unsupported, including Recreate and HEALTHCHECK session blast radius ([#10350](https://github.com/diegosouzapw/OmniRoute/issues/10350))
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(backend):** document that pre-write SQLite backups (including models.dev pricing) are throttled to once per 60 minutes and can be disabled with `DISABLE_SQLITE_AUTO_BACKUP` ([#10351](https://github.com/diegosouzapw/OmniRoute/issues/10351))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(tests):** drain three base-reds on the release branch — the Vietnamese locale regained parity with English (6 keys added), the chatCore SSE test now asserts the comment-free default that #10539 introduced instead of the trailer it replaced, and the Antigravity cloudcode test asserts the missing-messages guard it is named for instead of a `/ok/` regex that only ever matched the "ok" inside `: x-omniroute-tokens-in` ([#10704](https://github.com/diegosouzapw/OmniRoute/pull/10704))
|
||||
@@ -1 +0,0 @@
|
||||
- chore(security): remove the unused `enforceSecrets()` duplicate of the boot secret check and pin the live `enforceWebRuntimeEnv()` wiring with a regression test (#10775)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user