mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-18 13:14:56 +03:00
Compare commits
2 Commits
refactor/e
...
fix/10249-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84a62393f | ||
|
|
fb9984760c |
73
.env.example
73
.env.example
@@ -45,26 +45,6 @@ INITIAL_PASSWORD=CHANGEME
|
||||
# executor's on-disk thread-sticky session cache. Leave unset to rely on DATA_DIR.
|
||||
# OMNIROUTE_DATA_DIR=/var/lib/omniroute
|
||||
|
||||
# Escape hatch for the test-context DATA_DIR guard (#10428). A test run that never
|
||||
# chose a DATA_DIR is redirected to a throwaway temp dir so it cannot open the
|
||||
# operator's real database. Set to 1 only for a deliberate run against the real
|
||||
# DATA_DIR — never for CI. Used by: src/lib/dataPaths.ts
|
||||
# OMNIROUTE_ALLOW_DEFAULT_DATA_DIR=1
|
||||
|
||||
# Build provenance (#10427). OMNIROUTE_BUILD_SHA lets a container inject the artifact's git
|
||||
# SHA when the dist/BUILD_SHA sentinel is absent; it is also what `npm run build:release`
|
||||
# stamps. OMNIROUTE_RELEASE_REF is the ref the pack gate checks ancestry against, and
|
||||
# OMNIROUTE_ALLOW_CANARY_BUILD=1
|
||||
|
||||
# API key the canary-deploy smoke uses when the target gateway requires auth (#10429).
|
||||
# Used by: scripts/ops/deploy-canary.mjs — sent as `Authorization: Bearer` on the
|
||||
# /v1/chat/completions probe. Never needed by the server itself.
|
||||
# OMNIROUTE_SMOKE_API_KEY=sk-... records a deliberate off-release-line build instead of
|
||||
# failing it. Used by: scripts/build/buildProvenance.ts, src/lib/monitoring/buildSha.ts
|
||||
# OMNIROUTE_BUILD_SHA=abc1234
|
||||
# OMNIROUTE_RELEASE_REF=origin/main
|
||||
# OMNIROUTE_ALLOW_CANARY_BUILD=1
|
||||
|
||||
# Encryption key for SQLite database encryption at rest.
|
||||
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
|
||||
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.
|
||||
@@ -714,16 +694,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# Allow OmniRoute to write CLI config files (token refresh, etc.).
|
||||
# CLI_ALLOW_CONFIG_WRITES=true
|
||||
|
||||
# Force container detection on (1/true) or off (0/false). Leave unset for auto-detect
|
||||
# via /.dockerenv, /run/.containerenv, cgroup markers, or KUBERNETES_SERVICE_HOST.
|
||||
# Used by: src/shared/utils/containerEnv.ts — gates ephemeral-home CLI config writes.
|
||||
# OMNIROUTE_CONTAINER=1
|
||||
|
||||
# Allow CLI-tool config writes into an unmounted container path anyway (default off).
|
||||
# Prefer host-side `omniroute configure` / Remote Mode, or a bind-mounted CLI_CONFIG_HOME.
|
||||
# CLI equivalent: --allow-container-write. Used by: src/shared/utils/containerEnv.ts
|
||||
# OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE=true
|
||||
|
||||
# Auto-sync CLI profile files after provider model discovery changes. OPT-IN, default OFF for
|
||||
# both. When enabled, writes only the tool's profile files (~/.codex/*.config.toml or
|
||||
# ~/.claude/profiles/<name>/settings.json); never changes the active/default config. Both also
|
||||
@@ -745,21 +715,6 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_AUGGIE_BIN=auggie
|
||||
# AUGGIE_BIN=auggie
|
||||
|
||||
# ── ZCode (Z.ai GLM coding-plan CLI) local provider ──
|
||||
# The local "zcode" provider talks to the authenticated ZCode app-server over a
|
||||
# custom framed stdio protocol. Overrides below tune that stdio lifecycle.
|
||||
# ZCODE_BIN=zcode
|
||||
# ZCODE_ARGS=["--some-flag"]
|
||||
# ZCODE_CWD=
|
||||
# ZCODE_PROVIDER_ID=builtin:zai-coding-plan
|
||||
# ZCODE_SERVER_RUNTIME_ROOT=~/.zcode/server
|
||||
# ZCODE_SERVER_NODE=~/.zcode/server/node
|
||||
# ZCODE_SERVER_ENTRY=~/.zcode/server/zcode-server.cjs
|
||||
# ZCODE_STARTUP_TIMEOUT_MS=10000
|
||||
# ZCODE_RPC_TIMEOUT_MS=30000
|
||||
# ZCODE_TURN_TIMEOUT_MS=120000
|
||||
# ZCODE_POLL_INTERVAL_MS=250
|
||||
|
||||
# Override the Hermes Agent home directory (where OmniRoute reads/writes the
|
||||
# Hermes CLI config). Matches the env var the Hermes PowerShell installer sets
|
||||
# on Windows (%LOCALAPPDATA%\hermes); defaults to ~/.hermes when unset.
|
||||
@@ -1544,10 +1499,6 @@ APP_LOG_TO_FILE=true
|
||||
# Default: 100000
|
||||
# PROXY_LOGS_TABLE_MAX_ROWS=100000
|
||||
|
||||
# Include client/egress IPs and account prefixes in [ProxyEgress] console logs.
|
||||
# Default: false (the dashboard/database proxy-log records retain full details).
|
||||
# PROXY_LOG_INCLUDE_IPS=false
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
@@ -1898,13 +1849,6 @@ APP_LOG_TO_FILE=true
|
||||
# PROXY_AUTO_REMOVE=false
|
||||
# Consecutive failures before an auto-remove fires. Default: 3.
|
||||
# PROXY_AUTO_REMOVE_AFTER=3
|
||||
# Set "true" to let the scheduler auto-disable (status "dead") proxies after
|
||||
# repeated failures instead of deleting them. Non-destructive alternative to
|
||||
# PROXY_AUTO_REMOVE — the row stays in the registry, drops out of pool/rotation
|
||||
# resolution immediately, and is automatically re-activated once it starts
|
||||
# answering probes again. Shares the PROXY_AUTO_REMOVE_AFTER threshold above.
|
||||
# If both PROXY_AUTO_REMOVE and PROXY_AUTO_DISABLE are "true", auto-remove wins.
|
||||
# PROXY_AUTO_DISABLE=false
|
||||
# Let automated reachability probes (the scheduler + the "Test All" button) WRITE
|
||||
# a proxy's status. Default "false": probes are read-only and never deactivate a
|
||||
# proxy — only the operator sets active/inactive (a flaky probe must not strand an
|
||||
@@ -2409,12 +2353,6 @@ APP_LOG_TO_FILE=true
|
||||
# intended to be published as `omniroute-secure`. See SECURITY.md.
|
||||
# OMNIROUTE_BUILD_PROFILE=full
|
||||
|
||||
# Skip emitting `.tar.gz` tarballs during optional-pack staging for the Electron
|
||||
# standalone tree (pack directories + optional-packs.index.json are still produced).
|
||||
# Used by the desktop release workflow to trim artifact upload size.
|
||||
# Default (when unset): 1 (tarballs emitted). Set to 0 to disable.
|
||||
# OMNIROUTE_OPTIONAL_PACK_TAR=1
|
||||
|
||||
# Electron smoke harness (used by scripts/dev/smoke-electron-packaged.mjs).
|
||||
# ELECTRON_SMOKE_URL=http://127.0.0.1:20128/login
|
||||
# ELECTRON_SMOKE_TIMEOUT_MS=45000
|
||||
@@ -2714,9 +2652,9 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# Optional add-on (feature flag RADAR_ENABLED, default off — see feature flag
|
||||
# settings, not an env var) that overlays a signed, freshly-curated free-model
|
||||
# catalog on top of the release baseline. The first four variables below are
|
||||
# optional overrides for a self-hosted/forked feed or supporter-key flow. The
|
||||
# fifth is an optional, default-free link to the owner's private operations panel. Used by:
|
||||
# catalog on top of the release baseline. All four variables below are optional
|
||||
# and only needed to point the client at a self-hosted/forked feed or
|
||||
# supporter-key flow instead of the default OmniRoute Radar service. Used by:
|
||||
# src/lib/radar/sync.ts, src/lib/radar/pinnedKeys.ts, src/lib/radar/links.ts.
|
||||
|
||||
# Base URL of the Radar feed service. Overrides the built-in default so forks
|
||||
@@ -2737,11 +2675,6 @@ QUOTA_STORE_DRIVER=sqlite
|
||||
# page). No pricing/value lives in this repo — only the link.
|
||||
# RADAR_SUPPORTER_PLANS_URL=https://radar.omniroute.online/planos
|
||||
|
||||
# Owner-only link to the private Radar operations panel. There is deliberately
|
||||
# no default: when unset or invalid, no "Radar Admin" navigation item exists.
|
||||
# Use HTTPS for a tunnel/tailnet URL, or HTTP only for an SSH loopback forward.
|
||||
# RADAR_ADMIN_URL=http://127.0.0.1:9351
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# 27. RELEASE v3.8.50 ADDITIONS
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
2
.github/pull_request_template.md
vendored
2
.github/pull_request_template.md
vendored
@@ -18,7 +18,7 @@ Vitest, the 60% coverage gate, and the production build all run in CI on this PR
|
||||
- [ ] `npm run lint`
|
||||
- [ ] Reconciled with the current active release base; focused checks rerun afterward
|
||||
- [ ] Production-code changes include a new or updated automated test in this PR
|
||||
- SonarQube is temporarily opt-in while the private project has no quota; it is not a PR gate.
|
||||
- [ ] SonarQube PR analysis is green or any remaining issues are explicitly documented below
|
||||
|
||||
## Tests Added Or Updated
|
||||
|
||||
|
||||
16
.github/workflows/ci.yml
vendored
16
.github/workflows/ci.yml
vendored
@@ -697,12 +697,11 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
timeout-minutes: 30
|
||||
needs: build
|
||||
# WS1.5 (v3.8.49 plan): the Electron native-module path previously executed for
|
||||
# WS1.5 (v3.8.49 plan): the Electron rebuild/spawn path previously executed for
|
||||
# the FIRST time on the release tag — the v3.8.48 Windows bug (npx.cmd spawned
|
||||
# without shell, CVE-2024-27980 behavior change) could only surface at release.
|
||||
# windows-latest runs prepare:bundle (better-sqlite3 prebuild verification since
|
||||
# v13 — the node-gyp rebuild is gone) per release PR; ubuntu keeps the full
|
||||
# pack + headless smoke.
|
||||
# windows-latest runs prepare:bundle (the ABI rebuild + spawn plan) per release
|
||||
# PR; ubuntu keeps the full pack + headless smoke.
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
@@ -739,7 +738,7 @@ jobs:
|
||||
# precedent): its first-ever real run (2026-07-15, run 29457533565) died in
|
||||
# 0.7s with the error swallowed by pwsh — bash shell captures stderr and
|
||||
# continue-on-error keeps the heavy gate green while we harden it (#7336).
|
||||
- name: Prepare Electron standalone (Windows prebuild verification)
|
||||
- name: Prepare Electron standalone (Windows ABI rebuild + spawn path)
|
||||
if: runner.os == 'Windows'
|
||||
working-directory: electron
|
||||
continue-on-error: true
|
||||
@@ -999,10 +998,7 @@ jobs:
|
||||
name: SonarQube
|
||||
runs-on: ubuntu-latest
|
||||
needs: test-coverage
|
||||
# Temporarily opt-in: the private project currently has no Sonar quota.
|
||||
# Re-enable without another code change by setting the repository Actions
|
||||
# variable SONARQUBE_ENABLED=true after quota/project access is restored.
|
||||
if: ${{ vars.SONARQUBE_ENABLED == 'true' && !cancelled() && needs.test-coverage.result == 'success' }}
|
||||
if: ${{ !cancelled() && needs.test-coverage.result == 'success' }}
|
||||
env:
|
||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||
@@ -1401,7 +1397,7 @@ jobs:
|
||||
echo "| i18n UI Coverage | $(status '${{ needs.i18n-ui-coverage.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| i18n Glossary (zh-CN, ko) | $(status '${{ needs.i18n-glossary-zhcn.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| PR Test Policy | $(status '${{ needs.pr-test-policy.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| SonarQube (opt-in; disabled without SONARQUBE_ENABLED=true) | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "| SonarQube | $(status '${{ needs.sonarqube.result }}') |" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
echo "" >> "$GITHUB_STEP_SUMMARY"
|
||||
echo "## 🏗️ Build" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
4
.github/workflows/codeql.yml
vendored
4
.github/workflows/codeql.yml
vendored
@@ -22,10 +22,10 @@ jobs:
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: github/codeql-action/init@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
- uses: github/codeql-action/init@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
with:
|
||||
languages: javascript-typescript
|
||||
queries: security-extended
|
||||
- uses: github/codeql-action/analyze@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
|
||||
- uses: github/codeql-action/analyze@f205ea1c3313d32999d8d6a48b4f6530d4437b38 # v4.37.4
|
||||
with:
|
||||
category: "/language:javascript-typescript"
|
||||
|
||||
2
.github/workflows/dast-smoke.yml
vendored
2
.github/workflows/dast-smoke.yml
vendored
@@ -37,7 +37,7 @@ jobs:
|
||||
with:
|
||||
node-version: "24"
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
- name: Build CLI bundle
|
||||
env:
|
||||
OMNIROUTE_BUILD_BACKEND_ONLY: "1"
|
||||
|
||||
2
.github/workflows/docker-publish.yml
vendored
2
.github/workflows/docker-publish.yml
vendored
@@ -372,7 +372,7 @@ jobs:
|
||||
- name: Upload Trivy SARIF to Security tab
|
||||
if: needs.prepare.outputs.version != 'main'
|
||||
continue-on-error: true
|
||||
uses: github/codeql-action/upload-sarif@v4.37.6
|
||||
uses: github/codeql-action/upload-sarif@v4.37.4
|
||||
with:
|
||||
sarif_file: trivy-results.sarif
|
||||
category: trivy-image
|
||||
|
||||
116
.github/workflows/electron-release.yml
vendored
116
.github/workflows/electron-release.yml
vendored
@@ -55,75 +55,9 @@ jobs:
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
echo "✓ Valid version: $VERSION"
|
||||
|
||||
web-build:
|
||||
name: Build shared Next standalone
|
||||
needs: validate
|
||||
# Stage 8 (issue #10321): the four desktop legs used to each run the full
|
||||
# `npm run build` (Next standalone) — ~111 runner-minutes per release just to
|
||||
# produce the same platform-independent bundle four times. This job builds it
|
||||
# once on ubuntu; every leg then restores the byte-verified archive and
|
||||
# re-forks its native optionals (scripts/build/standaloneBundle.mjs).
|
||||
#
|
||||
# Rollback lever: set the repo variable ELECTRON_SHARED_STANDALONE=disabled.
|
||||
# This job then skips, every leg falls back to building its own web bundle
|
||||
# (the legacy step below), and the pipeline behaves exactly like pre-Stage 8 —
|
||||
# no revert needed.
|
||||
if: ${{ !cancelled() && needs.validate.result == 'success' && vars.ELECTRON_SHARED_STANDALONE != 'disabled' }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v7
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
NPM_CONFIG_LEGACY_PEER_DEPS: true
|
||||
|
||||
- name: Build Next.js standalone
|
||||
# webpack, not Turbopack, for the same hosted-runner RAM reason as the
|
||||
# linux leg (see the long comment on the fallback step in `build`).
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
NODE_OPTIONS: "--max_old_space_size=6144"
|
||||
OMNIROUTE_USE_TURBOPACK: "0"
|
||||
run: npm run build
|
||||
|
||||
- name: Pack standalone bundle
|
||||
# Deterministic tar.gz + byte-level manifest; the manifest embeds the
|
||||
# archive's own sha256 so artifact-transfer corruption is caught before
|
||||
# extraction, and every entry is re-verified after extraction.
|
||||
run: node scripts/build/standaloneBundle.mjs pack --out web-bundle.tar.gz
|
||||
|
||||
- name: Upload shared web bundle
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: web-standalone-bundle
|
||||
# compression-level 0: the payload is already a deterministic tar.gz;
|
||||
# re-zipping would only burn runner CPU without shrinking it further.
|
||||
compression-level: 0
|
||||
# Legs consume this within minutes; no reason to retain it like the
|
||||
# installer artifacts (default 90d).
|
||||
retention-days: 3
|
||||
path: |
|
||||
web-bundle.tar.gz
|
||||
web-bundle.tar.gz.manifest.json
|
||||
|
||||
build:
|
||||
name: Build Electron (${{ matrix.platform }})
|
||||
needs: [validate, web-build]
|
||||
# `web-build` is skipped when ELECTRON_SHARED_STANDALONE=disabled (rollback
|
||||
# mode); legs then run the legacy per-leg web build below. If it ran and
|
||||
# failed, fail closed: legs cannot package without the bundle, and silently
|
||||
# falling back to four per-leg builds would hide exactly the regression the
|
||||
# shared job exists to surface.
|
||||
if: ${{ !cancelled() && needs.validate.result == 'success' && (needs.web-build.result == 'success' || needs.web-build.result == 'skipped') }}
|
||||
needs: validate
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: write # electron-builder may publish artifacts with GH_TOKEN
|
||||
@@ -135,27 +69,19 @@ jobs:
|
||||
runner: windows-latest
|
||||
target: win
|
||||
ext: .exe
|
||||
os: win32
|
||||
arch: x64
|
||||
- platform: macos-intel
|
||||
runner: macos-15-intel
|
||||
target: mac-x64
|
||||
ext: .dmg
|
||||
os: darwin
|
||||
arch: x64
|
||||
- platform: macos-arm64
|
||||
runner: macos-latest
|
||||
target: mac-arm64
|
||||
ext: -arm64.dmg
|
||||
os: darwin
|
||||
arch: arm64
|
||||
- platform: linux
|
||||
runner: ubuntu-latest
|
||||
target: linux
|
||||
ext: .AppImage
|
||||
deb_ext: .deb
|
||||
os: linux
|
||||
arch: x64,arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
@@ -167,6 +93,14 @@ jobs:
|
||||
node-version: 24
|
||||
cache: npm
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v6.1.0
|
||||
with:
|
||||
path: node_modules
|
||||
key: ${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-node-
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
env:
|
||||
@@ -182,11 +116,7 @@ jobs:
|
||||
mkdir -p "$RUNNER_TEMP/home"
|
||||
echo "USERPROFILE=$RUNNER_TEMP/home" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Build Next.js standalone (legacy per-leg fallback)
|
||||
# Stage 8: only runs in rollback mode (ELECTRON_SHARED_STANDALONE=disabled)
|
||||
# or when the shared web-build job was skipped. Otherwise the leg restores
|
||||
# the shared bundle from the `web-build` job below.
|
||||
if: needs.web-build.result == 'skipped'
|
||||
- name: Build Next.js standalone
|
||||
env:
|
||||
JWT_SECRET: ci-build-secret-with-sufficient-length-for-validation
|
||||
NODE_OPTIONS: "--max_old_space_size=6144"
|
||||
@@ -204,30 +134,6 @@ jobs:
|
||||
OMNIROUTE_USE_TURBOPACK: ${{ matrix.platform == 'linux' && '0' || '1' }}
|
||||
run: npm run build
|
||||
|
||||
- name: Download shared web bundle
|
||||
# Stage 8: inverse of the fallback step above — runs exactly when the
|
||||
# shared `web-build` job produced the bundle.
|
||||
if: needs.web-build.result == 'success'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: web-standalone-bundle
|
||||
|
||||
- name: Restore + hydrate shared web bundle
|
||||
if: needs.web-build.result == 'success'
|
||||
shell: bash
|
||||
# restore: verify the archive's sha256 against the manifest, extract, then
|
||||
# re-verify every entry (existence + size + content hash + symlink
|
||||
# targets, and no unlisted files) byte-for-byte.
|
||||
# hydrate: the bundle was built on ubuntu, so install-machine-forked native
|
||||
# optionals (@img/sharp-*, @img/sharp-libvips-*, @ngrok/ngrok-*,
|
||||
# fsevents) carry linux forks. Replace them with the forks this
|
||||
# leg's own `npm ci` resolved, then assert every bundled native
|
||||
# (koffi triplets, better-sqlite3 prebuilds, wreq-js, onnxruntime)
|
||||
# can service this leg's platform/arch before packaging starts.
|
||||
run: |
|
||||
node scripts/build/standaloneBundle.mjs restore --archive web-bundle.tar.gz
|
||||
node scripts/build/standaloneBundle.mjs hydrate --platform ${{ matrix.os }} --arch ${{ matrix.arch }}
|
||||
|
||||
- name: Sync version in electron/package.json
|
||||
shell: bash
|
||||
env:
|
||||
@@ -252,7 +158,7 @@ jobs:
|
||||
|
||||
- name: Install Electron dependencies
|
||||
working-directory: electron
|
||||
run: npm ci --no-audit --no-fund
|
||||
run: npm install --no-audit --no-fund
|
||||
|
||||
- name: Build Electron for ${{ matrix.platform }}
|
||||
working-directory: electron
|
||||
|
||||
58
.github/workflows/quality.yml
vendored
58
.github/workflows/quality.yml
vendored
@@ -60,49 +60,13 @@ jobs:
|
||||
build:
|
||||
name: Build (advisory)
|
||||
needs: changes
|
||||
# FORK PRs ONLY. build.yml's `Fast Production Build` triggers on `push: branches: ["**"]`
|
||||
# and runs `build:release` — a superset of this job — so for an own-origin branch this job
|
||||
# was building the same tree twice. A fork contributor pushes to THEIR repo, so that push
|
||||
# never fires here, and this is the only pre-merge build signal they get. Measured
|
||||
# 2026-08-14: 72 of the last 100 PRs into release/** came from forks, so the fork case is
|
||||
# the majority of the traffic, not the exception — this job earns its place, it just should
|
||||
# not duplicate build.yml for the own-origin 28%.
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true' && github.event.pull_request.head.repo.full_name != github.repository) }}
|
||||
# PINNED to hosted — this was the last job in THIS workflow still on the USE_VPS_RUNNER
|
||||
# switch (ci.yml's Build, nightly-release-green and npm-publish keep it, so the variable
|
||||
# stays meaningful), and with USE_VPS_RUNNER=true it produced NO signal at all here.
|
||||
# Measured 2026-08-14 over the last 25
|
||||
# quality.yml runs: not one Build (advisory) reached a conclusion. Every sample was either
|
||||
# queued on the self-hosted pool (2 runners, `omniroute-113-6/7`, both permanently busy — one
|
||||
# job sat queued 2h+ and was still unclaimed) or, when it did land, killed mid-build by this
|
||||
# workflow's own `cancel-in-progress` concurrency. 6/6 sampled "failures" are exit 143 /
|
||||
# "The runner has received a shutdown signal" at ~3.5 min into `npm run build` — zero OOM,
|
||||
# zero build errors. So the job burned a scarce runner that the gates actually need while
|
||||
# reporting a permanent red on every PR.
|
||||
#
|
||||
# Gap 19 left USE_VPS_RUNNER governing build-like jobs on the premise that "the build needs
|
||||
# the .113's RAM". That premise no longer holds: `Fast Production Build` (build.yml) runs
|
||||
# `build:release` — a SUPERSET of this job's `npm run build`, plus the CLI bundle — on plain
|
||||
# ubuntu-latest and passed 24/25 of its last runs in ~15 min. What it has and this job did
|
||||
# not is memory PROVISIONING: a 10 GB swapfile plus a 12 GB V8 heap. That matters because
|
||||
# --max-old-space-size only bounds V8's JS heap, never Turbopack's native (Rust) allocation
|
||||
# (#6409) — swap is what absorbs the native peak. Both are mirrored below.
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event_name != 'pull_request' || ((github.event.pull_request.draft == false || startsWith(github.head_ref, 'mergify/merge-queue/')) && needs.changes.outputs.code == 'true') }}
|
||||
# Dynamic runner — same fork-safe rule as ci.yml / fast-gates.
|
||||
runs-on: ${{ (vars.USE_VPS_RUNNER == 'true' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository)) && fromJSON('["self-hosted","omni-release"]') || 'ubuntu-latest' }}
|
||||
# #7307: advisory for the first week of release-PR runs; remove
|
||||
# continue-on-error after the production-build signal is stable.
|
||||
continue-on-error: true
|
||||
steps:
|
||||
# Mirrors build.yml: Turbopack's native peak is not bounded by --max-old-space-size, so
|
||||
# the hosted runner needs swap headroom before the build starts.
|
||||
- name: Expand virtual memory (10 GB swap)
|
||||
run: |
|
||||
sudo swapoff -a || true
|
||||
sudo rm -f /mnt/swapfile /swapfile
|
||||
sudo fallocate -l 10G /mnt/swapfile || sudo dd if=/dev/zero of=/mnt/swapfile bs=1M count=10240
|
||||
sudo chmod 600 /mnt/swapfile
|
||||
sudo mkswap /mnt/swapfile
|
||||
sudo swapon /mnt/swapfile
|
||||
free -h
|
||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
@@ -115,10 +79,6 @@ jobs:
|
||||
- run: npm run build
|
||||
env:
|
||||
OMNIROUTE_USE_TURBOPACK: "1"
|
||||
# Same heap build.yml proves sufficient. build-next-isolated.mjs defaults to 8192 and
|
||||
# honours OMNIROUTE_BUILD_MEMORY_MB; NODE_OPTIONS is set for parity with build.yml.
|
||||
NODE_OPTIONS: "--max-old-space-size=12288"
|
||||
OMNIROUTE_BUILD_MEMORY_MB: "12288"
|
||||
# No artifact upload here: the PR-to-release quality workflow has no
|
||||
# downstream package/e2e jobs that consume the Next.js build output.
|
||||
|
||||
@@ -137,7 +97,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
# One walk of src/app/api for openapi-routes + docs-symbols (both still fail independently).
|
||||
- run: npm run check:api-docs-refs
|
||||
- name: Docs accuracy (fabricated-docs + i18n mirrors, strict)
|
||||
@@ -181,7 +141,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
@@ -430,7 +390,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
# WS5.2/5.3: JUnit feeds Trunk Flaky Tests — the fast-path runs on EVERY PR,
|
||||
# which is where flaky-detection volume actually comes from (ci.yml's heavy
|
||||
# jobs only run on the release PR). Advisory upload, own-origin only.
|
||||
@@ -476,7 +436,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
# QW-d: fonte única — o mesmo npm script do CI pesado/local. Fecha dois drifts do
|
||||
# comando inline antigo: os dirs `memory` e `usage` estavam FORA do glob (testes
|
||||
# silenciosamente não rodavam no fast path) e o setupPolyfill não era importado.
|
||||
@@ -516,7 +476,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
- name: Restore ESLint file cache
|
||||
uses: actions/cache@v6
|
||||
with:
|
||||
@@ -583,7 +543,7 @@ jobs:
|
||||
with:
|
||||
node-version: ${{ env.CI_NODE_VERSION }}
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm ci
|
||||
- name: CHANGELOG integrity (nenhum bullet da base pode sumir no merge-result)
|
||||
run: npm run check:changelog-integrity
|
||||
- name: Agent-skills generator sync (SKILL.md gerado ≡ catálogo)
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -1,7 +1,6 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# project-specific directories
|
||||
/output/
|
||||
.slim/deepwork/
|
||||
.omnivscodeagent/
|
||||
omnirouteCloud/
|
||||
@@ -70,8 +69,6 @@ yarn-error.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
# Local gitleaks artifacts (do not commit)
|
||||
gitleaks-local.json
|
||||
!.env.example
|
||||
!.env.homolog.example
|
||||
!.env.devin-bridge.example
|
||||
|
||||
@@ -14,11 +14,3 @@ open-sse/config/freeModelCatalog.data.ts
|
||||
# Prettier reformats the frontmatter (blank line after ---), which makes the gate
|
||||
# fail on any skill that happens to pass through lint-staged.
|
||||
skills/*/SKILL.md
|
||||
|
||||
# check:changelog-integrity compares release bullets against the base as exact
|
||||
# strings. Prettier normalizes markdown emphasis inside them (*from* -> _from_)
|
||||
# and re-wraps table rows, so any PR that stages CHANGELOG.md would "lose" base
|
||||
# bullets and turn the merge-integrity job red. The changelog is generated and
|
||||
# reconciled by scripts/release/*, which are its formatter of record.
|
||||
CHANGELOG.md
|
||||
docs/i18n/*/CHANGELOG.md
|
||||
|
||||
47
AGENTS.md
47
AGENTS.md
@@ -46,22 +46,22 @@ Repository map and Reference Documentation sections below.
|
||||
|
||||
## Project at a Glance
|
||||
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 341 LLM providers, auto-fallback.
|
||||
**OmniRoute** — unified AI proxy/router. One endpoint, 339 LLM providers, auto-fallback.
|
||||
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
|
||||
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
|
||||
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
||||
| 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 (150 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 |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
| Layer | Location | Purpose |
|
||||
| ------------- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
|
||||
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
|
||||
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
||||
| 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 (145 migrations) |
|
||||
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
||||
| MCP Server | `open-sse/mcp-server/` | 105 tools (43 base + memory/skill/agentSkill/pool/notion/obsidian/gamification/plugin modules), 3 transports (stdio / SSE / Streamable HTTP), 31 scopes |
|
||||
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
||||
| Skills | `src/lib/skills/` | Extensible skill framework |
|
||||
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
||||
|
||||
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
|
||||
|
||||
@@ -118,18 +118,11 @@ upstream/service level, so one unhealthy provider does not slow down every reque
|
||||
- `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
|
||||
breaker, failure opens it again.
|
||||
|
||||
**Defaults** (`open-sse/config/constants.ts` → `PROVIDER_PROFILES`). Two thresholds live side by
|
||||
side — do not confuse them:
|
||||
**Defaults** (`open-sse/config/constants.ts`):
|
||||
|
||||
| Profile | `providerFailureThreshold` (whole provider) | `providerCooldownMs` | `circuitBreakerThreshold` (one connection) | `circuitBreakerReset` |
|
||||
| ------- | ------------------------------------------: | -------------------: | -----------------------------------------: | --------------------: |
|
||||
| OAuth | `10` | `5min` | `8` | `60s` |
|
||||
| API key | `15` | `10min` | `12` | `30s` |
|
||||
| Local | `2` | `1min` | `2` | `15s` |
|
||||
|
||||
The provider-level thresholds were scaled up for deployments with 500+ connections (OAuth was
|
||||
`3`, API key was `5`); every default is overridable through the `OMNIROUTE_PROVIDER_BREAKER_*`
|
||||
and `OMNIROUTE_CIRCUIT_BREAKER_*` env vars.
|
||||
- OAuth providers: threshold `3`, reset timeout `60s`.
|
||||
- API-key providers: threshold `5`, reset timeout `30s`.
|
||||
- Local providers: threshold `2`, reset timeout `15s`.
|
||||
|
||||
Only provider-level failure statuses should trip the provider breaker:
|
||||
|
||||
@@ -686,7 +679,7 @@ the stale-enforcement added in Fase 6A.3.
|
||||
causa-raiz de DOIS wipes (2026-08-08 e 2026-08-10: `git reset --hard` materializou o
|
||||
symlink rastreado por cima do diretório real e o git apagou todo o conteúdo ignorado sem
|
||||
aviso); (c) após qualquer escrita relevante, `git -C _tasks add -A && git -C _tasks commit
|
||||
&& git -C _tasks push` — o push frequente é o backup real; (d) repetir esta proibição
|
||||
&& git -C _tasks push` — o push frequente é o backup real; (d) repetir esta proibição
|
||||
VERBATIM no prompt de todo subagente que toque git; (e) se `_tasks` aparecer como symlink
|
||||
quebrado, NÃO commitar nada — restaurar do remote e avisar o operador. O gate
|
||||
`check:tracked-artifacts` (pre-commit + CI) bloqueia `_tasks` rastreado em qualquer forma.
|
||||
|
||||
122
CHANGELOG.md
122
CHANGELOG.md
@@ -148,26 +148,9 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **feat(admission) — direct pushes:** adaptive overload/pressure controls with shared admission wired across the LLM routes, plus mutation-test registration for the capability-filter suite
|
||||
- **feat(agentrouter) — direct pushes:** support Claude and Codex protocols — infer the protocol from the client endpoint and honor the alternate protocol through the chat pipeline
|
||||
- **feat(providers) — direct pushes:** ChatGPT Web session credential guide with a Cookie Editor fast-path (canonical chromewebstore install link) and web-session fast-path test coverage
|
||||
- **feat(sse):** honor provider-rule lock scope for agentrouter (connection vs model) ([#10419](https://github.com/diegosouzapw/OmniRoute/pull/10419))
|
||||
- **feat(ocr):** Vertex AI DeepSeek-OCR provider ([#10398](https://github.com/diegosouzapw/OmniRoute/pull/10398))
|
||||
- **feat(providers):** derive imageToText from the OCR registry + chutes dots.ocr seed ([#10400](https://github.com/diegosouzapw/OmniRoute/pull/10400))
|
||||
- **feat(ocr):** multi-provider /v1/ocr with transformation layer (Azure Document Intelligence) ([#10283](https://github.com/diegosouzapw/OmniRoute/pull/10283))
|
||||
- **feat(providers):** declare imageToText serviceKind on major vision providers ([#10275](https://github.com/diegosouzapw/OmniRoute/pull/10275))
|
||||
- **feat(bridge):** native-vision skip guard + configurable describe output cap ([#10289](https://github.com/diegosouzapw/OmniRoute/pull/10289))
|
||||
- **feat(bridge):** normalize images to 2048px long edge before vision describe self-call ([#10287](https://github.com/diegosouzapw/OmniRoute/pull/10287))
|
||||
- **feat(sse):** restate agentrouter quota 403/400 as retryable 429 with provider-scoped error rules ([#10335](https://github.com/diegosouzapw/OmniRoute/pull/10335))
|
||||
- **feat(sse):** add i-have-adhd output style to compression catalog ([#10271](https://github.com/diegosouzapw/OmniRoute/pull/10271))
|
||||
- **feat(codex):** add OAuth fingerprint convergence modes ([#10243](https://github.com/diegosouzapw/OmniRoute/pull/10243)) — thanks @xz-dev
|
||||
- **feat(i18n):** complete Portuguese (PT-PT) translation ([#10250](https://github.com/diegosouzapw/OmniRoute/pull/10250)) — thanks @DarkEsteves
|
||||
- **feat(providers):** publish Poolside's probed Laguna Preview catalog ([#10216](https://github.com/diegosouzapw/OmniRoute/pull/10216)) — thanks @pacocartones
|
||||
- **feat(crof):** advertise reasoning effort tiers incl. max from live discovery and registry ([#10062](https://github.com/diegosouzapw/OmniRoute/pull/10062)) — thanks @excessivechaos
|
||||
- **feat(open-sse):** expose provider-level circuit breaker thresholds via env vars (#10040) ([#10046](https://github.com/diegosouzapw/OmniRoute/pull/10046)) — thanks @tiangao88
|
||||
- **feat(dashboard):** Kimi 15% first-top-up campaign — dedicated tracked link + discount-first banner copy ([#10240](https://github.com/diegosouzapw/OmniRoute/pull/10240))
|
||||
- **feat(providers):** integrate audited free-tier gateways ([#9210](https://github.com/diegosouzapw/OmniRoute/pull/9210))
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **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)
|
||||
- fix(vision-bridge): describe-model no longer returns unreachable "openai/gpt-4o-mini" when every vision-capable provider is unreachable on the instance — returns null instead and surfaces a clear error (#8430)
|
||||
@@ -570,63 +553,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **fix(i18n) — direct pushes:** restore/unescape HTML entities in UI strings, translate capability-filter messages, complete web-session guide translations and Vietnamese parity
|
||||
- **fix(providers) — direct pushes:** repair the DeepAI registry import + executor
|
||||
- **fix(deps) — direct pushes:** CVE-driven bumps (nanoid, dompurify, mermaid, js-yaml + transitive deps for 26 Dependabot alerts) and retained isolated-build runtime dependencies in the pack
|
||||
- **fix(ci):** pin Build (advisory) to a hosted runner with memory provisioning ([#10408](https://github.com/diegosouzapw/OmniRoute/pull/10408))
|
||||
- **fix(providers):** refresh the translate-path golden for the bailian Token Plan endpoint ([#10410](https://github.com/diegosouzapw/OmniRoute/pull/10410))
|
||||
- **fix(sse):** surface Qwen/Alibaba personal Token Plan quota in dashboard and preflight ([#10290](https://github.com/diegosouzapw/OmniRoute/pull/10290))
|
||||
- **fix(deps):** pin next to an exact version so a fresh upstream release cannot break installs ([#10340](https://github.com/diegosouzapw/OmniRoute/pull/10340))
|
||||
- **fix(types):** restore custom model output limit contract ([#10339](https://github.com/diegosouzapw/OmniRoute/pull/10339)) — thanks @backryun
|
||||
- **fix(sse):** stop the executor-contract guard from hot-looping the router ([#10373](https://github.com/diegosouzapw/OmniRoute/pull/10373))
|
||||
- **fix(types):** validate nonstreaming JSON contracts ([#10258](https://github.com/diegosouzapw/OmniRoute/pull/10258)) — thanks @backryun
|
||||
- **fix(types):** narrow refresh token rotation inputs ([#10257](https://github.com/diegosouzapw/OmniRoute/pull/10257)) — thanks @backryun
|
||||
- **fix(types):** normalize executor result contracts ([#10256](https://github.com/diegosouzapw/OmniRoute/pull/10256)) — thanks @backryun
|
||||
- **fix(types):** align Responses stream options ([#10255](https://github.com/diegosouzapw/OmniRoute/pull/10255)) — thanks @backryun
|
||||
- **fix(types):** narrow combo credential preflight ([#10254](https://github.com/diegosouzapw/OmniRoute/pull/10254)) — thanks @backryun
|
||||
- **fix(compression):** cap countTextTokens at 50k chars and strip base64 data URIs ([#10118](https://github.com/diegosouzapw/OmniRoute/pull/10118)) — thanks @adevwithpurpose
|
||||
- **fix(ci):** clear base-reds on release/v3.8.50 (round 4) ([#10260](https://github.com/diegosouzapw/OmniRoute/pull/10260))
|
||||
- **fix(sse):** extract perplexity-web answers from workflow_block ([#10259](https://github.com/diegosouzapw/OmniRoute/pull/10259)) — thanks @jeyhunfaslanov
|
||||
- **fix(mcp):** persist and re-attach Gemini thoughtSignature on the direct Claude<->Gemini path ([#9448](https://github.com/diegosouzapw/OmniRoute/pull/9448)) — thanks @Sam280903
|
||||
- **fix(opencode-plugin):** respect log level for lifecycle output (#8982) ([#9316](https://github.com/diegosouzapw/OmniRoute/pull/9316)) — thanks @xiaoyaner0201
|
||||
- **fix(providers):** raise default provider probe timeout from 5s to 8s ([#9283](https://github.com/diegosouzapw/OmniRoute/pull/9283)) — thanks @Sam280903
|
||||
- **fix(opencode-plugin):** stop warning when an auto combo replaces its expected /v1/models twin (#8983) ([#9042](https://github.com/diegosouzapw/OmniRoute/pull/9042)) — thanks @xiaoyaner0201
|
||||
- **fix(opencode):** force CLI User-Agent when CLI identity synthesis is enabled ([#10222](https://github.com/diegosouzapw/OmniRoute/pull/10222)) — thanks @adevwithpurpose
|
||||
- **fix(deepseek-web):** classify business auth rejection as 401 ([#10218](https://github.com/diegosouzapw/OmniRoute/pull/10218)) — thanks @Zartharas
|
||||
- **fix(combo):** make failoverBeforeRetry actually skip the same-model retry ([#10217](https://github.com/diegosouzapw/OmniRoute/pull/10217)) — thanks @hartmark
|
||||
- **fix(responses):** preserve case-insensitive combo names before Codex rewrite ([#10177](https://github.com/diegosouzapw/OmniRoute/pull/10177)) — thanks @ddarkr
|
||||
- **fix(discovery):** parse reasoning tiers nested under metadata.reasoning.supported_efforts ([#10138](https://github.com/diegosouzapw/OmniRoute/pull/10138)) — thanks @excessivechaos
|
||||
- **fix(combo):** isolate session stickiness by combo ([#10137](https://github.com/diegosouzapw/OmniRoute/pull/10137)) — thanks @hydraxman
|
||||
- **fix(combo):** default chaos SSE to comment-only for OpenAI-compatible clients ([#10128](https://github.com/diegosouzapw/OmniRoute/pull/10128)) — thanks @herjarsa
|
||||
- **fix(kimi):** normalize MFJS tool schemas ([#10079](https://github.com/diegosouzapw/OmniRoute/pull/10079)) — thanks @xz-dev
|
||||
- **fix(mcp):** move pack validation out of unit suite ([#10065](https://github.com/diegosouzapw/OmniRoute/pull/10065)) — thanks @yansigit
|
||||
- **fix(zed-hosted):** send the provider wire values cloud.zed.dev accepts ([#10051](https://github.com/diegosouzapw/OmniRoute/pull/10051)) — thanks @ARC345
|
||||
- **fix(ci):** repair and wire the two live-server E2E suites ([#10050](https://github.com/diegosouzapw/OmniRoute/pull/10050)) — thanks @ARC345
|
||||
- **fix(reasoning):** preserve and replay assistant turns ([#10045](https://github.com/diegosouzapw/OmniRoute/pull/10045)) — thanks @jackjinke
|
||||
- **fix(types):** tighten chatCore helper contracts ([#10175](https://github.com/diegosouzapw/OmniRoute/pull/10175)) — thanks @backryun
|
||||
- **fix(cli):** read the full provider catalog instead of the 6-entry fallback ([#10097](https://github.com/diegosouzapw/OmniRoute/pull/10097)) — thanks @amartinawi
|
||||
- **fix(cli):** stop swallowing non-2xx responses into benign-looking results ([#10092](https://github.com/diegosouzapw/OmniRoute/pull/10092)) — thanks @amartinawi
|
||||
- **fix(cli):** openapi endpoints/paths/validate accept the served catalog shape ([#10091](https://github.com/diegosouzapw/OmniRoute/pull/10091)) — thanks @amartinawi
|
||||
- **fix(cli):** doctor detects prebuilt better-sqlite3 binaries ([#10090](https://github.com/diegosouzapw/OmniRoute/pull/10090)) — thanks @amartinawi
|
||||
- **fix(providers):** kilo-gateway authType should be optional, not apikey ([#10086](https://github.com/diegosouzapw/OmniRoute/pull/10086)) — thanks @TengSivtean
|
||||
- **fix(cli):** strip inline comments when parsing .env values ([#10101](https://github.com/diegosouzapw/OmniRoute/pull/10101)) — thanks @amartinawi
|
||||
- **fix(logging):** document CHAT_LOG_MAX_BODY_KB, capture messageCount for Responses API bodies ([#10038](https://github.com/diegosouzapw/OmniRoute/pull/10038)) — thanks @hartmark
|
||||
- **fix(dashboard):** expose OpenAI Responses store toggle for non-Codex connections ([#10121](https://github.com/diegosouzapw/OmniRoute/pull/10121)) — thanks @hartmark
|
||||
- **fix(combo):** clear LKGP pin when its target fails, not only set it on success ([#10034](https://github.com/diegosouzapw/OmniRoute/pull/10034)) — thanks @hartmark
|
||||
- **fix(sse):** provider-response summary format bugs (dashboard Provider Response panel) ([#10037](https://github.com/diegosouzapw/OmniRoute/pull/10037)) — thanks @hartmark
|
||||
- **fix(responses-api):** tool call after reasoning collided on the same output_index ([#10025](https://github.com/diegosouzapw/OmniRoute/pull/10025)) — thanks @hartmark
|
||||
- **fix(responses-api):** explicit function-tool declaration must win over apply_patch-is-custom fallback ([#10041](https://github.com/diegosouzapw/OmniRoute/pull/10041)) — thanks @hartmark
|
||||
- **fix(kimi):** recupera limite temporario sem bloquear conta ([#10058](https://github.com/diegosouzapw/OmniRoute/pull/10058)) — thanks @bortolidiego
|
||||
- **fix(translator):** preserve Responses custom tools for OpenAI-compatible providers ([#10114](https://github.com/diegosouzapw/OmniRoute/pull/10114)) — thanks @mtb-ninja
|
||||
- **fix(providers):** xai-oauth chat→responses body + missing breaker import (#10165) ([#10170](https://github.com/diegosouzapw/OmniRoute/pull/10170)) — thanks @nordz0r
|
||||
- **fix(ollama-cloud):** map xhigh reasoning effort to max ([#10160](https://github.com/diegosouzapw/OmniRoute/pull/10160)) — thanks @Chewji9875
|
||||
- **fix(translator):** strip Codex encrypted tool-schema key for Gemini/Antigravity ([#10053](https://github.com/diegosouzapw/OmniRoute/pull/10053)) — thanks @XDayonline
|
||||
- **fix(combo):** preserve OpenCode Free oc/ prefix for connections ([#10180](https://github.com/diegosouzapw/OmniRoute/pull/10180)) — thanks @AStupidBear
|
||||
- **fix(sse):** apply free-tier filter to auto/best-free on chat path ([#10199](https://github.com/diegosouzapw/OmniRoute/pull/10199)) — thanks @ggdayup
|
||||
- **fix(providers):** default missing cache_control.ttl to 1h on the native Claude OAuth path ([#10221](https://github.com/diegosouzapw/OmniRoute/pull/10221)) — thanks @jeff-alves
|
||||
- **fix(ci):** clear base-reds on release/v3.8.50 (round 3) ([#10213](https://github.com/diegosouzapw/OmniRoute/pull/10213))
|
||||
- **fix(security):** correct XML double-unescape and non-CSPRNG nonce from CodeQL sweep ([#10154](https://github.com/diegosouzapw/OmniRoute/pull/10154))
|
||||
- **fix(docker):** eliminate npm-bundled CVEs from the published image ([#10182](https://github.com/diegosouzapw/OmniRoute/pull/10182))
|
||||
- **fix(security):** resolve open CodeQL alerts ([#10188](https://github.com/diegosouzapw/OmniRoute/pull/10188))
|
||||
- **fix(dashboard):** retarget Kimi promo CTA to the API platform aff link ([#10200](https://github.com/diegosouzapw/OmniRoute/pull/10200))
|
||||
- **fix(build):** repair broken production build, red lint gate and SWR crash ([#10198](https://github.com/diegosouzapw/OmniRoute/pull/10198))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
@@ -772,10 +698,6 @@ _Living section — regenerated 2026-08-12 from all cycle commits (cycle open `e
|
||||
- **deps (rollup):** dependency bumps and lockfile maintenance across the cycle — Dependabot groups and manual CVE-driven bumps ([#9081](https://github.com/diegosouzapw/OmniRoute/pull/9081), [#9082](https://github.com/diegosouzapw/OmniRoute/pull/9082), [#9427](https://github.com/diegosouzapw/OmniRoute/pull/9427), [#9458](https://github.com/diegosouzapw/OmniRoute/pull/9458), [#9459](https://github.com/diegosouzapw/OmniRoute/pull/9459), [#9461](https://github.com/diegosouzapw/OmniRoute/pull/9461), [#9462](https://github.com/diegosouzapw/OmniRoute/pull/9462), [#9472](https://github.com/diegosouzapw/OmniRoute/pull/9472))
|
||||
- **docs/chore (rollup):** documentation, refactoring and repository-hygiene upkeep across the cycle ([#8954](https://github.com/diegosouzapw/OmniRoute/pull/8954), [#8991](https://github.com/diegosouzapw/OmniRoute/pull/8991), [#9059](https://github.com/diegosouzapw/OmniRoute/pull/9059), [#9194](https://github.com/diegosouzapw/OmniRoute/pull/9194), [#9258](https://github.com/diegosouzapw/OmniRoute/pull/9258), [#9508](https://github.com/diegosouzapw/OmniRoute/pull/9508))
|
||||
- **main-branch plumbing (rollup):** work that landed on `main` between cycles and was carried into this one — the Mergify merge-queue migration and tuning (#7168, #7179, #7216, #7220, #7225), npm-publish unblock via dynamic runner + CI build reuse (#8941), CodeQL-driven e2e mock hardening (#7559), hermetic self-ref guard (#6634, #7341), coverage-baseline tightening (#7347), Dependabot alert resolutions via npm overrides (#8067, #8070), README flag/doc-link polish (#8317), and the v3.8.49 release plumbing itself (#7076)
|
||||
- **deps:** bump the development group across 1 directory with 22 updates ([#10043](https://github.com/diegosouzapw/OmniRoute/pull/10043)) — thanks @app/dependabot
|
||||
- **deps:** bump electron from 43.2.0 to 43.3.0 in /electron ([#10042](https://github.com/diegosouzapw/OmniRoute/pull/10042)) — thanks @app/dependabot
|
||||
- **maint(release):** 45 direct pushes to the release branch with no PR ref — base-red and quality-gate repairs, i18n string completion and stream/type fixes (quality ×6, i18n ×5, deps ×3, agentrouter ×3, providers ×2, release ×2, security ×2, logging ×2)
|
||||
- **maint(repo):** 29 chore/ci/test/docs commits rolled up — quality baselines, mutation registration, CI re-triggers, doc restructure and repo hygiene (#10187, #10189, #10190, #10193, #10196, #10203, #10204, #10205, #10207, #10210, #10236, #10318)
|
||||
|
||||
### 🙌 Contributors
|
||||
|
||||
@@ -784,7 +706,7 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| Contributor | PRs / Issues |
|
||||
| --- | --- |
|
||||
| [@AbdullahFageeh](https://github.com/AbdullahFageeh) | #9087 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #9790, #10118, #10222 |
|
||||
| [@adevwithpurpose](https://github.com/adevwithpurpose) | #9790 |
|
||||
| [@adrianojiu](https://github.com/adrianojiu) | #8438 |
|
||||
| [@agisota](https://github.com/agisota) | #9837 |
|
||||
| [@AgnesRiber](https://github.com/AgnesRiber) | #9718, #9976 |
|
||||
@@ -792,24 +714,21 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@AIB1TAL0S](https://github.com/AIB1TAL0S) | #9284 |
|
||||
| [@AlanSyue](https://github.com/AlanSyue) | direct commit / report |
|
||||
| [@alex-jordan547](https://github.com/alex-jordan547) | #9235, #9245, #9813 |
|
||||
| [@amartinawi](https://github.com/amartinawi) | #10090, #10091, #10092, #10097, #10101 |
|
||||
| [@AndrianBalanescu](https://github.com/AndrianBalanescu) | #8888, #8889, #8890, #8891, #8892, #8893, #8894, #8895 |
|
||||
| [@AnhLead](https://github.com/AnhLead) | #9722 |
|
||||
| [@aniketshukla1](https://github.com/aniketshukla1) | #9148 |
|
||||
| [@Anjielon](https://github.com/Anjielon) | #8776 |
|
||||
| [@apoapostolov](https://github.com/apoapostolov) | #8916 |
|
||||
| [@ARC345](https://github.com/ARC345) | #9628, #10050, #10051 |
|
||||
| [@ARC345](https://github.com/ARC345) | #9628 |
|
||||
| [@artickc](https://github.com/artickc) | #8571, #8578, #8791, #8843, #8870, #8927, #8974, #9097, #9549 |
|
||||
| [@Arul-](https://github.com/Arul-) | #9761 |
|
||||
| [@AStupidBear](https://github.com/AStupidBear) | #10180 |
|
||||
| [@b1nhm1nh](https://github.com/b1nhm1nh) | direct commit / report |
|
||||
| [@backryun](https://github.com/backryun) | #8228, #8451, #8627, #8809, #8818, #9084, #9086, #9090, #9091, #9092, #9093, #9114, #9119, #9120, #9122, #9135, #9136, #9137, #9138, #9139, #9141, #9561, #9562, #9563, #9564, #9565, #9566, #9742, #9747, #9748, #9751, #9753, #9755, #9791, #9792, #9793, #9795, #9796, #9797, #9798, #9920, #9972, #9973, #9974, #9975, #9977, #9978, #9979, #9984, #9986, #9987, #9988, #9989, #9990, #9998, #10087, #10088, #10134, #10178, #10175, #10254, #10255, #10256, #10257, #10258, #10339 |
|
||||
| [@backryun](https://github.com/backryun) | #8228, #8451, #8627, #8809, #8818, #9084, #9086, #9090, #9091, #9092, #9093, #9114, #9119, #9120, #9122, #9135, #9136, #9137, #9138, #9139, #9141, #9561, #9562, #9563, #9564, #9565, #9566, #9742, #9747, #9748, #9751, #9753, #9755, #9791, #9792, #9793, #9795, #9796, #9797, #9798, #9920, #9972, #9973, #9974, #9975, #9977, #9978, #9979, #9984, #9986, #9987, #9988, #9989, #9990, #9998, #10087, #10088, #10134, #10178 |
|
||||
| [@Benson-mk](https://github.com/Benson-mk) | #8369 |
|
||||
| [@benzntech](https://github.com/benzntech) | #9810, #9812, #9939 |
|
||||
| [@Bl0ck154](https://github.com/Bl0ck154) | #9231 |
|
||||
| [@bortolidiego](https://github.com/bortolidiego) | #10058 |
|
||||
| [@branben](https://github.com/branben) | #9940 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #9257, #9420, #9821, #9994, #10160 |
|
||||
| [@Chewji9875](https://github.com/Chewji9875) | #9257, #9420, #9821, #9994 |
|
||||
| [@chirag127](https://github.com/chirag127) | #6674 |
|
||||
| [@chloeassistant](https://github.com/chloeassistant) | #9675, #9746 |
|
||||
| [@configurowebmax](https://github.com/configurowebmax) | #8877 |
|
||||
@@ -817,9 +736,7 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@costaeder](https://github.com/costaeder) | #8626, #8629, #8630 |
|
||||
| [@csoftware-arigpt](https://github.com/csoftware-arigpt) | #3440 |
|
||||
| [@DaDecky](https://github.com/DaDecky) | direct commit / report |
|
||||
| [@DarkEsteves](https://github.com/DarkEsteves) | #10250 |
|
||||
| [@ddarkr](https://github.com/ddarkr) | #9035, #9036, #10177 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
| [@ddarkr](https://github.com/ddarkr) | #9035, #9036 |
|
||||
| [@Dingding-leo](https://github.com/Dingding-leo) | #7987, #8640, #8678, #8704, #8774, #8790, #8808, #8817 |
|
||||
| [@DinonowDev](https://github.com/DinonowDev) | #8804 |
|
||||
| [@Dragost](https://github.com/Dragost) | #8339 |
|
||||
@@ -828,31 +745,26 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@engmarcosjr](https://github.com/engmarcosjr) | #9993 |
|
||||
| [@epsilonode](https://github.com/epsilonode) | #8871 |
|
||||
| [@ervareza](https://github.com/ervareza) | direct commit / report |
|
||||
| [@excessivechaos](https://github.com/excessivechaos) | #10062, #10138 |
|
||||
| [@fajarhide](https://github.com/fajarhide) | #9191, #9198 |
|
||||
| [@fenix007](https://github.com/fenix007) | #9618 |
|
||||
| [@Gecky2102](https://github.com/Gecky2102) | #9280 |
|
||||
| [@ggdayup](https://github.com/ggdayup) | #10199 |
|
||||
| [@Gioxaa](https://github.com/Gioxaa) | #9162, #9171 |
|
||||
| [@HaoNgo232](https://github.com/HaoNgo232) | direct commit / report |
|
||||
| [@Hariprajwal](https://github.com/Hariprajwal) | #9922 |
|
||||
| [@hartmark](https://github.com/hartmark) | #9635, #9704, #9711, #9712, #9727, #9734, #9735, #9738, #9741, #9744, #9745, #9822, #10025, #10034, #10037, #10038, #10041, #10121, #10217 |
|
||||
| [@hartmark](https://github.com/hartmark) | #9635, #9704, #9711, #9712, #9727, #9734, #9735, #9738, #9741, #9744, #9745, #9822 |
|
||||
| [@Hdiaktoros](https://github.com/Hdiaktoros) | #8930 |
|
||||
| [@HectorBernstorff](https://github.com/HectorBernstorff) | direct commit / report |
|
||||
| [@HellFiveOsborn](https://github.com/HellFiveOsborn) | #9248 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #9714, #9816, #9937, #9946, #10128 |
|
||||
| [@herjarsa](https://github.com/herjarsa) | #9714, #9816, #9937, #9946 |
|
||||
| [@horacecar](https://github.com/horacecar) | #7679 |
|
||||
| [@HouMinXi](https://github.com/HouMinXi) | #8886, #8904, #8905, #8976, #8984, #9079, #9106, #9207, #9242, #9328, #9340, #9342, #9351, #9365, #9380, #9381, #9392, #9449, #9482, #9483, #9509, #9510, #9572, #9631, #9634, #9695, #9929 |
|
||||
| [@hppsc1215](https://github.com/hppsc1215) | #8970 |
|
||||
| [@hydraxman](https://github.com/hydraxman) | #10137 |
|
||||
| [@Iammilansoni](https://github.com/Iammilansoni) | #9353, #9397 |
|
||||
| [@ikelvingo](https://github.com/ikelvingo) | #8591, #8872, #9053 |
|
||||
| [@infinit-X](https://github.com/infinit-X) | #9095 |
|
||||
| [@isaaclb98](https://github.com/isaaclb98) | #9730 |
|
||||
| [@jackjinke](https://github.com/jackjinke) | #9556, #9601, #10005, #10045 |
|
||||
| [@jackjinke](https://github.com/jackjinke) | #9556, #9601, #10005 |
|
||||
| [@jax-novita](https://github.com/jax-novita) | #8913 |
|
||||
| [@jeff-alves](https://github.com/jeff-alves) | #10221 |
|
||||
| [@jeyhunfaslanov](https://github.com/jeyhunfaslanov) | #10259 |
|
||||
| [@jhordanjw123](https://github.com/jhordanjw123) | #8736 |
|
||||
| [@jktan0504](https://github.com/jktan0504) | #9025 |
|
||||
| [@joachimBrindeau](https://github.com/joachimBrindeau) | #9200 |
|
||||
@@ -883,15 +795,12 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@MohitRawat017](https://github.com/MohitRawat017) | #8718, #8772, #9605 |
|
||||
| [@Momen4444](https://github.com/Momen4444) | #9612 |
|
||||
| [@MrShitFox](https://github.com/MrShitFox) | #9826 |
|
||||
| [@mtb-ninja](https://github.com/mtb-ninja) | #10114 |
|
||||
| [@MumuTW](https://github.com/MumuTW) | #8839 |
|
||||
| [@mvanhorn](https://github.com/mvanhorn) | #9542 |
|
||||
| [@Mynacol](https://github.com/Mynacol) | #9733 |
|
||||
| [@nguyenha935](https://github.com/nguyenha935) | #9044, #9215 |
|
||||
| [@nordz0r](https://github.com/nordz0r) | #10170 |
|
||||
| [@nosolosoft](https://github.com/nosolosoft) | #8900 |
|
||||
| [@oyi77](https://github.com/oyi77) | #8299, #8752, #9158, #9818 |
|
||||
| [@pacocartones](https://github.com/pacocartones) | #10216 |
|
||||
| [@PixmaNts](https://github.com/PixmaNts) | #9432 |
|
||||
| [@pizzav-xyz](https://github.com/pizzav-xyz) | #9077 |
|
||||
| [@Poid-ZA](https://github.com/Poid-ZA) | #9467 |
|
||||
@@ -908,7 +817,7 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@ryanngit](https://github.com/ryanngit) | direct commit / report |
|
||||
| [@sadSanta-07](https://github.com/sadSanta-07) | #9938 |
|
||||
| [@SalyyS1](https://github.com/SalyyS1) | direct commit / report |
|
||||
| [@Sam280903](https://github.com/Sam280903) | #9274, #9278, #9281, #9283, #9448 |
|
||||
| [@Sam280903](https://github.com/Sam280903) | #9274, #9278, #9281 |
|
||||
| [@seakleangnhak](https://github.com/seakleangnhak) | direct commit / report |
|
||||
| [@seanford](https://github.com/seanford) | #8523 |
|
||||
| [@SemonCat](https://github.com/SemonCat) | direct commit / report |
|
||||
@@ -922,28 +831,27 @@ Thanks to everyone whose work landed in v3.8.50:
|
||||
| [@tald26](https://github.com/tald26) | #9959 |
|
||||
| [@taltas](https://github.com/taltas) | direct commit / report |
|
||||
| [@TechNickAI](https://github.com/TechNickAI) | #9251 |
|
||||
| [@TengSivtean](https://github.com/TengSivtean) | #10000, #10002, #10086 |
|
||||
| [@TengSivtean](https://github.com/TengSivtean) | #10000, #10002 |
|
||||
| [@TheFrenchGhosty](https://github.com/TheFrenchGhosty) | #9326 |
|
||||
| [@tiangao88](https://github.com/tiangao88) | #10046 |
|
||||
| [@tuxmonteiro](https://github.com/tuxmonteiro) | #9065 |
|
||||
| [@vinogradovnet](https://github.com/vinogradovnet) | #9581 |
|
||||
| [@VXNCXNX](https://github.com/VXNCXNX) | #9111, #9783 |
|
||||
| [@wgordon17](https://github.com/wgordon17) | #8909, #9233, #9441, #9619 |
|
||||
| [@Witroch4](https://github.com/Witroch4) | #8713 |
|
||||
| [@witt3rd](https://github.com/witt3rd) | #9962, #9963 |
|
||||
| [@XDayonline](https://github.com/XDayonline) | #10053 |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8757, #8869, #8876, #8883, #8906, #8931, #9021, #9027, #9452, #9042, #9316 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #8908, #9199, #9205, #9262, #9290, #9313, #9555, #9569, #9629, #9788, #9983, #10079, #10243 |
|
||||
| [@yansigit](https://github.com/yansigit) | #9834, #9911, #9917, #9921, #10065 |
|
||||
| [@xiaoyaner0201](https://github.com/xiaoyaner0201) | #8757, #8869, #8876, #8883, #8906, #8931, #9021, #9027, #9452 |
|
||||
| [@xz-dev](https://github.com/xz-dev) | #8908, #9199, #9205, #9262, #9290, #9313, #9555, #9569, #9629, #9788, #9983 |
|
||||
| [@yansigit](https://github.com/yansigit) | #9834, #9911, #9917, #9921 |
|
||||
| [@yidecode](https://github.com/yidecode) | direct commit / report |
|
||||
| [@yulinlina](https://github.com/yulinlina) | #10013 |
|
||||
| [@yutuknown](https://github.com/yutuknown) | #8999 |
|
||||
| [@zabrodschiipavel-sketch](https://github.com/zabrodschiipavel-sketch) | #9312 |
|
||||
| [@Zartharas](https://github.com/Zartharas) | #9161, #9164, #9181, #9182, #9184, #9185, #9186, #9189, #9294, #9825, #9833, #9936, #9965, #9992, #10218 |
|
||||
| [@Zartharas](https://github.com/Zartharas) | #9161, #9164, #9181, #9182, #9184, #9185, #9186, #9189, #9294, #9825, #9833, #9936, #9965, #9992 |
|
||||
| [@Zenlyte](https://github.com/Zenlyte) | #9005 |
|
||||
| [@zhiru](https://github.com/zhiru) | #9099, #9101 |
|
||||
| [@ziuus](https://github.com/ziuus) | #8912 |
|
||||
| [@zuckdorsey](https://github.com/zuckdorsey) | #9723 |
|
||||
| [@diegosouzapw](https://github.com/diegosouzapw) | maintainer |
|
||||
|
||||
---
|
||||
|
||||
|
||||
22
Dockerfile
22
Dockerfile
@@ -8,8 +8,8 @@ WORKDIR /app
|
||||
# that already have a fix published in trixie. CVEs without an upstream fix yet
|
||||
# (local-only TOCTOU, etc.) remain until the distro patches them and the image
|
||||
# is rebuilt; none are reachable from the proxy's request surface at runtime.
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get upgrade -y \
|
||||
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
|
||||
@@ -61,8 +61,8 @@ FROM base AS builder
|
||||
|
||||
# Build tools for native module compilation
|
||||
# apt-get update needed here because base's rm -rf clears the shared cache
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends python3 make g++ \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -108,7 +108,7 @@ RUN test -f package-lock.json \
|
||||
# in production (TlsClientUnavailableError, #7802). Run it explicitly here so
|
||||
# a broken/rate-limited fetch fails the BUILD loudly instead of shipping a
|
||||
# broken image.
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
|
||||
RUN --mount=type=cache,id=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) \
|
||||
@@ -158,7 +158,7 @@ ARG OMNIROUTE_BUILD_MEMORY_MB=4096
|
||||
ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_BUILD_MEMORY_MB}"
|
||||
|
||||
COPY . ./
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-next-cache,target=/app/.build/next/cache \
|
||||
RUN --mount=type=cache,id=next-cache,target=/app/.build/next/cache \
|
||||
mkdir -p /app/data \
|
||||
&& npm run build \
|
||||
&& node --input-type=module -e "import { createRequire } from 'node:module'; import { pathToFileURL } from 'node:url'; const standaloneRoot = '/app/.build/next/standalone/node_modules/'; const require = createRequire('/app/.build/next/standalone/package.json'); for (const pkg of ['@atjsh/llmlingua-2', '@huggingface/transformers', '@tensorflow/tfjs', 'js-tiktoken']) { const resolved = require.resolve(pkg); if (!resolved.startsWith(standaloneRoot)) throw new Error(pkg + ' resolved outside standalone: ' + resolved); await import(pathToFileURL(resolved).href); } const onnxRuntime = require.resolve('onnxruntime-node'); if (!onnxRuntime.startsWith(standaloneRoot)) throw new Error('onnxruntime-node resolved outside standalone: ' + onnxRuntime); await import(pathToFileURL(onnxRuntime).href);"
|
||||
@@ -262,8 +262,8 @@ COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
|
||||
# browsers land under /home/node which persists across image layers and is
|
||||
# accessible to the non-root runtime user.
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
apt-get update \
|
||||
&& node node_modules/playwright/cli.js install chromium --with-deps \
|
||||
&& chown -R node:node /home/node/.cache \
|
||||
@@ -284,15 +284,15 @@ COPY --from=builder /app/node_modules/playwright-core ./node_modules/playwright-
|
||||
COPY --from=builder /app/node_modules/playwright ./node_modules/playwright
|
||||
|
||||
# Install system dependencies required by openclaw (git+ssh references).
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt,sharing=locked \
|
||||
--mount=type=cache,id=apt-lists,target=/var/lib/apt/lists,sharing=locked \
|
||||
apt-get update \
|
||||
&& apt-get install -y --no-install-recommends git ca-certificates docker.io docker-compose \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& git config --system url."https://github.com/".insteadOf "ssh://git@github.com/"
|
||||
|
||||
# Install CLI tools globally. Separate layer from apt for better cache reuse.
|
||||
RUN --mount=type=cache,id=s/92ca8a61-c1ba-421f-a389-d48ac7258c2d-npm-cache,target=/root/.npm \
|
||||
RUN --mount=type=cache,id=npm-cache,target=/root/.npm \
|
||||
npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest
|
||||
|
||||
USER node
|
||||
|
||||
139
README.md
139
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 → 341 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. 341 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 → 339 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. 339 AI providers · 90+ free tiers · ~1.51B free tokens/mo · 19 routing strategies · $0 to start."/>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -63,10 +63,10 @@
|
||||
|
||||
| | v3.8.49 | **v3.8.50** | `v3.8.51+` |
|
||||
| ------------------------- | :-----: | :---------: | :---------: |
|
||||
| 🌐 Providers | 290 | **341** | more queued |
|
||||
| 🧠 Documented models | 1185 | **1202** | — |
|
||||
| 🌐 Providers | 291 | **339** | more queued |
|
||||
| 🧠 Documented models | 500+ | **1200+** | — |
|
||||
| 🖼️ Modality Bridge | — | 🆕 vision | video |
|
||||
| 📡 Radar free catalog | — | 🆕 opt-in | — |
|
||||
| 📡 Radar free catalog | — | — | 🔭 next |
|
||||
| ⚖️ Quota-aware scheduling | — | — | 🔭 next |
|
||||
| 📊 Quota telemetry | — | — | 🔭 next |
|
||||
|
||||
@@ -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="#-341-ai-providers--90-free">🌐 Providers</a></td>
|
||||
<td align="center"><a href="#-339-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. 341 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 341 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. 339 providers. Never stop building — OmniRoute picks the cheapest one that works. Six pillars: Never hit limits (auto-fallback across 339 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, 40+ 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 105 tools, A2A, memory, guardrails, evals — 25,000+ tests)."/>
|
||||
|
||||
<br/>
|
||||
<br/>
|
||||
@@ -449,7 +449,7 @@ All **19** strategies — mix & match per combo step:
|
||||
|
||||
### 🧱 Resilience is built in (3 independent layers)
|
||||
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 10× / API-key 15× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
<img src="./docs/diagrams/resilience-layers.svg" width="100%" alt="OmniRoute resilience — 3 independent self-healing layers, the right layer for the right failure. Layer 1 provider circuit breaker (whole provider): trips only on 408/5xx, thresholds OAuth 3× / API-key 5× / local 2×, resets 60s/30s/15s into a HALF-OPEN probe, lazy recovery; while OPEN the combo reroutes to the next provider. Layer 2 connection cooldown (one key/account): base 5s OAuth / 3s API-key, exponential ×2 backoff with anti-thundering-herd guard, 429 honors Retry-After, success clears all error state; one cooling key is skipped while sibling keys keep serving. Layer 3 model lockout (one model): per-model 429, local 404 or mode denials lock just that model — never the whole connection. Terminal states (banned, expired, credits exhausted) are for the operator, not cooldowns."/>
|
||||
|
||||
<sub>📖 [Auto-Combo Engine](docs/routing/AUTO-COMBO.md) · [Resilience Guide](docs/architecture/RESILIENCE_GUIDE.md)</sub>
|
||||
|
||||
@@ -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: 341 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: 339 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, built-in MCP server with 105 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>
|
||||
|
||||
@@ -513,27 +513,6 @@ Pix copia-e-cola:
|
||||
|
||||
<br/>
|
||||
|
||||
<p><strong>Developer notes:</strong> The project may generate a local <code>.env</code> file during npm install/postinstall for developer convenience. This file is intentionally ignored via <code>.gitignore</code> (see <code>.gitignore</code>) and must never be committed — if accidentally committed, rotate any exposed secrets and remove the file from history. See <a href="docs/DEVELOPER-ENVIRONMENT.md">docs/DEVELOPER-ENVIRONMENT.md</a> for guidance on managing local environment files and secrets.</p>
|
||||
|
||||
## 📡 OmniRoute Radar
|
||||
|
||||
The main free-tier headline remains **~1.53B tokens/month** from the documented,
|
||||
pool-deduplicated catalog above. Temporary provider signup credits can separately lift the first
|
||||
month to **~2.15B**. 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.
|
||||
|
||||
Supporters can receive the live catalog and additional provider opportunities. Its separate,
|
||||
mutable ceiling is **approximately 3B tokens/month at most**, depending on provider availability.
|
||||
That ceiling is not a guarantee: providers can change quotas, eligibility, models, or regions at
|
||||
any time.
|
||||
|
||||
Radar is opt-in and GET-only. The OmniRoute client does not upload prompts, traffic, provider
|
||||
configuration, usage telemetry, or local announcement-dismiss state. Learn about eligibility and
|
||||
the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute.online/planos)**.
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## ✨ What's New
|
||||
@@ -559,7 +538,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
- **🖼️ New endpoints** — `/v1/ocr` (Mistral OCR) and `/v1/audio/translations` (Whisper-style) round out the media surface. → [API Reference](docs/reference/API_REFERENCE.md)
|
||||
- **🎨 Image / video / audio generation** — one API for media: xAI Grok Imagine & Novita AI video, ComfyUI, 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 **341-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 **339-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)
|
||||
|
||||
@@ -612,7 +591,7 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
<b>+ also works with</b> · Kiro · Command Code · Antigravity · Windsurf · AMP · <b>any OpenAI-compatible tool</b>
|
||||
</div>
|
||||
|
||||
<sub>📖 Per-tool setup for all 34 tools (26 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
<sub>📖 Per-tool setup for all 33 tools (25 CLI Code's + 8 CLI Agents) → [`docs/reference/CLI-TOOLS.md`](docs/reference/CLI-TOOLS.md) · 🧩 OpenCode plugin → [`@omniroute/opencode-provider`](https://www.npmjs.com/package/@omniroute/opencode-provider)</sub>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -620,11 +599,11 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🌐 341 AI Providers — 90+ Free
|
||||
## 🌐 339 AI Providers — 90+ Free
|
||||
|
||||
</div>
|
||||
|
||||
> The most complete catalog of any open-source router: **341 providers**, **90+ with a free tier**, **56 free forever**.
|
||||
> The most complete catalog of any open-source router: **339 providers**, **90+ with a free tier**, **40+ free forever**.
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -704,7 +683,6 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
<tr><td align="left" nowrap>📱 <b>Android (Termux)</b></td><td align="left" nowrap><code>pkg install nodejs && npx -y omniroute</code></td><td align="left">Runs <b>on your phone</b>, 24/7, no root</td></tr>
|
||||
<tr><td align="left" nowrap>📲 <b>PWA</b></td><td align="left" nowrap>"Add to Home Screen"</td><td align="left">Fullscreen, offline, installable from browser</td></tr>
|
||||
<tr><td align="left" nowrap>🧩 <b>OpenCode plugin</b></td><td align="left" nowrap><code>@omniroute/opencode-provider</code></td><td align="left">Native OpenCode integration</td></tr>
|
||||
<tr><td align="left" nowrap>🤖 <b>VS Code Copilot Chat</b></td><td align="left" nowrap>install <b>OmniCopilot</b> extension</td><td align="left">Every OmniRoute model in the native Copilot Chat picker — stable & Insiders</td></tr>
|
||||
<tr><td align="left" nowrap>🛠️ <b>From source</b></td><td align="left" nowrap><code>npm install && npm run dev</code></td><td align="left">Hack on it, contribute</td></tr>
|
||||
</table>
|
||||
|
||||
@@ -714,33 +692,6 @@ the current catalog at **[radar.omniroute.online/planos](https://radar.omniroute
|
||||
|
||||
<div align="center">
|
||||
|
||||
### 🧩 New: OmniRoute inside VS Code's native Copilot Chat
|
||||
|
||||
</div>
|
||||
|
||||
> No new sidebar, no new chat UI — every model OmniRoute serves shows up right in the
|
||||
> **Copilot Chat model picker you already use**. Since VS Code 1.122, provider models work
|
||||
> without a GitHub sign-in or a Copilot subscription — agent mode, tool calling and vision, for
|
||||
> free.
|
||||
|
||||
Install the **[OmniCopilot](https://github.com/diegosouzapw/OmniCopilot)** extension, point it
|
||||
at your OmniRoute server (defaults to `localhost:20128`), then open Copilot Chat → model picker
|
||||
→ **Manage Models…** → **OmniRoute**.
|
||||
|
||||
<table>
|
||||
<tr><th align="left">Store</th><th align="left">Link</th><th align="left">Works with</th></tr>
|
||||
<tr><td align="left" nowrap>🧩 <b>VS Code Marketplace</b></td><td align="left"><a href="https://marketplace.visualstudio.com/items?itemName=diegosouzapw.omnicopilot">Install →</a></td><td align="left">VS Code — stable & Insiders</td></tr>
|
||||
<tr><td align="left" nowrap>🔓 <b>Open VSX Registry</b></td><td align="left"><a href="https://open-vsx.org/extension/diegosouzapw/omnicopilot">Install →</a></td><td align="left">Cursor, Windsurf, VSCodium, Theia, code-server, Gitpod, Antigravity, Kiro…</td></tr>
|
||||
</table>
|
||||
|
||||
From inside the editor: open the **Extensions** view, search **"OmniRoute"**, click **Install**
|
||||
— works the same way on both stores. Source, issues and the publishing runbook live at
|
||||
[diegosouzapw/OmniCopilot](https://github.com/diegosouzapw/OmniCopilot).
|
||||
|
||||
<br/>
|
||||
|
||||
<div align="center">
|
||||
|
||||
## 🔒 Private & Local-First
|
||||
|
||||
</div>
|
||||
@@ -797,7 +748,7 @@ Expose OmniRoute over **MCP**, **A2A**, a **REST API**, **webhooks** or a **remo
|
||||
<table>
|
||||
<tr><th align="left">Interface</th><th align="left">Endpoint / command</th><th align="left">Use it for</th></tr>
|
||||
<tr><td align="left" nowrap>🧰 <b>MCP (stdio)</b></td><td align="left" nowrap><code>omniroute --mcp</code></td><td align="left">Plug into Claude Desktop, Cursor, any MCP client</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>109 tools</b>, 33 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>🌊 <b>MCP (HTTP)</b></td><td align="left" nowrap><code>/api/mcp/stream</code></td><td align="left">Remote MCP — <b>105 tools</b>, 31 scopes, full audit trail</td></tr>
|
||||
<tr><td align="left" nowrap>📡 <b>MCP (SSE)</b></td><td align="left" nowrap><code>/api/mcp/sse</code></td><td align="left">Streaming MCP transport</td></tr>
|
||||
<tr><td align="left" nowrap>🤝 <b>A2A</b></td><td align="left" nowrap><code>/.well-known/agent.json</code></td><td align="left">Agent-to-agent, <b>JSON-RPC 2.0</b> + SSE, 6 skills</td></tr>
|
||||
<tr><td align="left" nowrap>🌐 <b>REST API</b></td><td align="left" nowrap><code>/v1/*</code></td><td align="left">OpenAI-compatible — chat, embeddings, images, audio, OCR</td></tr>
|
||||
@@ -1048,69 +999,31 @@ same process on one port, so there is no separate CLI-only package today.
|
||||
|
||||
</div>
|
||||
|
||||
## 📹 Video Guides
|
||||
|
||||
<div align="center">
|
||||
|
||||
<sub>Dados de cobertura social em 2026-08-17 · YT: 741 | TT: 137 | IG: 124 · Frescor (dias): YT 0 · TT 14 · IG 15</sub>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
<td align="center" width="320">
|
||||
<a href="https://www.instagram.com/reel/Da8ZthUPK98/">
|
||||
<img src="https://placehold.co/320x180/111827/FFFFFF?text=Instagram+Reel+%7C+nick_saraev&font=montserrat&bold=true" alt="Instagram Reel" width="300"/>
|
||||
</a><br/>
|
||||
<b>🎬 #1 — Instagram</b><br/>
|
||||
<sub>nick_saraev — 1,628,910 views</sub>
|
||||
<td align="center" width="264">
|
||||
<a href="https://www.youtube.com/watch?v=Rxdc36yUyOQ"><img src="https://img.youtube.com/vi/Rxdc36yUyOQ/maxresdefault.jpg" alt="Guia em Português" width="260"/></a><br/>
|
||||
<b>🇧🇷 Português</b><br/><sub>Guia completo</sub>
|
||||
</td>
|
||||
<td align="center" width="320">
|
||||
<a href="https://www.youtube.com/watch?v=QucgvbO5gsM">
|
||||
<img src="https://img.youtube.com/vi/QucgvbO5gsM/maxresdefault.jpg" alt="YouTube — Vaibhav Sisinty" width="300"/>
|
||||
</a><br/>
|
||||
<b>🎬 #2 — YouTube</b><br/>
|
||||
<sub>Vaibhav Sisinty — 373,084 views</sub>
|
||||
<td align="center" width="264">
|
||||
<a href="https://www.youtube.com/watch?v=CMzyOiUyEVc"><img src="https://img.youtube.com/vi/CMzyOiUyEVc/maxresdefault.jpg" alt="English Guide" width="260"/></a><br/>
|
||||
<b>🇺🇸 English</b><br/><sub>Complete walkthrough</sub>
|
||||
</td>
|
||||
<td align="center" width="320">
|
||||
<a href="https://www.youtube.com/shorts/fZIBK_4fKq8">
|
||||
<img src="https://img.youtube.com/vi/fZIBK_4fKq8/maxresdefault.jpg" alt="YouTube Shorts" width="300"/>
|
||||
</a><br/>
|
||||
<b>🎬 #3 — YouTube Shorts</b><br/>
|
||||
<sub>Nick Automates — 207,714 views</sub>
|
||||
</td>
|
||||
<td align="center" width="320">
|
||||
<a href="https://www.tiktok.com/@milesreevesai/video/7667980059189366019">
|
||||
<img src="https://placehold.co/320x180/111827/FFFFFF?text=TikTok+Top+1&font=montserrat&bold=true" alt="TikTok Thumbnail" width="300"/>
|
||||
</a><br/>
|
||||
<b>🎬 #4 — TikTok</b><br/>
|
||||
<sub>milesreevesai — 620,400 views</sub>
|
||||
</td>
|
||||
<td align="center" width="320">
|
||||
<a href="https://www.youtube.com/watch?v=LkP6ocAoQkk">
|
||||
<img src="https://img.youtube.com/vi/LkP6ocAoQkk/maxresdefault.jpg" alt="Valency Labs" width="300"/>
|
||||
</a><br/>
|
||||
<b>🎬 #5 — YouTube</b><br/>
|
||||
<sub>Valency Labs — 135,974 views</sub>
|
||||
<td align="center" width="264">
|
||||
<a href="https://www.youtube.com/watch?v=il_5Ii6v4-Y"><img src="https://img.youtube.com/vi/il_5Ii6v4-Y/maxresdefault.jpg" alt="Руководство" width="260"/></a><br/>
|
||||
<b>🇷🇺 Русский</b><br/><sub>Полное руководство</sub>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</div>
|
||||
|
||||
**Ranking completo (`v > 0`, maior alcance):**
|
||||
|
||||
| #1 | #2 | #3 | #4 | #5 |
|
||||
| -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
|
||||
| [nick_saraev — Instagram](https://www.instagram.com/reel/Da8ZthUPK98/) — **1,628,910** | [milesreevesai — TikTok](https://www.tiktok.com/@milesreevesai/video/7667980059189366019) — **620,400** | [Vaibhav Sisinty — YouTube](https://www.youtube.com/watch?v=QucgvbO5gsM) — **373,084** | [Nick Automates — YouTube Shorts](https://www.youtube.com/shorts/fZIBK_4fKq8) — **207,714** | [midudev — TikTok](https://www.tiktok.com/@midudev/video/7664636453544152342) — **177,800** |
|
||||
|
||||
| #6 | #7 | #8 | #9 | #10 |
|
||||
| ------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
|
||||
| [theopenstack — Instagram](https://www.instagram.com/reel/DaSs65mMrHk/) — **155,453** | [t.ghoush.ai — TikTok](https://www.tiktok.com/@t.ghoush.ai/video/7669497680527248656) — **152,800** | [Valency Labs — YouTube](https://www.youtube.com/watch?v=LkP6ocAoQkk) — **135,974** | [Asati — YouTube](https://www.youtube.com/watch?v=JjPtJcqwhqg) — **126,130** | [Vaibhav Sisinty — YouTube](https://www.youtube.com/watch?v=NuNDpeZYQ28) — **122,672** |
|
||||
|
||||
Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações conhecidas · 595 perfis/canais · 13+ idiomas · 13+ criadores.
|
||||
<div align="center">
|
||||
|
||||
> 🎬 **Made a video about OmniRoute?** Open an [issue](https://github.com/diegosouzapw/OmniRoute/issues/new) or [discussion](https://github.com/diegosouzapw/OmniRoute/discussions) with the link — we'll feature it here.
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
||||
@@ -1148,7 +1061,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) — 117 domain modules, 150 migrations</td></tr>
|
||||
<tr><td nowrap><b>Database</b></td><td>better-sqlite3 (SQLite, WAL journaling) + LowDB (JSON legacy) — 95 domain modules, 145 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>
|
||||
@@ -1222,7 +1135,7 @@ Métricas de validação: 1002 vídeos rastreados · 7,069,190 visualizações c
|
||||
<tr><th align="left">Document</th><th align="left">Description</th></tr>
|
||||
<tr><td nowrap><b><a href="docs/reference/API_REFERENCE.md">API Reference</a></b></td><td>All endpoints with examples</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/openapi.yaml">OpenAPI Spec</a></b></td><td>OpenAPI 3.0 specification</td></tr>
|
||||
<tr><td nowrap><b><a href="open-sse/mcp-server/README.md">MCP Server</a></b></td><td>109 MCP tools, IDE configs, Python/TS/Go clients</td></tr>
|
||||
<tr><td nowrap><b><a href="open-sse/mcp-server/README.md">MCP Server</a></b></td><td>105 MCP tools, IDE configs, Python/TS/Go clients</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/frameworks/MCP-SERVER.md">MCP Server Guide</a></b></td><td>MCP installation, transports, and tool reference</td></tr>
|
||||
<tr><td nowrap><b><a href="src/lib/a2a/README.md">A2A Server</a></b></td><td>JSON-RPC 2.0 protocol, skills, streaming, task mgmt</td></tr>
|
||||
<tr><td nowrap><b><a href="docs/frameworks/A2A-SERVER.md">A2A Server Guide</a></b></td><td>A2A agent card, tasks, skills, and streaming</td></tr>
|
||||
|
||||
@@ -208,7 +208,7 @@ _omniroute() {
|
||||
COMPREPLY=()
|
||||
cur="\${COMP_WORDS[COMP_CWORD]}"
|
||||
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
||||
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills run"
|
||||
cmds="setup doctor status logs providers config test update serve stop restart keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills"
|
||||
|
||||
case "\${prev}" in
|
||||
combo) COMPREPLY=($(compgen -W "list switch create delete show suggest" -- "\${cur}")); return 0 ;;
|
||||
@@ -242,7 +242,7 @@ function generateFishScript() {
|
||||
return `# OmniRoute CLI fish completion (dynamic)
|
||||
complete -c omniroute -f
|
||||
|
||||
set -l commands serve stop restart setup doctor status logs providers config keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills update test run
|
||||
set -l commands serve stop restart setup doctor status logs providers config keys models combo chat stream completion dashboard open backup restore health quota cache mcp a2a tunnel env memory skills update test
|
||||
|
||||
for cmd in $commands
|
||||
complete -c omniroute -n '__fish_is_nth_token 1' -a $cmd
|
||||
|
||||
@@ -5,7 +5,6 @@ import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
import { registerContexts } from "./contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function ensureBackup(configPath) {
|
||||
if (!fs.existsSync(configPath)) return;
|
||||
@@ -88,13 +87,6 @@ async function runConfigSetCommand(toolId, opts = {}) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const guard = await guardHostConfigTarget(result.configPath, {
|
||||
toolLabel: toolId,
|
||||
hostCommand: `omniroute config set ${toolId}`,
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
|
||||
const nonInteractive = opts.nonInteractive || opts.yes;
|
||||
|
||||
if (!nonInteractive) {
|
||||
@@ -279,10 +271,6 @@ export function registerConfig(program) {
|
||||
.option("--model <model>", "Model identifier (where applicable)")
|
||||
.option("--non-interactive", "Do not prompt for confirmation")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write the config even when OmniRoute runs in a container and the target is not mounted from the host"
|
||||
)
|
||||
.action(async (tool, opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runConfigSetCommand(tool, {
|
||||
@@ -318,10 +306,6 @@ export function registerConfig(program) {
|
||||
.option("--model <model>", "Model identifier")
|
||||
.option("--non-interactive", "Do not prompt for confirmation")
|
||||
.option("--yes", "Skip confirmation prompt")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write the config even when OmniRoute runs in a container and the target is not mounted from the host"
|
||||
)
|
||||
.action(async (opts, cmd) => {
|
||||
const globalOpts = cmd.parent.optsWithGlobals();
|
||||
const exitCode = await runConfigSetCommand("opencode", {
|
||||
|
||||
@@ -4,7 +4,6 @@ import { existsSync, mkdirSync, writeFileSync, copyFileSync } from "node:fs";
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { createPrompt, printSuccess, printError, printInfo, printHeading } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
/**
|
||||
* `omniroute configure <cli>` — interactive provider+model picker that writes a
|
||||
@@ -76,12 +75,6 @@ function buildCodexProfile(modelId, ctx) {
|
||||
|
||||
async function configureCodex(modelId, ctxWindow, opts) {
|
||||
const codexHome = opts.codexHome || path.join(os.homedir(), ".codex");
|
||||
const guard = await guardHostConfigTarget(codexHome, {
|
||||
toolLabel: "Codex",
|
||||
hostCommand: "omniroute configure codex",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
if (!existsSync(codexHome)) mkdirSync(codexHome, { recursive: true });
|
||||
const profile = opts.name || profileNameFromModel(modelId);
|
||||
const filePath = path.join(codexHome, `${profile}.config.toml`);
|
||||
@@ -93,7 +86,6 @@ async function configureCodex(modelId, ctxWindow, opts) {
|
||||
printInfo(`Use it: codex --profile ${profile}`);
|
||||
printInfo("Prereq: ~/.codex/config.toml must define the [model_providers.omniroute] block");
|
||||
printInfo(" (run the Codex setup once — see docs/guides/CODEX-CLI-CONFIGURATION.md).");
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runConfigureCommand(cli, opts = {}, cmd) {
|
||||
@@ -138,9 +130,7 @@ export async function runConfigureCommand(cli, opts = {}, cmd) {
|
||||
}
|
||||
const inProvider = ids.filter((id) => providerList.includes(providerOf(byId(models, id))));
|
||||
const candidates = inProvider.length ? inProvider : ids;
|
||||
printInfo(
|
||||
`Models: ${candidates.slice(0, 40).join(", ")}${candidates.length > 40 ? " …" : ""}`
|
||||
);
|
||||
printInfo(`Models: ${candidates.slice(0, 40).join(", ")}${candidates.length > 40 ? " …" : ""}`);
|
||||
chosenId = await prompt.ask("Model id");
|
||||
} finally {
|
||||
prompt.close();
|
||||
@@ -159,7 +149,7 @@ export async function runConfigureCommand(cli, opts = {}, cmd) {
|
||||
const ctxWindow = contextWindowOf(entry);
|
||||
|
||||
if (target === "codex") {
|
||||
return await configureCodex(chosenId, ctxWindow, opts);
|
||||
await configureCodex(chosenId, ctxWindow, opts);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -183,10 +173,6 @@ export function registerConfigure(program) {
|
||||
.option("--model <id>", "Model id (skips the interactive model prompt)")
|
||||
.option("--name <name>", "Profile name to write (default: derived from model)")
|
||||
.option("--codex-home <dir>", "Codex home dir (default: ~/.codex)")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write the config even when OmniRoute runs in a container and the target is not mounted from the host"
|
||||
)
|
||||
.action(async (cli, opts, cmd) => {
|
||||
const code = await runConfigureCommand(cli, opts, cmd);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -170,8 +170,8 @@ export function buildCodexEnv(baseEnv, authToken) {
|
||||
* @param {string} baseUrl OmniRoute root URL (no /v1)
|
||||
* @returns {string[]}
|
||||
*/
|
||||
export function buildCodexProviderArgs(baseUrl, model) {
|
||||
const args = [
|
||||
export function buildCodexProviderArgs(baseUrl) {
|
||||
return [
|
||||
"-c",
|
||||
tomlAssign("model_provider", "omniroute"),
|
||||
"-c",
|
||||
@@ -185,15 +185,6 @@ export function buildCodexProviderArgs(baseUrl, model) {
|
||||
"-c",
|
||||
tomlAssign("model_providers.omniroute.requires_openai_auth", false),
|
||||
];
|
||||
|
||||
if (model) {
|
||||
const normalized = String(model).trim();
|
||||
if (normalized) {
|
||||
args.push("-c", tomlAssign("model_providers.omniroute.model", normalized));
|
||||
}
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -216,7 +207,7 @@ export async function runLaunchCodexCommand(opts = {}, codexArgs = []) {
|
||||
|
||||
// Provider injected via -c (works without config.toml); then the profile (model),
|
||||
// then the user's pass-through args.
|
||||
const providerArgs = buildCodexProviderArgs(baseUrl, opts.model);
|
||||
const providerArgs = buildCodexProviderArgs(baseUrl);
|
||||
const profileArgs = opts.profile ? ["--profile", opts.profile] : [];
|
||||
const extraArgs = [...providerArgs, ...profileArgs, ...codexArgs];
|
||||
const env = buildCodexEnv(process.env, authToken);
|
||||
|
||||
@@ -190,10 +190,7 @@ export async function runLaunchCommand(opts = {}, claudeArgs = []) {
|
||||
const configDir = opts.profile
|
||||
? join(opts.claudeHome || join(os.homedir(), ".claude"), "profiles", opts.profile)
|
||||
: undefined;
|
||||
const env = buildClaudeEnv(process.env, baseUrl, authToken, {
|
||||
configDir,
|
||||
model: opts.model,
|
||||
});
|
||||
const env = buildClaudeEnv(process.env, baseUrl, authToken, { configDir });
|
||||
|
||||
const { command, shell } = await resolveClaudeSpawn(process.platform);
|
||||
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { t } from "../i18n.mjs";
|
||||
import { resolveDataDir } from "../data-dir.mjs";
|
||||
import {
|
||||
EXIT_CODES,
|
||||
emit,
|
||||
exitWith,
|
||||
printError,
|
||||
printInfo,
|
||||
printSuccess,
|
||||
printWarning,
|
||||
} from "../output.mjs";
|
||||
import { findPack } from "../../../scripts/packs/optionalPackManifest.mjs";
|
||||
import {
|
||||
findPackIndexFile,
|
||||
installPack,
|
||||
listPackStates,
|
||||
packState,
|
||||
packsRoot,
|
||||
readPackIndex,
|
||||
removePack,
|
||||
} from "../../../scripts/packs/optionalPackInstaller.mjs";
|
||||
|
||||
const CLI_DIR = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
||||
|
||||
/**
|
||||
* Locate + parse the bundle-shipped `optional-packs.index.json`.
|
||||
* Search order: explicit --source dir, then walking up from the CLI module
|
||||
* (bundle installs keep the index at the bundle root), then cwd.
|
||||
*/
|
||||
function loadIndex(sourceDir) {
|
||||
const indexFile = findPackIndexFile([sourceDir, CLI_DIR, process.cwd()]);
|
||||
if (!indexFile) return { indexFile: null, index: null };
|
||||
return { indexFile, index: readPackIndex(indexFile) };
|
||||
}
|
||||
|
||||
function stateRow(state, dataDir) {
|
||||
return {
|
||||
pack: state.name,
|
||||
packVersion: state.packVersion,
|
||||
installed: state.installed ? "yes" : "no",
|
||||
verified: state.verified === null ? "-" : state.verified ? "ok" : "FAILED",
|
||||
members: state.members.length,
|
||||
installDir: path.join(packsRoot(dataDir), state.name),
|
||||
errors: state.errors ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
const STATE_SCHEMA = [
|
||||
{ key: "pack", header: "pack" },
|
||||
{ key: "packVersion", header: "packVersion" },
|
||||
{ key: "installed", header: "installed" },
|
||||
{ key: "verified", header: "verified" },
|
||||
{ key: "members", header: "members" },
|
||||
];
|
||||
|
||||
async function run(action) {
|
||||
try {
|
||||
await action();
|
||||
} catch (err) {
|
||||
exitWith(EXIT_CODES.ERROR, err instanceof Error ? err.message : String(err));
|
||||
}
|
||||
}
|
||||
|
||||
export function registerPacks(program) {
|
||||
const packs = program.command("packs").description(t("packs.description"));
|
||||
|
||||
packs
|
||||
.command("list")
|
||||
.description(t("packs.listDescription"))
|
||||
.option("--source <dir>", t("packs.sourceOpt"))
|
||||
.action(async (opts) => {
|
||||
await run(async () => {
|
||||
const dataDir = resolveDataDir();
|
||||
const { index } = loadIndex(opts.source);
|
||||
emit(
|
||||
(await listPackStates({ dataDir, index })).map((s) => stateRow(s, dataDir)),
|
||||
opts,
|
||||
STATE_SCHEMA
|
||||
);
|
||||
if (!index) printWarning(t("packs.warnNoIndex"));
|
||||
});
|
||||
});
|
||||
|
||||
packs
|
||||
.command("install <name>")
|
||||
.description(t("packs.installDescription"))
|
||||
.option("--source <dir>", t("packs.sourceOpt"))
|
||||
.action(async (name, opts) => {
|
||||
await run(async () => {
|
||||
if (!findPack(name)) exitWith(EXIT_CODES.INVALID_ARG, t("packs.errUnknown", { name }));
|
||||
const { indexFile, index } = loadIndex(opts.source);
|
||||
if (!index) exitWith(EXIT_CODES.ERROR, t("packs.errNoIndex"));
|
||||
const dataDir = resolveDataDir();
|
||||
// The payload (tarball or extracted pack dir) lives next to the index
|
||||
// unless the caller pointed elsewhere via --source.
|
||||
await installPack(name, {
|
||||
dataDir,
|
||||
index,
|
||||
sourceDir: opts.source || path.dirname(indexFile),
|
||||
log: (msg) => printInfo(msg.replace(/^\[optional-packs\]\s*/, "")),
|
||||
});
|
||||
const installDir = path.join(packsRoot(dataDir), name);
|
||||
printSuccess(t("packs.installed", { name, dir: installDir }));
|
||||
printInfo(t("packs.restartHint"));
|
||||
emit({ pack: name, installed: "yes", verified: "ok", installDir }, opts, STATE_SCHEMA);
|
||||
});
|
||||
});
|
||||
|
||||
packs
|
||||
.command("verify [name]")
|
||||
.description(t("packs.verifyDescription"))
|
||||
.option("--source <dir>", t("packs.sourceOpt"))
|
||||
.action(async (name, opts) => {
|
||||
await run(async () => {
|
||||
if (name && !findPack(name))
|
||||
exitWith(EXIT_CODES.INVALID_ARG, t("packs.errUnknown", { name }));
|
||||
const { index } = loadIndex(opts.source);
|
||||
if (!index) exitWith(EXIT_CODES.ERROR, t("packs.errNoIndex"));
|
||||
const dataDir = resolveDataDir();
|
||||
const states = name
|
||||
? [await packState(name, { dataDir, index })]
|
||||
: await listPackStates({ dataDir, index });
|
||||
emit(
|
||||
states.map((s) => stateRow(s, dataDir)),
|
||||
opts,
|
||||
STATE_SCHEMA
|
||||
);
|
||||
const broken = states.filter((s) => s.installed && s.verified !== true);
|
||||
if (broken.length > 0) {
|
||||
for (const state of broken) {
|
||||
for (const error of state.errors ?? []) printError(`${state.name}: ${error}`);
|
||||
}
|
||||
exitWith(EXIT_CODES.ERROR, t("packs.verifyFailed", { count: broken.length }));
|
||||
}
|
||||
if (!states.some((s) => s.installed)) {
|
||||
printInfo(t("packs.noneInstalled"));
|
||||
return;
|
||||
}
|
||||
printSuccess(t("packs.verifyOk"));
|
||||
});
|
||||
});
|
||||
|
||||
packs
|
||||
.command("remove <name>")
|
||||
.description(t("packs.removeDescription"))
|
||||
.action(async (name, opts) => {
|
||||
await run(async () => {
|
||||
if (!findPack(name)) exitWith(EXIT_CODES.INVALID_ARG, t("packs.errUnknown", { name }));
|
||||
const dataDir = resolveDataDir();
|
||||
const removed = removePack(name, {
|
||||
dataDir,
|
||||
log: (msg) => printInfo(msg.replace(/^\[optional-packs\]\s*/, "")),
|
||||
});
|
||||
if (removed) {
|
||||
printSuccess(t("packs.removed", { name }));
|
||||
printInfo(t("packs.restartHint"));
|
||||
} else {
|
||||
printInfo(t("packs.notInstalled", { name }));
|
||||
}
|
||||
emit({ pack: name, installed: removed ? "no" : "no" }, opts, STATE_SCHEMA);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
import { apiFetch } from "../api.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { emit } from "../output.mjs";
|
||||
|
||||
const statusSchema = [
|
||||
{ key: "feed", header: "Feed" },
|
||||
{ key: "available", header: "Available" },
|
||||
{ key: "version", header: "Version" },
|
||||
{ key: "tier", header: "Tier" },
|
||||
{ key: "fetchedAt", header: "Fetched" },
|
||||
];
|
||||
|
||||
const syncSchema = [
|
||||
{ key: "feed", header: "Feed" },
|
||||
{ key: "status", header: "Status" },
|
||||
{ key: "version", header: "Version" },
|
||||
{ key: "reason", header: "Reason" },
|
||||
];
|
||||
|
||||
function exitCodeFor(response) {
|
||||
return Number.isInteger(response.exitCode) ? response.exitCode : response.status === 401 ? 4 : 1;
|
||||
}
|
||||
|
||||
export async function runRadarStatusCommand(opts = {}) {
|
||||
const response = await apiFetch("/api/radar/status", { acceptNotOk: true });
|
||||
if (!response.ok) return exitCodeFor(response);
|
||||
const data = await response.json();
|
||||
if (opts.output === "json") {
|
||||
emit(data, opts);
|
||||
return 0;
|
||||
}
|
||||
const rows = Object.entries(data.feeds ?? {}).map(([feed, value]) => ({
|
||||
feed,
|
||||
...(value && typeof value === "object" ? value : { available: false }),
|
||||
}));
|
||||
emit(rows, opts, statusSchema);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function runRadarSyncCommand(opts = {}) {
|
||||
const response = await apiFetch("/api/radar/sync-all", {
|
||||
method: "POST",
|
||||
body: {},
|
||||
acceptNotOk: true,
|
||||
});
|
||||
if (!response.ok) return exitCodeFor(response);
|
||||
const data = await response.json();
|
||||
if (opts.output === "json") {
|
||||
emit(data, opts);
|
||||
return 0;
|
||||
}
|
||||
const rows = Object.entries(data).map(([feed, value]) => ({
|
||||
feed,
|
||||
...(value && typeof value === "object" ? value : { status: "error" }),
|
||||
}));
|
||||
emit(rows, opts, syncSchema);
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerRadar(program) {
|
||||
const radar = program.command("radar").description(t("radar.description"));
|
||||
radar
|
||||
.command("status")
|
||||
.description(t("radar.status"))
|
||||
.action(async (_opts, command) => {
|
||||
const code = await runRadarStatusCommand(command.optsWithGlobals());
|
||||
if (code !== 0) process.exitCode = code;
|
||||
});
|
||||
radar
|
||||
.command("sync")
|
||||
.description(t("radar.sync"))
|
||||
.action(async (_opts, command) => {
|
||||
const code = await runRadarSyncCommand(command.optsWithGlobals());
|
||||
if (code !== 0) process.exitCode = code;
|
||||
});
|
||||
}
|
||||
@@ -60,7 +60,6 @@ import { registerAutostart } from "./autostart.mjs";
|
||||
import { registerRepl } from "./repl.mjs";
|
||||
import { registerLaunch } from "./launch.mjs";
|
||||
import { registerLaunchCodex } from "./launch-codex.mjs";
|
||||
import { registerRun } from "./run.mjs";
|
||||
import { registerSetupCodex } from "./setup-codex.mjs";
|
||||
import { registerSetupClaude } from "./setup-claude.mjs";
|
||||
import { registerSetupOpencode } from "./setup-opencode.mjs";
|
||||
@@ -79,8 +78,6 @@ import { registerTokens } from "./tokens.mjs";
|
||||
import { registerConfigure } from "./configure.mjs";
|
||||
import { registerApiCommands } from "../api-commands/registry.mjs";
|
||||
import { registerPlugin } from "./plugin.mjs";
|
||||
import { registerRadar } from "./radar.mjs";
|
||||
import { registerPacks } from "./packs.mjs";
|
||||
|
||||
export function registerCommands(program) {
|
||||
registerMemory(program);
|
||||
@@ -146,7 +143,6 @@ export function registerCommands(program) {
|
||||
registerRepl(program);
|
||||
registerLaunch(program);
|
||||
registerLaunchCodex(program);
|
||||
registerRun(program);
|
||||
registerSetupCodex(program);
|
||||
registerSetupClaude(program);
|
||||
registerSetupOpencode(program);
|
||||
@@ -165,6 +161,4 @@ export function registerCommands(program) {
|
||||
registerConfigure(program);
|
||||
registerApiCommands(program);
|
||||
registerPlugin(program);
|
||||
registerRadar(program);
|
||||
registerPacks(program);
|
||||
}
|
||||
|
||||
@@ -1,316 +0,0 @@
|
||||
import {
|
||||
runLaunchCommand as runLaunchClaudeCommand,
|
||||
buildClaudeEnv,
|
||||
resolveClaudeSpawn,
|
||||
quoteClaudeArgs,
|
||||
resolveLaunchTarget,
|
||||
} from "./launch.mjs";
|
||||
import {
|
||||
buildCodexEnv,
|
||||
buildCodexProviderArgs,
|
||||
resolveCodexSpawn,
|
||||
quoteCodexArgs,
|
||||
resolveCodexTarget,
|
||||
runLaunchCodexCommand as runLaunchCodexCommand,
|
||||
} from "./launch-codex.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import os from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
|
||||
const RUN_TARGETS = {
|
||||
claude: {
|
||||
aliases: ["claude", "claude-code", "cc"],
|
||||
description: "Claude Code",
|
||||
},
|
||||
codex: {
|
||||
aliases: ["codex", "openai-codex", "openai"],
|
||||
description: "OpenAI Codex CLI",
|
||||
},
|
||||
};
|
||||
|
||||
/** @type {Record<string,string>} */
|
||||
const RUN_TARGET_ALIAS_TO_CANONICAL = {
|
||||
claude: "claude",
|
||||
"claude-code": "claude",
|
||||
cc: "claude",
|
||||
codex: "codex",
|
||||
"openai-codex": "codex",
|
||||
openai: "codex",
|
||||
};
|
||||
|
||||
function isBlank(value) {
|
||||
return value === undefined || value === null || String(value).trim() === "";
|
||||
}
|
||||
|
||||
function toAuthSource(targetOpts) {
|
||||
const explicit =
|
||||
!isBlank(targetOpts.token) || !isBlank(targetOpts.apiKey) || !isBlank(targetOpts["api-key"]);
|
||||
if (explicit) return "option";
|
||||
|
||||
try {
|
||||
const context = resolveActiveContext(targetOpts.context || process.env.OMNIROUTE_CONTEXT);
|
||||
if (context && (context.accessToken || context.apiKey)) return "context";
|
||||
} catch {
|
||||
// no active context
|
||||
}
|
||||
|
||||
if (!isBlank(process.env.OMNIROUTE_API_KEY)) return "env";
|
||||
if (!isBlank(process.env.ANTHROPIC_AUTH_TOKEN)) return "env";
|
||||
return "none";
|
||||
}
|
||||
|
||||
/** Resolve supported target to canonical id. */
|
||||
export function resolveRunTarget(target) {
|
||||
const raw = String(target || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return RUN_TARGET_ALIAS_TO_CANONICAL[raw];
|
||||
}
|
||||
|
||||
export function listRunTargets() {
|
||||
return Object.keys(RUN_TARGETS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize `--provider` + `--model` into one model id.
|
||||
*
|
||||
* - when model contains a slash, keep it as-is
|
||||
* - when provider exists and model does not, prefix provider/
|
||||
*/
|
||||
export function resolveModelFromTargetOptions(targetOpts = {}) {
|
||||
const provider = String(targetOpts.provider || "").trim();
|
||||
const model = String(targetOpts.model || "").trim();
|
||||
if (!model) return "";
|
||||
if (provider && !model.includes("/")) return `${provider}/${model}`;
|
||||
return model;
|
||||
}
|
||||
|
||||
function describeCommand(command, shellMode) {
|
||||
return `${command}${shellMode ? " [shell]" : ""}`;
|
||||
}
|
||||
|
||||
function envPreview(before = {}, after = {}) {
|
||||
const beforeKeys = new Set(Object.keys(before));
|
||||
const changedOrAdded = [];
|
||||
const removed = [];
|
||||
|
||||
for (const key of Object.keys(after)) {
|
||||
if (!beforeKeys.has(key) || String(before[key]) !== String(after[key])) {
|
||||
changedOrAdded.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
for (const key of Object.keys(before)) {
|
||||
if (!(key in after)) removed.push(key);
|
||||
}
|
||||
|
||||
return {
|
||||
changedOrAdded,
|
||||
removed,
|
||||
};
|
||||
}
|
||||
|
||||
async function buildClaudePlan(rawOpts, args = []) {
|
||||
const model = resolveModelFromTargetOptions(rawOpts);
|
||||
const merged = {
|
||||
...rawOpts,
|
||||
model,
|
||||
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
|
||||
token: rawOpts.token || rawOpts.apiKey || rawOpts["api-key"],
|
||||
profile: rawOpts.profile ?? rawOpts.p,
|
||||
};
|
||||
|
||||
const { baseUrl, authToken } = resolveLaunchTarget(merged);
|
||||
const commandSpec = await resolveClaudeSpawn(process.platform);
|
||||
|
||||
const configDir = merged.profile
|
||||
? join(merged.claudeHome || join(os.homedir(), ".claude"), "profiles", merged.profile)
|
||||
: undefined;
|
||||
|
||||
const env = buildClaudeEnv(process.env, baseUrl, authToken, {
|
||||
configDir,
|
||||
model: merged.model || undefined,
|
||||
});
|
||||
const quotedArgs = quoteClaudeArgs(args, process.platform);
|
||||
|
||||
return {
|
||||
target: "claude",
|
||||
baseUrl,
|
||||
command: commandSpec.command,
|
||||
shell: commandSpec.shell,
|
||||
args: quotedArgs,
|
||||
model: merged.model || undefined,
|
||||
envDiff: envPreview(process.env, env),
|
||||
authSource: toAuthSource(merged),
|
||||
commandDisplay: describeCommand(commandSpec.command, commandSpec.shell),
|
||||
};
|
||||
}
|
||||
|
||||
async function buildCodexPlan(rawOpts, args = []) {
|
||||
const model = resolveModelFromTargetOptions(rawOpts);
|
||||
const merged = {
|
||||
...rawOpts,
|
||||
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
|
||||
model,
|
||||
profile: rawOpts.profile ?? rawOpts.p,
|
||||
};
|
||||
|
||||
const { baseUrl, authToken } = resolveCodexTarget(merged);
|
||||
const commandSpec = await resolveCodexSpawn(process.platform);
|
||||
|
||||
const providerArgs = buildCodexProviderArgs(baseUrl, merged.model || undefined);
|
||||
const profileArgs = merged.profile ? ["--profile", merged.profile] : [];
|
||||
|
||||
const env = buildCodexEnv(process.env, authToken);
|
||||
const fullArgs = [...providerArgs, ...profileArgs, ...args];
|
||||
const quotedArgs = quoteCodexArgs(fullArgs, process.platform);
|
||||
|
||||
return {
|
||||
target: "codex",
|
||||
baseUrl,
|
||||
command: commandSpec.command,
|
||||
shell: commandSpec.shell,
|
||||
args: quotedArgs,
|
||||
model: merged.model || undefined,
|
||||
envDiff: envPreview(process.env, env),
|
||||
authSource: toAuthSource(merged),
|
||||
commandDisplay: describeCommand(commandSpec.command, commandSpec.shell),
|
||||
providerArgs,
|
||||
profileArgs,
|
||||
};
|
||||
}
|
||||
|
||||
/** Build a launch plan and redact any resolved secret values. */
|
||||
export async function buildRunPlan(target, rawOpts = {}, args = []) {
|
||||
const canonical = resolveRunTarget(target);
|
||||
if (!canonical) {
|
||||
throw new Error("unsupported target");
|
||||
}
|
||||
|
||||
if (canonical === "claude") {
|
||||
return buildClaudePlan(rawOpts, args);
|
||||
}
|
||||
|
||||
return buildCodexPlan(rawOpts, args);
|
||||
}
|
||||
|
||||
function writeDryRunOutput(plan, opts = {}) {
|
||||
const output = {
|
||||
target: plan.target,
|
||||
baseUrl: plan.baseUrl,
|
||||
command: plan.command,
|
||||
args: plan.args,
|
||||
auth: {
|
||||
source: plan.authSource,
|
||||
present: plan.authSource !== "none",
|
||||
},
|
||||
shell: !!plan.shell,
|
||||
model: plan.model || null,
|
||||
env: {
|
||||
changedOrAdded: plan.envDiff.changedOrAdded,
|
||||
removed: plan.envDiff.removed,
|
||||
},
|
||||
};
|
||||
|
||||
if (opts.json) {
|
||||
console.error(`Running in dry-run mode for '${plan.target}'.`);
|
||||
console.log(JSON.stringify(output, null, 2));
|
||||
} else {
|
||||
console.log(`target: ${output.target}`);
|
||||
console.log(`baseUrl: ${output.baseUrl}`);
|
||||
console.log(`command: ${output.command}`);
|
||||
console.log(`shell: ${output.shell ? "yes" : "no"}`);
|
||||
console.log(`args: ${JSON.stringify(output.args)}`);
|
||||
console.log(`auth: ${JSON.stringify(output.auth)}`);
|
||||
console.log(`model: ${output.model || "(not set)"}`);
|
||||
if (output.env.changedOrAdded.length) {
|
||||
console.log(`env added/changed: ${output.env.changedOrAdded.join(", ")}`);
|
||||
}
|
||||
if (output.env.removed.length) {
|
||||
console.log(`env removed: ${output.env.removed.join(", ")}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildExecutionOptionsForClaude(rawOpts) {
|
||||
return {
|
||||
...rawOpts,
|
||||
model: resolveModelFromTargetOptions(rawOpts),
|
||||
token: rawOpts.token || rawOpts.apiKey || rawOpts["api-key"],
|
||||
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
|
||||
profile: rawOpts.profile || rawOpts.p,
|
||||
};
|
||||
}
|
||||
|
||||
function buildExecutionOptionsForCodex(rawOpts) {
|
||||
return {
|
||||
...rawOpts,
|
||||
model: resolveModelFromTargetOptions(rawOpts),
|
||||
apiKey: rawOpts.apiKey || rawOpts["api-key"] || rawOpts.token,
|
||||
profile: rawOpts.profile || rawOpts.p,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute or preview one target launch.
|
||||
*
|
||||
* Return code conventions:
|
||||
* 0 success, 1 runtime launch failure, 2 invalid args.
|
||||
*/
|
||||
export async function runCliTarget(target, opts = {}, args = []) {
|
||||
const canonical = resolveRunTarget(target);
|
||||
if (!canonical) {
|
||||
process.stderr.write(
|
||||
`Unsupported target '${target}'. Supported targets: ${Object.keys(RUN_TARGETS).join(", ")}\n`
|
||||
);
|
||||
return 2;
|
||||
}
|
||||
|
||||
const plan = await buildRunPlan(target, opts, args);
|
||||
|
||||
if (opts.dryRun) {
|
||||
writeDryRunOutput(plan, opts);
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (canonical === "claude") {
|
||||
return await runLaunchClaudeCommand(buildExecutionOptionsForClaude(opts), args);
|
||||
}
|
||||
|
||||
return await runLaunchCodexCommand(buildExecutionOptionsForCodex(opts), args);
|
||||
}
|
||||
|
||||
export function registerRun(program) {
|
||||
program
|
||||
.command("run <target>")
|
||||
.description(t("run.description") || "Run a supported CLI target through OmniRoute")
|
||||
.option(
|
||||
"--port <port>",
|
||||
"Local OmniRoute port (ignored when --remote or --base-url is set)",
|
||||
"20128"
|
||||
)
|
||||
.option(
|
||||
"--remote <url>",
|
||||
"Remote OmniRoute base URL (overrides --port, --base-url, and the active context)"
|
||||
)
|
||||
.option("--provider <id>", "Provider id for shorthand model composition")
|
||||
.option("--model <id>", "Model id to inject in the launched target where supported")
|
||||
.option("--profile <name>", "Profile/alias argument for target launchers that support it")
|
||||
.option("-p, --p <name>", "Alias for --profile")
|
||||
.option("--token <token>", "Authentication token for the launched target (same as --api-key)")
|
||||
.option("--api-key <key>", "Authentication token for the launched target")
|
||||
.option("--dry-run", "Show planned command and env keys without executing")
|
||||
.option("--json", "Return dry-run output in machine-readable format")
|
||||
.allowUnknownOption(true)
|
||||
.allowExcessArguments(true)
|
||||
.argument("[toolArgs...]")
|
||||
.action(async (target, toolArgs = [], opts, cmd) => {
|
||||
const globalOpts = cmd?.optsWithGlobals ? cmd.optsWithGlobals() : {};
|
||||
const merged = { ...globalOpts, ...opts };
|
||||
const code = await runCliTarget(target, merged, toolArgs);
|
||||
// process.exit() here can interrupt cleanup when the child terminates;
|
||||
// setting process.exitCode lets the event loop drain first.
|
||||
process.exitCode = code;
|
||||
});
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { platform, totalmem } from "node:os";
|
||||
import { platform, totalmem, hostname as osHostname } from "node:os";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { writePidFile, cleanupPidFile, waitForServer } from "../utils/pid.mjs";
|
||||
import { ServerSupervisor, detectMitmCrash } from "../runtime/processSupervisor.mjs";
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
isFatalInstrumentationHookFailure,
|
||||
formatAndroidInstrumentationFailureHint,
|
||||
} from "../utils/ensureAndroidCacheDir.mjs";
|
||||
import { resolveServerHost } from "../utils/serverHost.mjs";
|
||||
import {
|
||||
resolveMaxOldSpaceMb,
|
||||
calibrateHeapFallbackMb,
|
||||
@@ -208,10 +207,16 @@ export async function runServe(opts = {}) {
|
||||
PORT: String(dashboardPort),
|
||||
DASHBOARD_PORT: String(dashboardPort),
|
||||
API_PORT: String(apiPort),
|
||||
// #10492: HOSTNAME is standard shell state on Unix-like systems, not an
|
||||
// OmniRoute bind setting. The resolver only keeps its legacy meaning on
|
||||
// Windows; OMNIROUTE_SERVER_HOST is the cross-platform explicit setting.
|
||||
HOSTNAME: resolveServerHost(),
|
||||
// #6194: POSIX shells (bash/zsh) auto-set HOSTNAME to the machine name — the
|
||||
// .env loader (first-wins) can never override it. Ignore HOSTNAME when it
|
||||
// matches the OS-reported hostname (the auto-set signature). OMNIROUTE_SERVER_HOST
|
||||
// takes precedence; legacy HOSTNAME values that don't match os.hostname() are
|
||||
// still honoured for backward compatibility (e.g. Windows CMD/PowerShell users
|
||||
// who set HOSTNAME in .env where it is NOT auto-set).
|
||||
HOSTNAME:
|
||||
process.env.OMNIROUTE_SERVER_HOST ||
|
||||
(process.env.HOSTNAME !== osHostname() ? process.env.HOSTNAME : undefined) ||
|
||||
"0.0.0.0",
|
||||
NODE_ENV: "production",
|
||||
// #5238: preserve a user-set NODE_OPTIONS (incl. their own
|
||||
// `--max-old-space-size=…`) instead of clobbering it with the calibrated
|
||||
|
||||
@@ -13,7 +13,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function stripToRoot(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
@@ -26,9 +25,7 @@ export function resolveAiderTarget(opts = {}) {
|
||||
if (opts.remote) root = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
root = stripToRoot(
|
||||
resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl
|
||||
);
|
||||
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
@@ -81,7 +78,7 @@ async function fetchModelIds(apiBase, apiKey) {
|
||||
const res = await fetch(`${apiBase}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -91,16 +88,7 @@ async function fetchModelIds(apiBase, apiKey) {
|
||||
export async function runSetupAiderCommand(opts = {}) {
|
||||
const { apiBase, apiKey } = resolveAiderTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".aider.conf.yml");
|
||||
|
||||
const guard = await guardHostConfigTarget(configPath, {
|
||||
toolLabel: "Aider",
|
||||
hostCommand: "omniroute setup-aider",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".aider.conf.yml");
|
||||
|
||||
printHeading("OmniRoute → Aider (openai-compatible via LiteLLM)");
|
||||
printInfo(`OPENAI_API_BASE: ${apiBase} (no /v1 — LiteLLM appends it)`);
|
||||
@@ -119,9 +107,7 @@ export async function runSetupAiderCommand(opts = {}) {
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
printError(
|
||||
"A model is required. Pass --model <id> (the openai/ prefix is added automatically)."
|
||||
);
|
||||
printError("A model is required. Pass --model <id> (the openai/ prefix is added automatically).");
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -153,10 +139,6 @@ export function registerSetupAider(program) {
|
||||
.option("--config-path <path>", ".aider.conf.yml path (default: ~/.aider.conf.yml)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupAiderCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -20,7 +20,6 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
import {
|
||||
categoriseModel,
|
||||
isCodexCompatibleTextModel,
|
||||
@@ -148,14 +147,6 @@ export async function runSetupClaudeCommand(opts = {}) {
|
||||
printHeading("OmniRoute → Claude Code profile generator");
|
||||
printInfo(`Connecting to ${baseUrl} …`);
|
||||
|
||||
const guard = await guardHostConfigTarget(profilesRoot, {
|
||||
toolLabel: "Claude Code",
|
||||
hostCommand: "omniroute setup-claude",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
|
||||
// ── Fetch model catalog ───────────────────────────────────────────────────
|
||||
let models;
|
||||
try {
|
||||
@@ -229,10 +220,6 @@ export function registerSetupClaude(program) {
|
||||
"Comma-separated substrings — only matching model IDs (e.g. glm,kimi)"
|
||||
)
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const exitCode = await runSetupClaudeCommand(opts);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
|
||||
@@ -16,7 +16,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function stripToRoot(url) {
|
||||
let s = String(url || "").replace(/\/+$/, "");
|
||||
@@ -29,14 +28,11 @@ export function resolveClineTarget(opts = {}) {
|
||||
if (opts.remote) baseUrl = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
baseUrl = stripToRoot(
|
||||
resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl
|
||||
);
|
||||
baseUrl = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
if (!baseUrl)
|
||||
baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
|
||||
if (!baseUrl) baseUrl = `http://localhost:${Number(opts.port ?? process.env.PORT ?? 20128) || 20128}`;
|
||||
}
|
||||
let apiKey = opts.apiKey ?? opts["api-key"];
|
||||
if (!apiKey) {
|
||||
@@ -85,7 +81,7 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
const res = await fetch(`${baseUrl}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -97,14 +93,6 @@ export async function runSetupClineCommand(opts = {}) {
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const clineDir = opts.clineDir ?? opts["cline-dir"] ?? join(os.homedir(), ".cline", "data");
|
||||
|
||||
const guard = await guardHostConfigTarget(clineDir, {
|
||||
toolLabel: "Cline",
|
||||
hostCommand: "omniroute setup-cline",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
|
||||
printHeading("OmniRoute → Cline (OpenAI-compatible)");
|
||||
printInfo(`Server: ${baseUrl}`);
|
||||
|
||||
@@ -134,18 +122,7 @@ export async function runSetupClineCommand(opts = {}) {
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n── [dry-run] ${gsPath} ──`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
actModeApiProvider: globalState.actModeApiProvider,
|
||||
planModeApiProvider: globalState.planModeApiProvider,
|
||||
openAiBaseUrl: globalState.openAiBaseUrl,
|
||||
openAiModelId: globalState.openAiModelId,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
console.log(JSON.stringify({ actModeApiProvider: globalState.actModeApiProvider, planModeApiProvider: globalState.planModeApiProvider, openAiBaseUrl: globalState.openAiBaseUrl, openAiModelId: globalState.openAiModelId }, null, 2));
|
||||
console.log(`\n── [dry-run] ${secPath} ── (openAiApiKey: ${apiKey ? "set" : "sk_omniroute"})`);
|
||||
} else {
|
||||
if (!existsSync(clineDir)) mkdirSync(clineDir, { recursive: true });
|
||||
@@ -156,9 +133,7 @@ export async function runSetupClineCommand(opts = {}) {
|
||||
}
|
||||
|
||||
// The VS Code extension uses opaque globalStorage — can't be file-written.
|
||||
printInfo(
|
||||
"\nFor the Cline VS Code extension, set these in its Settings → API (OpenAI Compatible):"
|
||||
);
|
||||
printInfo("\nFor the Cline VS Code extension, set these in its Settings → API (OpenAI Compatible):");
|
||||
printInfo(` Base URL: ${baseUrl} (NOT /v1 — Cline appends it)`);
|
||||
printInfo(` API Key: <your OMNIROUTE_API_KEY>`);
|
||||
printInfo(` Model: ${model}`);
|
||||
@@ -178,10 +153,6 @@ export function registerSetupCline(program) {
|
||||
.option("--cline-dir <dir>", "Cline data dir (default: ~/.cline/data)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupClineCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -16,7 +16,6 @@ import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
|
||||
// ── Model categorisation ──────────────────────────────────────────────────────
|
||||
@@ -307,14 +306,6 @@ export async function runSetupCodexCommand(opts = {}) {
|
||||
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
|
||||
|
||||
printHeading(`OmniRoute → Codex CLI profile generator`);
|
||||
|
||||
const guard = await guardHostConfigTarget(codexHome, {
|
||||
toolLabel: "Codex",
|
||||
hostCommand: "omniroute setup-codex",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
printInfo(`Connecting to ${baseUrl} …`);
|
||||
|
||||
// ── Fetch model catalog ───────────────────────────────────────────────────
|
||||
@@ -389,10 +380,6 @@ export function registerSetupCodex(program) {
|
||||
"Comma-separated substrings — only generate profiles for matching model IDs (e.g. glm,kimi)"
|
||||
)
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const exitCode = await runSetupCodexCommand(opts);
|
||||
if (exitCode !== 0) process.exit(exitCode);
|
||||
|
||||
@@ -14,7 +14,6 @@ import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { categoriseModel } from "./setup-codex.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
const SECRET_REF = "${{ secrets.OMNIROUTE_API_KEY }}";
|
||||
|
||||
@@ -93,7 +92,7 @@ async function fetchModelIds(apiBase, apiKey) {
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch (e) {
|
||||
throw new Error(`Could not fetch models: ${e.message}`);
|
||||
@@ -103,22 +102,8 @@ async function fetchModelIds(apiBase, apiKey) {
|
||||
export async function runSetupContinueCommand(opts = {}) {
|
||||
const { apiBase, apiKey } = resolveContinueTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const only = opts.only
|
||||
? opts.only
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".continue", "config.yaml");
|
||||
|
||||
const guard = await guardHostConfigTarget(configPath, {
|
||||
toolLabel: "Continue",
|
||||
hostCommand: "omniroute setup-continue",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".continue", "config.yaml");
|
||||
|
||||
printHeading("OmniRoute → Continue (config.yaml)");
|
||||
printInfo(`apiBase: ${apiBase}`);
|
||||
@@ -165,7 +150,7 @@ export async function runSetupContinueCommand(opts = {}) {
|
||||
printInfo("\nProvide the key (config.yaml references it, not stores it):");
|
||||
printInfo(" cn CLI: export OMNIROUTE_API_KEY=... (read from your shell)");
|
||||
printInfo(" IDE: echo 'OMNIROUTE_API_KEY=...' >> ~/.continue/.env");
|
||||
printInfo('Run: cn -p "reply OK"');
|
||||
printInfo("Run: cn -p \"reply OK\"");
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -181,10 +166,6 @@ export function registerSetupContinue(program) {
|
||||
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
|
||||
.option("--config-path <path>", "config.yaml path (default: ~/.continue/config.yaml)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupContinueCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -13,7 +13,6 @@ import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { categoriseModel } from "./setup-codex.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
const API_KEY_REF = "$OMNIROUTE_API_KEY";
|
||||
|
||||
@@ -88,29 +87,15 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
});
|
||||
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
}
|
||||
|
||||
export async function runSetupCrushCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveCrushTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const only = opts.only
|
||||
? opts.only
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "crush", "crush.json");
|
||||
|
||||
const guard = await guardHostConfigTarget(configPath, {
|
||||
toolLabel: "Crush",
|
||||
hostCommand: "omniroute setup-crush",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "crush", "crush.json");
|
||||
|
||||
printHeading("OmniRoute → Crush (openai-compat)");
|
||||
printInfo(`base_url: ${baseUrl}`);
|
||||
@@ -135,17 +120,13 @@ export async function runSetupCrushCommand(opts = {}) {
|
||||
|
||||
if (dryRun) {
|
||||
console.log("\n" + (out.length > 3500 ? out.slice(0, 3500) + "\n… (truncated)" : out));
|
||||
printInfo(
|
||||
`[dry-run] ${provider.models.length} model(s) under providers.omniroute → ${configPath}`
|
||||
);
|
||||
printInfo(`[dry-run] ${provider.models.length} model(s) under providers.omniroute → ${configPath}`);
|
||||
return 0;
|
||||
}
|
||||
mkdirSync(join(configPath, ".."), { recursive: true });
|
||||
writeFileSync(configPath, out, "utf8");
|
||||
printSuccess(`Wrote ${configPath} (${provider.models.length} models under providers.omniroute)`);
|
||||
printInfo(
|
||||
"Provide the key (config references $OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=..."
|
||||
);
|
||||
printInfo("Provide the key (config references $OMNIROUTE_API_KEY): export OMNIROUTE_API_KEY=...");
|
||||
printInfo("Then run: crush");
|
||||
return 0;
|
||||
}
|
||||
@@ -160,10 +141,6 @@ export function registerSetupCrush(program) {
|
||||
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
|
||||
.option("--config-path <path>", "crush.json path (default: ~/.config/crush/crush.json)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupCrushCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
import { printHeading, printInfo, printSuccess } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { isContainerRuntime } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function ensureV1(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
@@ -72,7 +71,7 @@ async function fetchModelIds(apiBase, apiKey) {
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -85,32 +84,19 @@ export async function runSetupCursorCommand(opts = {}) {
|
||||
printInfo(`Server: ${apiBase}`);
|
||||
|
||||
let models = [];
|
||||
const only = opts.only
|
||||
? opts.only
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: null;
|
||||
const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : null;
|
||||
const ids = await fetchModelIds(apiBase, apiKey);
|
||||
models = only ? ids.filter((id) => only.some((f) => id.includes(f))) : ids;
|
||||
|
||||
console.log("\n" + buildCursorInstructions({ apiBase, models }));
|
||||
printSuccess("\nCursor is configured manually (no file written — Cursor's storage is opaque).");
|
||||
if (await isContainerRuntime()) {
|
||||
printInfo(
|
||||
"Note: this ran inside a container, so the base URL above is the container's own view. " +
|
||||
"Use the address the host reaches OmniRoute on (e.g. the published port) in Cursor's settings."
|
||||
);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerSetupCursor(program) {
|
||||
program
|
||||
.command("setup-cursor")
|
||||
.description(
|
||||
"Print the steps to point Cursor at OmniRoute (chat panel; Cursor config is not file-writable)"
|
||||
)
|
||||
.description("Print the steps to point Cursor at OmniRoute (chat panel; Cursor config is not file-writable)")
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
|
||||
@@ -14,7 +14,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function stripToRoot(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
@@ -27,9 +26,7 @@ export function resolveGooseTarget(opts = {}) {
|
||||
if (opts.remote) root = stripToRoot(opts.remote);
|
||||
else {
|
||||
try {
|
||||
root = stripToRoot(
|
||||
resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl
|
||||
);
|
||||
root = stripToRoot(resolveActiveContext(opts.context ?? process.env.OMNIROUTE_CONTEXT)?.baseUrl);
|
||||
} catch {
|
||||
/* none */
|
||||
}
|
||||
@@ -83,7 +80,7 @@ async function fetchModelIds(host, apiKey) {
|
||||
const res = await fetch(`${host}/v1/models`, { headers, signal: AbortSignal.timeout(8000) });
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -93,16 +90,7 @@ async function fetchModelIds(host, apiKey) {
|
||||
export async function runSetupGooseCommand(opts = {}) {
|
||||
const { host, apiKey } = resolveGooseTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const configPath =
|
||||
opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "goose", "config.yaml");
|
||||
|
||||
const guard = await guardHostConfigTarget(configPath, {
|
||||
toolLabel: "Goose",
|
||||
hostCommand: "omniroute setup-goose",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const configPath = opts.configPath ?? opts["config-path"] ?? join(os.homedir(), ".config", "goose", "config.yaml");
|
||||
|
||||
printHeading("OmniRoute → Goose (openai-compatible)");
|
||||
printInfo(`OPENAI_HOST: ${host} (no /v1 — Goose appends it)`);
|
||||
@@ -140,16 +128,14 @@ export async function runSetupGooseCommand(opts = {}) {
|
||||
|
||||
printInfo("\nProvide the key (Goose reads it from the env / OS keyring):");
|
||||
console.log(buildGooseEnvRecipe({ host, model }));
|
||||
printInfo('Then run: goose session (or: goose run -t "reply OK")');
|
||||
printInfo("Then run: goose session (or: goose run -t \"reply OK\")");
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function registerSetupGoose(program) {
|
||||
program
|
||||
.command("setup-goose")
|
||||
.description(
|
||||
"Configure Goose for OmniRoute: write ~/.config/goose/config.yaml + print the env recipe"
|
||||
)
|
||||
.description("Configure Goose for OmniRoute: write ~/.config/goose/config.yaml + print the env recipe")
|
||||
.option("--port <port>", "Local OmniRoute port (ignored when --remote is set)", "20128")
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
@@ -157,10 +143,6 @@ export function registerSetupGoose(program) {
|
||||
.option("--config-path <path>", "config.yaml path (default: ~/.config/goose/config.yaml)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupGooseCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -14,7 +14,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError, createPrompt } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
/** Ensure the URL ends with /v1 (Kilo appends /chat/completions to it). */
|
||||
function ensureV1(url) {
|
||||
@@ -62,11 +61,7 @@ export function buildKiloAuth(existing, { apiKey, baseUrl, model }) {
|
||||
/** Merge the kilocode.* keys into VS Code settings.json (extension surface). */
|
||||
export function buildKiloVscodeSettings(existing, { apiKey, baseUrl, model }) {
|
||||
const s = { ...(existing || {}) };
|
||||
s["kilocode.customProvider"] = {
|
||||
name: "OmniRoute",
|
||||
baseURL: baseUrl,
|
||||
apiKey: apiKey || "sk_omniroute",
|
||||
};
|
||||
s["kilocode.customProvider"] = { name: "OmniRoute", baseURL: baseUrl, apiKey: apiKey || "sk_omniroute" };
|
||||
s["kilocode.defaultModel"] = model;
|
||||
return s;
|
||||
}
|
||||
@@ -90,7 +85,7 @@ async function fetchModelIds(root, apiKey) {
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -100,22 +95,9 @@ async function fetchModelIds(root, apiKey) {
|
||||
export async function runSetupKiloCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveKiloTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const authPath =
|
||||
opts.authPath ??
|
||||
opts["auth-path"] ??
|
||||
join(os.homedir(), ".local", "share", "kilo", "auth.json");
|
||||
|
||||
const guard = await guardHostConfigTarget(authPath, {
|
||||
toolLabel: "Kilo Code",
|
||||
hostCommand: "omniroute setup-kilo",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const authPath = opts.authPath ?? opts["auth-path"] ?? join(os.homedir(), ".local", "share", "kilo", "auth.json");
|
||||
const vscodePath =
|
||||
opts.vscodeSettings ??
|
||||
opts["vscode-settings"] ??
|
||||
join(os.homedir(), ".config", "Code", "User", "settings.json");
|
||||
opts.vscodeSettings ?? opts["vscode-settings"] ?? join(os.homedir(), ".config", "Code", "User", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Kilo Code (OpenAI-compatible)");
|
||||
printInfo(`Server: ${baseUrl}`);
|
||||
@@ -134,9 +116,7 @@ export async function runSetupKiloCommand(opts = {}) {
|
||||
}
|
||||
}
|
||||
if (!model) {
|
||||
printError(
|
||||
"A model is required. Pass --model <id> (Kilo's extension has no model auto-discovery)."
|
||||
);
|
||||
printError("A model is required. Pass --model <id> (Kilo's extension has no model auto-discovery).");
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -152,19 +132,12 @@ export async function runSetupKiloCommand(opts = {}) {
|
||||
console.log(`\n── [dry-run] ${authPath} ──`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
"openai-compatible": {
|
||||
...auth["openai-compatible"],
|
||||
apiKey: apiKey ? "set" : "sk_omniroute",
|
||||
},
|
||||
},
|
||||
{ "openai-compatible": { ...auth["openai-compatible"], apiKey: apiKey ? "set" : "sk_omniroute" } },
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would merge kilocode.* keys)" : "(skipped — file absent)"}`
|
||||
);
|
||||
console.log(`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would merge kilocode.* keys)" : "(skipped — file absent)"}`);
|
||||
} else {
|
||||
mkdirSync(join(authPath, ".."), { recursive: true });
|
||||
writeFileSync(authPath, JSON.stringify(auth, null, 2) + "\n", "utf8");
|
||||
@@ -194,20 +167,10 @@ export function registerSetupKilo(program) {
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
.option("--model <id>", "Model id for Kilo (required unless picked interactively)")
|
||||
.option(
|
||||
"--auth-path <path>",
|
||||
"Kilo CLI auth.json path (default: ~/.local/share/kilo/auth.json)"
|
||||
)
|
||||
.option(
|
||||
"--vscode-settings <path>",
|
||||
"VS Code settings.json (default: ~/.config/Code/User/settings.json)"
|
||||
)
|
||||
.option("--auth-path <path>", "Kilo CLI auth.json path (default: ~/.local/share/kilo/auth.json)")
|
||||
.option("--vscode-settings <path>", "VS Code settings.json (default: ~/.config/Code/User/settings.json)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupKiloCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -30,7 +30,6 @@ import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { t } from "../i18n.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -317,13 +316,6 @@ export async function runSetupOpenCodeCommand(opts = {}) {
|
||||
printInfo(`OpenCode config dir: ${opencodeConfigDir}`);
|
||||
printInfo(`OpenCode data dir: ${opencodeDataDir}`);
|
||||
|
||||
const guard = await guardHostConfigTarget(opencodeConfigDir, {
|
||||
toolLabel: "OpenCode",
|
||||
hostCommand: "omniroute setup opencode",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
});
|
||||
if (guard !== 0) return { exitCode: guard };
|
||||
|
||||
// 1. Resolve bundled plugin
|
||||
let pluginInfo;
|
||||
try {
|
||||
@@ -428,10 +420,6 @@ export function registerSetupOpenCode(setupCommand) {
|
||||
false
|
||||
)
|
||||
.option("--non-interactive", "Do not prompt; skip the auth login step", false)
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts, cmd) => {
|
||||
// The parent `setup` command uses cmd.optsWithGlobals(); we mirror
|
||||
// that here so global flags (--json, --base-url, --api-key) still
|
||||
|
||||
@@ -14,7 +14,6 @@ import { basename, dirname } from "node:path";
|
||||
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
const ENV_KEY_REF = "{env:OMNIROUTE_API_KEY}";
|
||||
const JSON_FORMATTING_OPTIONS = { insertSpaces: true, tabSize: 2 };
|
||||
@@ -120,15 +119,6 @@ export async function runSetupOpencodeCommand(opts = {}) {
|
||||
const { resolveOpencodeConfigPath } =
|
||||
await import("../../../src/shared/services/opencodeConfigPath.ts");
|
||||
configPath = resolveOpencodeConfigPath();
|
||||
|
||||
const guard = await guardHostConfigTarget(configPath, {
|
||||
toolLabel: "OpenCode",
|
||||
hostCommand: "omniroute setup-opencode",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
|
||||
raw = await generateOpencodeConfig({
|
||||
baseUrl,
|
||||
apiKey,
|
||||
@@ -173,10 +163,6 @@ export function registerSetupOpencode(program) {
|
||||
.option("--model <id>", "Set the default top-level model (omniroute/<id>)")
|
||||
.option("--only <patterns>", "Comma-separated substrings — keep only matching model IDs")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupOpencodeCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
normalizeQwenCodeBaseUrl,
|
||||
} from "../../../src/shared/services/qwenCodeConfig.ts";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
import { createPrompt, printError, printHeading, printInfo, printSuccess } from "../io.mjs";
|
||||
|
||||
/** Resolve base URL and key from flags, active context, then local defaults. */
|
||||
@@ -103,16 +102,6 @@ export async function runSetupQwenCommand(opts = {}) {
|
||||
printHeading("OmniRoute → Qwen Code (OpenAI-compatible)");
|
||||
printInfo(`baseUrl: ${baseUrl}`);
|
||||
|
||||
for (const target of [settingsPath, envPath]) {
|
||||
const guard = await guardHostConfigTarget(target, {
|
||||
toolLabel: "Qwen Code",
|
||||
hostCommand: "omniroute setup-qwen",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
}
|
||||
|
||||
let model = String(opts.model || "").trim();
|
||||
if (!model && !opts.yes) {
|
||||
const modelIds = await fetchModelIds(baseUrl, apiKey);
|
||||
@@ -170,10 +159,6 @@ export function registerSetupQwen(program) {
|
||||
.option("--env-path <path>", "Qwen Code .env path")
|
||||
.option("--yes", "Non-interactive; requires --model")
|
||||
.option("--dry-run", "Print settings without writing files or secrets")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupQwenCommand(opts);
|
||||
if (code !== 0) process.exitCode = code;
|
||||
|
||||
@@ -16,7 +16,6 @@ import { join } from "node:path";
|
||||
import os from "node:os";
|
||||
import { printHeading, printInfo, printSuccess, printError } from "../io.mjs";
|
||||
import { resolveActiveContext } from "../contexts.mjs";
|
||||
import { guardHostConfigTarget } from "../utils/config-home-guard.mjs";
|
||||
|
||||
function ensureV1(url) {
|
||||
const s = String(url || "").replace(/\/+$/, "");
|
||||
@@ -90,7 +89,7 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
});
|
||||
if (!res.ok) return [];
|
||||
const body = await res.json();
|
||||
const list = Array.isArray(body) ? body : (body.data ?? body.models ?? []);
|
||||
const list = Array.isArray(body) ? body : body.data ?? body.models ?? [];
|
||||
return list.map((m) => (typeof m === "string" ? m : m?.id)).filter(Boolean);
|
||||
} catch {
|
||||
return [];
|
||||
@@ -100,20 +99,9 @@ async function fetchModelIds(baseUrl, apiKey) {
|
||||
export async function runSetupRooCommand(opts = {}) {
|
||||
const { baseUrl, apiKey } = resolveRooTarget(opts);
|
||||
const dryRun = Boolean(opts.dryRun ?? opts["dry-run"]);
|
||||
const importPath =
|
||||
opts.importPath ?? opts["import-path"] ?? join(os.homedir(), ".omniroute", "roo-settings.json");
|
||||
|
||||
const guard = await guardHostConfigTarget(importPath, {
|
||||
toolLabel: "Roo Code",
|
||||
hostCommand: "omniroute setup-roo",
|
||||
allowContainerWrite: Boolean(opts.allowContainerWrite ?? opts["allow-container-write"]),
|
||||
dryRun,
|
||||
});
|
||||
if (guard !== 0) return guard;
|
||||
const importPath = opts.importPath ?? opts["import-path"] ?? join(os.homedir(), ".omniroute", "roo-settings.json");
|
||||
const vscodePath =
|
||||
opts.vscodeSettings ??
|
||||
opts["vscode-settings"] ??
|
||||
join(os.homedir(), ".config", "Code", "User", "settings.json");
|
||||
opts.vscodeSettings ?? opts["vscode-settings"] ?? join(os.homedir(), ".config", "Code", "User", "settings.json");
|
||||
|
||||
printHeading("OmniRoute → Roo Code (OpenAI-compatible)");
|
||||
printInfo(`Server: ${baseUrl}`);
|
||||
@@ -142,27 +130,8 @@ export async function runSetupRooCommand(opts = {}) {
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n── [dry-run] ${importPath} ──`);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
...importDoc,
|
||||
providerProfiles: {
|
||||
...importDoc.providerProfiles,
|
||||
apiConfigs: {
|
||||
OmniRoute: {
|
||||
...importDoc.providerProfiles.apiConfigs.OmniRoute,
|
||||
openAiApiKey: apiKey ? "set" : "sk_omniroute",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
);
|
||||
console.log(
|
||||
`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would set roo-cline.autoImportSettingsPath)" : "(skipped — file absent)"}`
|
||||
);
|
||||
console.log(JSON.stringify({ ...importDoc, providerProfiles: { ...importDoc.providerProfiles, apiConfigs: { OmniRoute: { ...importDoc.providerProfiles.apiConfigs.OmniRoute, openAiApiKey: apiKey ? "set" : "sk_omniroute" } } } }, null, 2));
|
||||
console.log(`\n── [dry-run] ${vscodePath} ── ${vscodeExists ? "(would set roo-cline.autoImportSettingsPath)" : "(skipped — file absent)"}`);
|
||||
} else {
|
||||
mkdirSync(join(importPath, ".."), { recursive: true });
|
||||
writeFileSync(importPath, JSON.stringify(importDoc, null, 2) + "\n", "utf8");
|
||||
@@ -192,20 +161,10 @@ export function registerSetupRoo(program) {
|
||||
.option("--remote <url>", "Remote OmniRoute URL, e.g. http://192.168.0.15:20128")
|
||||
.option("--api-key <key>", "OmniRoute API key (defaults to OMNIROUTE_API_KEY env var)")
|
||||
.option("--model <id>", "Model id for Roo (required unless picked interactively)")
|
||||
.option(
|
||||
"--import-path <path>",
|
||||
"Roo import JSON path (default: ~/.omniroute/roo-settings.json)"
|
||||
)
|
||||
.option(
|
||||
"--vscode-settings <path>",
|
||||
"VS Code settings.json (default: ~/.config/Code/User/settings.json)"
|
||||
)
|
||||
.option("--import-path <path>", "Roo import JSON path (default: ~/.omniroute/roo-settings.json)")
|
||||
.option("--vscode-settings <path>", "VS Code settings.json (default: ~/.config/Code/User/settings.json)")
|
||||
.option("--yes", "Non-interactive: do not prompt (requires --model)")
|
||||
.option("--dry-run", "Print what would be written without touching the filesystem")
|
||||
.option(
|
||||
"--allow-container-write",
|
||||
"Write even when the target is inside a container and not mounted from the host"
|
||||
)
|
||||
.action(async (opts) => {
|
||||
const code = await runSetupRooCommand(opts);
|
||||
if (code !== 0) process.exit(code);
|
||||
|
||||
@@ -921,11 +921,6 @@
|
||||
"model": "Filter by model"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "Inspect and synchronize the local Radar catalog feeds",
|
||||
"status": "Show local Radar settings and feed cache status",
|
||||
"sync": "Synchronize catalog, referrals, offers, and Intel through the local server"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "Inspect and manage resilience mechanisms",
|
||||
"status": {
|
||||
@@ -1287,9 +1282,6 @@
|
||||
"notRunning": "OmniRoute is not reachable at {port}. Start it with 'omniroute serve'.",
|
||||
"notFound": "The 'claude' CLI was not found in PATH."
|
||||
},
|
||||
"run": {
|
||||
"description": "Launch a supported CLI target through OmniRoute"
|
||||
},
|
||||
"setupClaude": {
|
||||
"description": "Generate ~/.claude/profiles Claude Code profiles from the OmniRoute model catalog"
|
||||
},
|
||||
@@ -1307,23 +1299,5 @@
|
||||
},
|
||||
"setupCodex": {
|
||||
"description": "Generate ~/.codex profile files from OmniRoute live model catalog"
|
||||
},
|
||||
"packs": {
|
||||
"description": "Manage optional runtime packs (ML / browser automation)",
|
||||
"listDescription": "List optional packs and their install state",
|
||||
"installDescription": "Install an optional pack into DATA_DIR",
|
||||
"verifyDescription": "Verify installed packs against the shipped checksum index",
|
||||
"removeDescription": "Remove an installed optional pack",
|
||||
"sourceOpt": "Directory holding pack payloads and the pack index",
|
||||
"warnNoIndex": "optional-packs.index.json not found — install/verify are unavailable in this checkout (desktop bundles ship it)",
|
||||
"errUnknown": "unknown pack: {name}",
|
||||
"errNoIndex": "pack index not found; pass --source <dir> holding the pack payload (desktop bundles ship it next to the app)",
|
||||
"installed": "pack \"{name}\" installed and verified at {dir}",
|
||||
"restartHint": "restart the OmniRoute server (or desktop app) so the runtime picks the pack up",
|
||||
"removed": "pack \"{name}\" removed",
|
||||
"notInstalled": "pack \"{name}\" was not installed",
|
||||
"verifyOk": "all installed packs verified",
|
||||
"verifyFailed": "{count} pack(s) failed verification",
|
||||
"noneInstalled": "no optional packs installed"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,11 +918,6 @@
|
||||
"model": "Filtrar por model"
|
||||
}
|
||||
},
|
||||
"radar": {
|
||||
"description": "Inspecionar e sincronizar os feeds locais do catálogo Radar",
|
||||
"status": "Mostrar configurações locais e estado dos caches do Radar",
|
||||
"sync": "Sincronizar catálogo, indicações, ofertas e Intel pelo servidor local"
|
||||
},
|
||||
"resilience": {
|
||||
"description": "Inspecionar e gerenciar mecanismos de resiliência",
|
||||
"status": {
|
||||
@@ -1284,9 +1279,6 @@
|
||||
"notRunning": "OmniRoute não está acessível em {port}. Inicie com 'omniroute serve'.",
|
||||
"notFound": "O CLI 'claude' não foi encontrado no PATH."
|
||||
},
|
||||
"run": {
|
||||
"description": "Inicia um alvo de CLI compatível pelo OmniRoute"
|
||||
},
|
||||
"setupClaude": {
|
||||
"description": "Gera profiles do Claude Code em ~/.claude/profiles a partir do catálogo de modelos do OmniRoute"
|
||||
},
|
||||
@@ -1304,23 +1296,5 @@
|
||||
},
|
||||
"setupCodex": {
|
||||
"description": "Gera os arquivos de perfil ~/.codex a partir do catálogo de modelos ao vivo do OmniRoute"
|
||||
},
|
||||
"packs": {
|
||||
"description": "Gerencia packs opcionais de runtime (ML / automação de navegador)",
|
||||
"listDescription": "Lista os packs opcionais e seu estado de instalação",
|
||||
"installDescription": "Instala um pack opcional no DATA_DIR",
|
||||
"verifyDescription": "Verifica os packs instalados contra o índice de checksums embarcado",
|
||||
"removeDescription": "Remove um pack opcional instalado",
|
||||
"sourceOpt": "Diretório com os payloads dos packs e o índice de packs",
|
||||
"warnNoIndex": "optional-packs.index.json não encontrado — install/verify indisponíveis neste checkout (instaladores desktop o embarcam)",
|
||||
"errUnknown": "pack desconhecido: {name}",
|
||||
"errNoIndex": "índice de packs não encontrado; passe --source <dir> com o payload do pack (instaladores desktop o embarcam ao lado do app)",
|
||||
"installed": "pack \"{name}\" instalado e verificado em {dir}",
|
||||
"restartHint": "reinicie o servidor OmniRoute (ou o app desktop) para o runtime reconhecer o pack",
|
||||
"removed": "pack \"{name}\" removido",
|
||||
"notInstalled": "o pack \"{name}\" não estava instalado",
|
||||
"verifyOk": "todos os packs instalados verificados",
|
||||
"verifyFailed": "{count} pack(s) falharam na verificação",
|
||||
"noneInstalled": "nenhum pack opcional instalado"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,122 +0,0 @@
|
||||
import { printError, printInfo } from "../io.mjs";
|
||||
|
||||
/**
|
||||
* Container guard for CLI-tool config writes.
|
||||
*
|
||||
* `omniroute setup-*` writes to `~/.codex`, `~/.claude`, ... — paths that only
|
||||
* mean something on the operator's host. Run the same command inside the
|
||||
* OmniRoute container and the write "succeeds" into an ephemeral layer that no
|
||||
* host CLI ever reads and that disappears with the container. This guard turns
|
||||
* that silent no-op into an actionable refusal.
|
||||
*
|
||||
* Bind-mounted targets (the compose `host` profile) are allowed through: the
|
||||
* mount is the operator's explicit statement that the path reaches the host.
|
||||
*/
|
||||
|
||||
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
|
||||
|
||||
/** Exit code for a refused write — matches the CLI's usage-error convention. */
|
||||
export const CONTAINER_WRITE_EXIT_CODE = 2;
|
||||
|
||||
function envAllowsContainerWrite(env = process.env) {
|
||||
return TRUE_VALUES.has(
|
||||
String(env.OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE ?? "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Classify a pending config write.
|
||||
*
|
||||
* @param {string} targetPath Absolute path the command is about to write.
|
||||
* @param {{
|
||||
* toolLabel?: string,
|
||||
* hostCommand?: string,
|
||||
* allowContainerWrite?: boolean,
|
||||
* dryRun?: boolean,
|
||||
* env?: NodeJS.ProcessEnv,
|
||||
* deps?: object,
|
||||
* }} options
|
||||
* @returns {Promise<{ok: boolean, message?: string, warning?: string}>}
|
||||
*/
|
||||
export async function assertHostConfigTarget(targetPath, options = {}) {
|
||||
const {
|
||||
toolLabel,
|
||||
hostCommand,
|
||||
allowContainerWrite = false,
|
||||
dryRun = false,
|
||||
env = process.env,
|
||||
deps,
|
||||
} = options;
|
||||
|
||||
let describeContainerTarget;
|
||||
let buildContainerWriteRefusal;
|
||||
let CLI_OVERRIDE_HINT;
|
||||
try {
|
||||
// `.ts` extension is required so the published package (which ships only TS
|
||||
// source, resolved through tsx) can load these. See #2509.
|
||||
({ describeContainerTarget } = await import("../../../src/shared/utils/containerEnv.ts"));
|
||||
({ buildContainerWriteRefusal, CLI_OVERRIDE_HINT } =
|
||||
await import("../../../src/shared/utils/containerConfigGuard.ts"));
|
||||
} catch {
|
||||
// Fail open: a guard that cannot load must not block a legitimate host run.
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
const info = describeContainerTarget(targetPath, deps);
|
||||
if (!info.ephemeral) return { ok: true };
|
||||
|
||||
if (dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
warning:
|
||||
`[dry-run] ${targetPath} is inside the container and is not mounted from the host — ` +
|
||||
`a real run would be refused. See --allow-container-write.`,
|
||||
};
|
||||
}
|
||||
|
||||
if (allowContainerWrite || envAllowsContainerWrite(env)) {
|
||||
return {
|
||||
ok: true,
|
||||
warning:
|
||||
`Writing to ${targetPath} inside the container as requested — this file is lost when ` +
|
||||
`the container is recreated and host CLIs will not see it.`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false,
|
||||
message: buildContainerWriteRefusal(targetPath, {
|
||||
toolLabel,
|
||||
hostCommand,
|
||||
overrideHint: CLI_OVERRIDE_HINT,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Container check for commands that write nothing but still print host-oriented
|
||||
* instructions (setup-cursor). Fails closed to `false` so a broken import never
|
||||
* turns into a spurious warning.
|
||||
*/
|
||||
export async function isContainerRuntime(deps) {
|
||||
try {
|
||||
const { isRunningInContainer } = await import("../../../src/shared/utils/containerEnv.ts");
|
||||
return isRunningInContainer(deps);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Guard + report. Returns 0 to continue, or CONTAINER_WRITE_EXIT_CODE when the
|
||||
* caller should abort and return that code.
|
||||
*/
|
||||
export async function guardHostConfigTarget(targetPath, options = {}) {
|
||||
const result = await assertHostConfigTarget(targetPath, options);
|
||||
if (result.warning) printInfo(result.warning);
|
||||
if (result.ok) return 0;
|
||||
printError(result.message);
|
||||
return CONTAINER_WRITE_EXIT_CODE;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { hostname, platform } from "node:os";
|
||||
|
||||
/**
|
||||
* Resolve the bind host passed to the standalone Next.js server.
|
||||
*
|
||||
* HOSTNAME is a standard shell variable on Unix-like systems, so only the
|
||||
* dedicated OmniRoute variable is treated as configuration there. Windows
|
||||
* keeps the legacy HOSTNAME fallback for compatibility with existing .env
|
||||
* files, while still ignoring the OS-reported machine name.
|
||||
*
|
||||
* @param {NodeJS.ProcessEnv} [env]
|
||||
* @param {NodeJS.Platform} [runtimePlatform]
|
||||
* @param {string} [machineHostname]
|
||||
* @returns {string}
|
||||
*/
|
||||
export function resolveServerHost(
|
||||
env = process.env,
|
||||
runtimePlatform = platform(),
|
||||
machineHostname = hostname()
|
||||
) {
|
||||
if (env.OMNIROUTE_SERVER_HOST) return env.OMNIROUTE_SERVER_HOST;
|
||||
if (runtimePlatform === "win32" && env.HOSTNAME && env.HOSTNAME !== machineHostname) {
|
||||
return env.HOSTNAME;
|
||||
}
|
||||
return "0.0.0.0";
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(cli):** container-aware auto-config — `setup-*`, `omniroute configure`, `omniroute config set` and the CLI-tool config APIs now refuse to write into a containerised OmniRoute's ephemeral home (CLI exits `2`, API returns `422` with `containerEphemeralTarget`) and point at the host-CLI or bind-mount setup instead; `--allow-container-write` / `OMNIROUTE_ALLOW_CONTAINER_CONFIG_WRITE=true` opt back in. Also fixes `CLI_CONFIG_HOME` so the Compose `host` profile's `/host-home` bind mounts are honoured instead of silently falling back to the container home. (#10057)
|
||||
@@ -1 +0,0 @@
|
||||
- feat(modality-bridge): bridge Chat and Responses video parts through a strict trusted-loopback, quota-bounded FFmpeg broker; enforce HTTPS redirects/SSRF plus format, protocol, stream, pixel, frame, 50 MiB broker/remote, 36 MiB inline, and 120-second limits; propagate caller aborts; preserve the actual successful fallback model through cache/meta/headers; expose sampled latency and honest success telemetry; and ship the localized Video settings UI (#9760)
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** Persist local model display-name/enabled overrides and hide/restore tombstones, with authenticated catalog controls and feed safety precedence ([#9830](https://github.com/diegosouzapw/OmniRoute/pull/9830))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** add curated-family combo suggestions, a guided combo page, and the read-only Radar MCP catalog tool ([#9836](https://github.com/diegosouzapw/OmniRoute/pull/9836))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** add a signed live offers feed and supporter offers dashboard ([#9912](https://github.com/diegosouzapw/OmniRoute/pull/9912))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** add signed Intel insights, supporter recognition, and local Radar CLI commands ([#9923](https://github.com/diegosouzapw/OmniRoute/pull/9923))
|
||||
@@ -1 +0,0 @@
|
||||
- **feat(radar):** add a localized public news feed and dismissible dashboard launch banner, with the Radar announcement staged inactive for a separately authorized launch ([#9926](https://github.com/diegosouzapw/OmniRoute/pull/9926))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(oauth):** Claude connections created via `claude-auth/import` now send required CLI headers on the bootstrap identity call and persist a `cliUserID` device identity, fixing intermittent "Third-party apps now draw from your extra usage" 400s on otherwise valid imported subscription tokens ([#10144](https://github.com/diegosouzapw/OmniRoute/pull/10144), fixes [#10143](https://github.com/diegosouzapw/OmniRoute/issues/10143))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cursor):** Stop truncating pending tool calls on non-composer models when a KV checkpoint arrives after text but before the `exec_mcp` frame — the KV short-circuit is now gated to the composer family where it was verified ([#10215](https://github.com/diegosouzapw/OmniRoute/issues/10215)).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(responses):** repair corrupted SSE deltas for non-ASCII streams by keeping a single stream-aware `TextDecoder` (`{ stream: true }`) across `transform()` calls instead of recreating it per chunk and decoding without the `stream` flag. When a multi-byte UTF-8 character (CJK/emoji) was split across two TCP chunks — common in Chinese streaming text — the per-chunk decoder truncated it to `U+FFFD`, corrupting every delta while the rebuilt `*.done` snapshot stayed internally identical ([#10223](https://github.com/diegosouzapw/OmniRoute/issues/10223))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** FreeAIAPIKey now targets `api.freeaiapikey.com`, the host upstream names in its `410 endpoint_moved` response — every request through the provider was failing — and its catalog is resynced to the 10 models the live `/v1/models` actually serves ([#10233](https://github.com/diegosouzapw/OmniRoute/pull/10233))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** MonsterAPI's deprecation now actually applies — the flag was written as `isDeprecated`, a key no consumer or schema reads, so the provider kept rendering as healthy in the dashboard, the onboarding wizard and the generated provider reference ([#10234](https://github.com/diegosouzapw/OmniRoute/pull/10234))
|
||||
1
changelog.d/fixes/10249-dedup-hash-collision.md
Normal file
1
changelog.d/fixes/10249-dedup-hash-collision.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(open-sse): stop concurrent requests colliding on the same dedup hash for non-OpenAI target formats (#10249)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** preserve validator HTTP status codes in API-key and web connection-test results so callers can distinguish authentication, rate-limit, and upstream failures ([#10272](https://github.com/diegosouzapw/OmniRoute/pull/10272)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** tiny-budget reasoning probes (e.g. Claude Code's `/model` check sends `max_tokens: 1`) are answered with a valid truncated 200 instead of relaying the upstream 5xx "empty response content" — which previously also marked the connection unavailable and poisoned fallback/cooldown bookkeeping for a request that is only a probe ([#10281](https://github.com/diegosouzapw/OmniRoute/issues/10281)) — thanks @harkaranbrar7
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): mark gemini-3.5-flash as thinking-capable so reasoning_effort is no longer rejected with a spurious 400 (#10286)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(build):** stop Turbopack from dead-code-eliminating the Windows Tailscale branches of `src/lib/tailscaleTunnel.ts` in the published build (#10293). The release `dist` is bundled on a Linux runner, and the bundler constant-folds `process.platform`, pruning every non-Linux branch — the Windows installers shipped with no `where` lookup, an always-injected `--socket`, and a lost `net start Tailscale`/windows-default-binary path. The module now reads the platform at runtime via `os.platform()` (a function call a bundler cannot fold), so the Windows branches survive on any build machine; a vitest regression test mocking `os.platform()` → `win32` guards the anti-fold invariant (RED before, GREEN after).
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(chat-body-admission):** restore a single process-wide admission budget — heavyweight leases and queued bytes are now bounded once for the whole process instead of per session, so one session can no longer mint extra capacity or starve others; per-session fairness is preserved via round-robin dispatch ([#10110](https://github.com/diegosouzapw/OmniRoute/issues/10110))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** validate Z.ai web Local Storage sessions against the authenticated user-settings endpoint and preserve exact upstream status codes ([#10329](https://github.com/diegosouzapw/OmniRoute/pull/10329)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- fix(backend): redact client IPs and account prefixes from default proxy logs (#10348)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** `getSettings()` defaults `debugMode` to `false` — fresh installs no longer run in debug mode (persisted `debugMode: true` is preserved) ([#10372](https://github.com/diegosouzapw/OmniRoute/pull/10372) — thanks @lamchun1110)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(executors):** OpencodeExecutor and MimocodeExecutor now rotate to the next account on network exceptions (timeout, connection refused/reset) when the failed account has a dedicated proxy, not only on 429 — a throw on one account no longer fails the whole request when other accounts remain. Accounts sharing the default egress (no proxy) fail fast instead of retrying the same outage against every account. The shared rotation mechanics (`pickAccount`/`markCooldown`/`markSuccess`) are now extracted into `accountRotation.ts`, fixing an identical unconditional-cooldown gap that pre-dated this PR in MimocodeExecutor ([#10393](https://github.com/diegosouzapw/OmniRoute/pull/10393))
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** the header-budget drop warning fires once per unique dropped-header set instead of on every SSE response (warn-storm fix) ([#10397](https://github.com/diegosouzapw/OmniRoute/pull/10397) — thanks @lamchun1110)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(guardrails):** Vision Bridge now reroutes whole requests for named combos whose targets have zero vision-capable models (previously such image requests died with `capability_mismatch` when the describe path could not run), and when the fallback describe path also fails for every image the request degrades to explicit `(unavailable)` stub text instead of preserving images the combo cannot consume ([#10415](https://github.com/diegosouzapw/OmniRoute/pull/10415)) — thanks @rqzbeh
|
||||
@@ -1,2 +0,0 @@
|
||||
- **fix(antigravity):** geo-blocked egress (Google "User location is not supported") is now classified (scoped to the Google AI surfaces that emit it: Cloud Code/Gemini Code Assist, Gemini API, Vertex), cached as a 24h per-account exclusion so routing continues with other accounts, and surfaced with an actionable message; the dashboard connection test now probes the real `streamGenerateContent` model surface instead of the non-geo-restricted OAuth userinfo endpoint ([#10420](https://github.com/diegosouzapw/OmniRoute/pull/10420)) — thanks @rqzbeh
|
||||
- **fix(antigravity):** strip competing-agent identity sentences from system prompts (e.g. "You are a Claude agent, built on Anthropic's Claude Agent SDK.") that Antigravity flags and answers with 429 RESOURCE_EXHAUSTED (port of decolua/9router b566b20) ([#10420](https://github.com/diegosouzapw/OmniRoute/pull/10420)) — thanks @rqzbeh
|
||||
@@ -1,2 +0,0 @@
|
||||
- **fix(antigravity):** accounts with an empty Cloud Code `projectId` now heal themselves — failed auto-onboarding (`onboardUser`) attempts are retried after a short backoff instead of being memoized forever, so the missing Google project is created without user action on a later request or token refresh ([#10424](https://github.com/diegosouzapw/OmniRoute/pull/10424)) — thanks @rqzbeh
|
||||
- **fix(antigravity):** Google deprecated automatic project creation for standard-tier (personal) accounts — when `onboardUser` completes without a project id the account now fails fast with a clear `403 GCP_PROJECT_REQUIRED` message (no more generic 422 or delayed 429 RESOURCE_EXHAUSTED), and a manual GCP Project ID override is available in the connection editor so operators can enter their own project id ([#10424](https://github.com/diegosouzapw/OmniRoute/pull/10424)) — thanks @rqzbeh
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(usage):** read Gemini `usageMetadata` out of the antigravity `{ response: {...} }` envelope so non-streaming requests log real token usage instead of `IN 0 | OUT 0` (port of decolua/9router#59d858b) ([#10430](https://github.com/diegosouzapw/OmniRoute/pull/10430)) — thanks @rqzbeh
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(usage):** surface Gemini `cachedContentTokenCount` into `cached_tokens` for non-streaming requests so cache-hit accounting matches the OpenAI/Claude/Responses branches and the streaming path (follow-up to the #10430 envelope fix) ([#10465](https://github.com/diegosouzapw/OmniRoute/pull/10465)) — thanks @rqzbeh
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(docker):** point the bifrost sidecar at the real `ghcr.io/maximhq/bifrost:v1.6.11` tag and the cliproxyapi sidecar at the official `docker.io/eceasy/cli-proxy-api:v6.9.7` image (the previously pinned tags never existed), and complete the runtime `OMNIROUTE_BASE_PATH` subpath patch for Next 16 standalone (assetPrefix + client env + baked asset URLs) so prebuilt images respect the webpath env var ([#10482](https://github.com/diegosouzapw/OmniRoute/pull/10482))
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): stop ZWJ-obfuscating the substring "hermes" in user messages and hostnames (#10484)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(providers):** allow token-backed web sessions stored with `authType: "cookie"` to refresh their token through the provider update API ([#10518](https://github.com/diegosouzapw/OmniRoute/pull/10518)) — thanks @Zartharas
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(cli):** ignore the operating system `HOSTNAME` when choosing the server bind address on Linux and macOS, preventing startup failures when the shell hostname differs from `os.hostname()`; use `OMNIROUTE_SERVER_HOST` for explicit non-Windows configuration while preserving the legacy `HOSTNAME` fallback on Windows ([#10557](https://github.com/diegosouzapw/OmniRoute/pull/10557), closes [#10492](https://github.com/diegosouzapw/OmniRoute/issues/10492)) — thanks @redzrush101
|
||||
@@ -1 +0,0 @@
|
||||
- fix(combo): recovery hint for all_targets_skipped now points at provider quota/availability instead of 'transient, just retry' (#9303)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(providers): strip uniqueItems from Gemini tool schemas (Gemini rejects it with 400 'Unknown name uniqueItems') (#9617)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(dashboard): media mini-playgrounds authenticate via session instead of sending the masked API key as Bearer, fixing 401s under REQUIRE_API_KEY (#9935)
|
||||
@@ -1 +0,0 @@
|
||||
- fix(sse): exclude search providers from credential-health scheduler sweep to stop burning billed API queries (#9970)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api-manager):** Allowed Combos can now be restricted to zero entries: **All** is stored explicitly as `combo/*`, while **Restrict** with no selection saves an empty allowlist that denies Combo routes without blocking direct models. Existing keys are migrated to preserve their previous allow-all behavior.
|
||||
@@ -1 +0,0 @@
|
||||
- fix(ci): make `Build (advisory)` produce a signal again — pinned to a hosted runner with the swap/heap provisioning `Fast Production Build` proves sufficient, and scoped to fork PRs, which are the only ones `build.yml` cannot cover (72 of the last 100 PRs into `release/**`)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(api):** hash API keys in the `/v1/models` catalog cache Map key so heap dumps cannot leak bearer tokens (`src/app/api/v1/models/catalogCache.ts`)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** the `compression_run_telemetry` retention sweep now actually deletes expired rows. Its cutoff was computed in epoch seconds while the column stores epoch milliseconds, so `WHERE timestamp < cutoff` never matched and the table added by #6848 to bound `storage.sqlite` growth was unbounded in practice. Same unit mismatch as #9625, which corrected the sibling `domain_cost_history` sweep and missed this call site
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** database settings API no longer returns HTTP 500 on SQLite builds compiled without the optional `dbstat` virtual table (sql.js/WASM); per-table sizes degrade to 0 instead of failing the whole stats call
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(ops):** Docker HEALTHCHECK probes lightweight `/healthz` instead of `/api/monitoring/health` so a busy event loop does not mark the container Unhealthy (`scripts/dev/healthcheck.mjs`)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(sse):** keep Codex/Anthropic quota headers under the upstream forwarding budget; drop `x-codex-turn-state` and raise the 768-byte cap (`open-sse/handlers/chatCore/responseHeaders.ts`)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(models):** honor `MODELS_DEV_SYNC_ENABLED=0` as a hard kill switch over the dashboard setting so a wedged `/healthz` / UI can be recovered without HTTP (`src/lib/modelsDevSync.ts`)
|
||||
@@ -1 +0,0 @@
|
||||
- **fix(db):** the sql.js fallback now publishes the database atomically — temp file in the same directory, `fsync`, then `rename()` — instead of rewriting it in place with `writeFileSync`. sql.js has no incremental write path, so every save rewrote the whole image through an `O_TRUNC` open: for the duration of the write the on-disk database was 0 bytes and then partial, a window that scales with database size and recurs on every save. Unlike better-sqlite3 / node:sqlite, that window is not covered by SQLite's locking protocol, so it was visible to every OTHER process reading the same file (a backup job, a metrics exporter, an operator running `sqlite3`), which got `SQLITE_CORRUPT` — "database disk image is malformed" — while `PRAGMA integrity_check` passed moments later. It also closes a total-loss window: a crash mid-write used to leave the real database truncated, and now only leaves a stale temp file
|
||||
@@ -1 +0,0 @@
|
||||
- **docs(ops):** document Kubernetes probe recommendations — TCP (or soft HTTP) liveness, HTTP `/healthz` readiness, avoid `/api/monitoring/health` as kubelet liveness ([#10297](https://github.com/diegosouzapw/OmniRoute/pull/10297)) — thanks @RaviTharuma
|
||||
@@ -1 +0,0 @@
|
||||
- **chore(release):** resync the v3.8.50 provider and CLI catalogs, register the existing ChatCore mutation-coverage test, and document the local ZCode handshake identifier so the release quality gates reflect the current tree without changing ratchet baselines.
|
||||
@@ -1 +0,0 @@
|
||||
- **chore(release):** synchronize migration-count documentation and document the opt-in `PROXY_LOG_INCLUDE_IPS` logging flag so the v3.8.50 quality gates match the release tree.
|
||||
@@ -27,7 +27,6 @@
|
||||
"providers-bailian-coding-plan.spec.ts": 240,
|
||||
"providers-management.spec.ts": 324,
|
||||
"proxy-registry.smoke.spec.ts": 218,
|
||||
"radar-guided-setup.spec.ts": 177,
|
||||
"resilience-plan-alignment.spec.ts": 382,
|
||||
"responsive.spec.ts": 21,
|
||||
"search-tools-studio.spec.ts": 133,
|
||||
@@ -37,4 +36,4 @@
|
||||
"traffic-inspector.spec.ts": 212,
|
||||
"translator-friendly.spec.ts": 115,
|
||||
"visual-resilience-smoke.spec.ts": 20
|
||||
}
|
||||
}
|
||||
@@ -3319,4 +3319,4 @@
|
||||
"count": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -247,7 +247,7 @@ services:
|
||||
# fall back to the chatCore path with zero code changes. See
|
||||
# docs/architecture/cluster-decisions.md for the activation plan.
|
||||
bifrost:
|
||||
image: ghcr.io/maximhq/bifrost:v1.6.11
|
||||
image: ghcr.io/maximhq/bifrost:1.5.21
|
||||
container_name: omniroute-bifrost
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
@@ -266,12 +266,9 @@ services:
|
||||
- bifrost
|
||||
|
||||
# ── Profile: cliproxyapi (CLIProxyAPI as sidecar) ─────────────────
|
||||
# Official pre-built image lives on Docker Hub (eceasy/cli-proxy-api);
|
||||
# ghcr.io/router-for-me/* is not publicly pullable. v6.9.7 is the pinned
|
||||
# version the sidecar integration (port 8317, /v1/models healthcheck) targets.
|
||||
cliproxyapi:
|
||||
container_name: cliproxyapi
|
||||
image: docker.io/eceasy/cli-proxy-api:v6.9.7
|
||||
image: ghcr.io/router-for-me/cliproxyapi:v6.9.7
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "${CLIPROXYAPI_PORT:-8317}:${CLIPROXYAPI_PORT:-8317}"
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Developer environment notes
|
||||
|
||||
This page explains the project's local `.env` behavior and how to handle environment files and secrets when developing OmniRoute.
|
||||
|
||||
## .env postinstall behavior
|
||||
|
||||
The project may generate a local `.env` file during `npm install` / `postinstall` for developer convenience. This file is intended only for local development and testing and must never be committed to version control.
|
||||
|
||||
Key points:
|
||||
|
||||
- The repository's `.gitignore` already ignores `.env*` files (see the `.gitignore` entry). Do not remove or alter that rule unless you deliberately intend to commit a specific example file and have a documented process for it.
|
||||
- If a real secret is accidentally committed to the repo, rotate/revoke the credential immediately and remove it from the repository history (for example, using `git filter-repo` or an equivalent remediation workflow). Contact the security/contact owner if you need help.
|
||||
- For CI and production, use the CI secrets or a secrets manager (GitHub Actions Secrets, Azure Key Vault, HashiCorp Vault, etc.) rather than committing secrets to files.
|
||||
|
||||
## Recommended local workflow
|
||||
|
||||
- Keep `.env` in your local workspace only. Use `.env.example` (already tracked) to document required variables and acceptable example values.
|
||||
- When running tests locally that require secret-like values, prefer synthetic placeholders or runtime-generated ephemeral keys rather than real credentials.
|
||||
- Add a short comment in tests that use placeholders so reviewers understand the fixture is synthetic.
|
||||
|
||||
## Scanner notes
|
||||
|
||||
- Some compiled or binary assets (e.g., embedded base64 WASM blobs) can contain ASCII substrings that look like credentials and may trigger text-based secret scanners. If these assets are legitimate, either mark them in the scanner's allowlist or exclude the directories in the scanner config.
|
||||
|
||||
## If you find a leak
|
||||
|
||||
1. Rotate/revoke the key immediately.
|
||||
2. Remove the secret from the history and force-push a cleaned branch if necessary.
|
||||
3. Notify maintainers and follow your org's incident response checklist.
|
||||
@@ -205,10 +205,10 @@ Runs on pull requests only.
|
||||
|
||||
Runs after `build`. Blocks merge on failure.
|
||||
|
||||
| Suite | Validates | Blocking |
|
||||
| ---------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `test:vitest` | MCP server (109 tools), autoCombo, cache — vitest runner | Yes |
|
||||
| `test:vitest:ui` | UI component tests — vitest runner | **Blocking** — pre-existing failures are explicitly excluded in `vitest.config.ts`; new failures fail the job |
|
||||
| Suite | Validates | Blocking |
|
||||
| ---------------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `test:vitest` | MCP server (105 tools), autoCombo, cache — vitest runner | Yes |
|
||||
| `test:vitest:ui` | UI component tests — vitest runner | **Blocking** — pre-existing failures are explicitly excluded in `vitest.config.ts`; new failures fail the job |
|
||||
|
||||
### Nightly workflows (scheduled, advisory)
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ OmniRoute/
|
||||
| **.gitleaks.toml** | gitleaks secret-scan ruleset |
|
||||
| **.zizmor.yml** | zizmor GitHub-Actions security-lint config |
|
||||
| **socket.yml** | Socket.dev supply-chain config |
|
||||
| **news.json** | Localized v2 announcement feed; Radar launch item ships inactive |
|
||||
| **news.json** | In-app release-notes feed (read by `src/shared/utils/releaseNotes.ts`) |
|
||||
| **flake.nix** / **flake.lock** | Nix dev-shell definition + lock |
|
||||
| **.env** | Local secrets (gitignored — generated from `.env.example`) |
|
||||
|
||||
@@ -182,7 +182,7 @@ src/
|
||||
| `compliance/` | Audit log + provider audit — see `docs/security/COMPLIANCE.md` |
|
||||
| `compression/` | Compression engine glue (engines live in `open-sse/services/compression/`) |
|
||||
| `config/` | Runtime config helpers |
|
||||
| `db/` | 95+ domain DB modules + 148 migrations (always go through here for SQLite) |
|
||||
| `db/` | 95+ domain DB modules + 144 migrations (always go through here for SQLite) |
|
||||
| `quota/` | Quota Sharing Engine: `dimensions.ts` (types/Zod), `types.ts` (QuotaStore interface), `sqliteQuotaStore.ts`, `redisQuotaStore.ts`, `storeFactory.ts`, `fairShare.ts`, `burnRate.ts`, `planResolver.ts`, `planRegistry.ts`, `saturationSignals.ts`, `enforce.ts`, `spendRecorder.ts` — see `docs/routing/QUOTA_SHARE.md` |
|
||||
| `radar/` | Radar free-model catalog client: `feedSchema.ts`, `pinnedKeys.ts`, `verify.ts`, `sync.ts`, `applyFeed.ts`, `index.ts` (`getRadarCatalog()`) — see `docs/frameworks/RADAR.md` |
|
||||
| `display/` | UI formatting helpers (cost, latency, etc.) |
|
||||
@@ -196,7 +196,7 @@ src/
|
||||
| `memory/vectorStore.ts` | sqlite-vec v0.1.9 wrapper — KNN brute-force + hybrid RRF (FTS5 + vector, k=60). Lazy-init, degrades gracefully when sqlite-vec unavailable. (plan 21) |
|
||||
| `memory/reindex.ts` | `runReindexBatch()` — processes memories with `needs_reindex=1` in background; called by `POST /api/memory/reindex` and lazy-backfill path. (plan 21) |
|
||||
| `monitoring/` | Health checks, metrics emission |
|
||||
| `oauth/` | OAuth/import flows for 22 provider modules (agy, antigravity, claude, cline, codebuddy-cn, codex, cursor, devin-desktop, ghe-copilot, github, gitlab-duo, grok-cli-oauth, grok-cli, kilocode, kimi-coding, kiro, qoder, raycast, trae, xai-oauth, zed-hosted, zed) |
|
||||
| `oauth/` | OAuth/import flows for 22 provider modules (agy, antigravity, claude, cline, codebuddy-cn, codex, cursor, devin-desktop, ghe-copilot, github, gitlab-duo, grok-cli-oauth, grok-cli, kilocode, kimi-coding, kiro, qoder, raycast, trae, xai-oauth, zed-hosted, zed) |
|
||||
| `plugins/` | Plugin registry |
|
||||
| `promptCache/` | Anthropic-style prompt cache breakpoints |
|
||||
| `skills/` | Skills framework (built-in + marketplace + SkillsSH) — see `docs/frameworks/SKILLS.md` |
|
||||
@@ -209,7 +209,7 @@ src/
|
||||
| `cacheLayer.ts`, `idempotencyLayer.ts` | Request caching + idempotency |
|
||||
| (~30 more top-level files) | Specialized helpers (logEnv, modelsDevSync, piiSanitizer, etc.) |
|
||||
|
||||
### `src/db/` — Database (117 modules + 148 migrations)
|
||||
### `src/db/` — Database (117 modules + 144 migrations)
|
||||
|
||||
| Subdir | Purpose |
|
||||
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
@@ -256,7 +256,6 @@ src/
|
||||
| `utils/circuitBreaker.ts` | Provider circuit breaker (see `docs/architecture/RESILIENCE_GUIDE.md`) |
|
||||
| `utils/apiAuth.ts` | API key validation, scope checking |
|
||||
| `utils/fetchTimeout.ts` | Timeout/abort wrappers for upstream fetch |
|
||||
| `utils/releaseNotes.ts` | Closed v2/legacy announcement parser, localization and ID dismissal |
|
||||
|
||||
---
|
||||
|
||||
@@ -271,7 +270,7 @@ open-sse/
|
||||
├── translator/ # Format converters (9 request, 9 response, 9 helpers)
|
||||
├── transformer/ # Responses API ↔ Chat Completions (TransformStream)
|
||||
├── services/ # ~80+ service modules (combo, accountFallback, autoCombo, reasoningCache, claude code/chatgpt stealth, modelDeprecation, taskAwareRouter, workflowFSM, etc.)
|
||||
├── mcp-server/ # MCP server (109 tools, 3 transports, 33 scopes)
|
||||
├── mcp-server/ # MCP server (105 tools, 3 transports, 31 scopes)
|
||||
├── config/ # Provider/model registries, header config, model aliases
|
||||
├── utils/ # TLS client, proxy fetch/dispatcher, network helpers
|
||||
├── index.ts # Workspace entry
|
||||
@@ -407,7 +406,7 @@ open-sse/
|
||||
|
||||
| Doc | Purpose |
|
||||
| -------------------------- | ------------------------------------------------------------------- |
|
||||
| `MCP-SERVER.md` | MCP server: 109 tools, 3 transports, 33 scopes, REST endpoints |
|
||||
| `MCP-SERVER.md` | MCP server: 105 tools, 3 transports, 31 scopes, REST endpoints |
|
||||
| `A2A-SERVER.md` | A2A v0.3: JSON-RPC, 6 skills, REST helpers, agent card |
|
||||
| `AGENT_PROTOCOLS_GUIDE.md` | Unified guide: A2A vs ACP vs Cloud Agents |
|
||||
| `CLOUD_AGENT.md` | Codex Cloud / Devin / Jules orchestration |
|
||||
|
||||
@@ -330,75 +330,32 @@ excludeMarkers, defaultRetryAfterMs}`), matched via `applyStatusRestatement()`.
|
||||
|
||||
Permanent errors (agentrouter's `无权访问模型` — no access to this model) are
|
||||
NEVER restated: `excludeMarkers` vetoes the rule even when `textMarkers` hit,
|
||||
so the error keeps its original status and nothing retries it forever. The
|
||||
matching provider classification rule
|
||||
(`agentrouter-model-access-denied` in `open-sse/config/providerErrorRules.ts`:
|
||||
`reason: "auth_error"`, `scope: "model"`, a `6h` declared base cooldown) is
|
||||
consulted by `checkFallbackError` (`open-sse/services/accountFallback.ts`)
|
||||
*before* the generic apikey-category `FORBIDDEN` early-return, gated on
|
||||
`honorsRuleLockScope(provider)` (#10334 — currently agentrouter-exclusive via
|
||||
the `HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist in
|
||||
`providerErrorRules.ts`). The rule's declared 6h cooldown flows through as
|
||||
`fallbackResult.baseCooldownMs`, but it still feeds the pre-existing
|
||||
per-model-quota lockout path (`lockModelIfPerModelQuota()` /
|
||||
`recordModelLockoutFailure()`, unchanged by #10334 except for the cooldown
|
||||
source): it is clamped down to the operator's `mlSettings.maxCooldownMs`
|
||||
(default `1_800_000ms` / 30min), like every other model lockout, and the
|
||||
*persisted lockout reason* stays the pre-existing hardcoded `"forbidden"`,
|
||||
not the rule's `"auth_error"` — only the cooldown duration is honored
|
||||
end-to-end, not the reason string. The connection itself stays active;
|
||||
sibling models on the same connection are unaffected.
|
||||
so the error keeps its original status and nothing retries it forever. A
|
||||
separate provider classification rule
|
||||
(`agentrouter-model-access-denied` in `open-sse/config/providerErrorRules.ts`)
|
||||
declares an `auth_error`/scope-`model` match for this text, but it does not
|
||||
fire on the live production path today: the rule only matches `status ===
|
||||
403`, and `checkFallbackError`'s apikey-category `FORBIDDEN` branch
|
||||
(`open-sse/services/accountFallback.ts`) returns early for a plain 403
|
||||
*before* the provider-rule lookup ever runs. In practice a `无权访问模型` 403
|
||||
is handled the same way as the base apikey-provider 403 path (see Connection
|
||||
Cooldown, §2), not as a 6h model lockout. The rule still exists as a
|
||||
declarative classification consumable by future callers of `classifyError`
|
||||
with context — wiring it into the production `checkFallbackError` path is
|
||||
tracked as a follow-up, not yet done.
|
||||
|
||||
Restated quota errors (`额度不足`) reach a provider rule in production
|
||||
(`agentrouter-user-quota-exhausted`: `reason: "quota_exhausted"`, `scope:
|
||||
"connection"`, no declared cooldown of its own — the persistence layer's
|
||||
scaled backoff default applies). Since #10334, `scope` on
|
||||
`ProviderErrorRuleMatch` IS consumed end-to-end, but **only** for providers in
|
||||
the `HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist (`providerErrorRules.ts` —
|
||||
today only `"agentrouter"`, gated via `honorsRuleLockScope()`). For every
|
||||
other provider `scope` remains informational, exactly as before #10334.
|
||||
`checkFallbackError` surfaces the matched rule's scope as
|
||||
`fallbackResult.ruleScope`; `isAgentrouterConnectionQuotaScope()`
|
||||
(`src/sse/services/auth.ts`) is the shared guard that confirms a
|
||||
`ruleScope` is genuinely safe to honor as a connection-wide, self-recovering
|
||||
signal (scope `"connection"`, reason `quota_exhausted`, never `permanent`,
|
||||
never `creditsExhausted` — a defense against a future rule pairing scope
|
||||
`"connection"` with a permanent account state). Two consumers call it:
|
||||
|
||||
- **Persistence** (`markAccountUnavailable()`, `src/sse/services/auth.ts`):
|
||||
instead of falling into the passthrough-provider **per-model** lockout
|
||||
branch (agentrouter is `passthroughModels: true` → `hasPerModelQuota()`
|
||||
returns `true`), it applies a **temporary connection cooldown** —
|
||||
`testStatus: "unavailable"` + `rateLimitedUntil`, never a terminal status
|
||||
(`credits_exhausted`/`banned`/`expired`) — so the connection self-recovers
|
||||
once the cooldown lapses instead of requiring a manual credential reset.
|
||||
Skipped for connections with `disableCooling: true` (#2997): that opt-out
|
||||
falls through to the per-model lockout instead (a documented trade-off —
|
||||
see the code comment above the branch).
|
||||
- **Same-request combo routing** (`applyComboTargetExhaustion()`,
|
||||
`open-sse/services/combo/targetExhaustion.ts`): the same guard marks the
|
||||
connection into the in-memory `exhaustedConnections` set, keyed
|
||||
`${provider}:${connectionId}`. This only skips a remaining SAME-REQUEST
|
||||
target that *itself already carries that exact `connectionId`* on its own
|
||||
target object (`getExhaustedTargetSkipReason()`,
|
||||
`open-sse/services/combo/comboPredicates.ts`, `if (provider &&
|
||||
connectionId)` before the `exhaustedConnections` lookup) — a plain
|
||||
model-list combo, where sibling targets carry no pinned `connectionId` of
|
||||
their own and one is only resolved per-dispatch from the response's
|
||||
`X-OmniRoute-Selected-Connection-Id` header, never hits that key match. For
|
||||
that common case, the real protection against a remaining leg reusing the
|
||||
just-exhausted account is NOT this Set — it is the persistence layer above
|
||||
(the connection's `rateLimitedUntil` is now in the future) combined with
|
||||
this same guard suppressing `transientRateLimitedProviders` for the
|
||||
failure (see "Two-stage design" and the code comment on the
|
||||
`isAgentrouterConnectionQuotaScope` branch in `targetExhaustion.ts`): with
|
||||
that Set left unmarked, `combo.ts`'s `allowRateLimitedConnection` force-allow
|
||||
(`open-sse/services/combo.ts:1005-1013`, `:2734-2738`) does NOT kick in for
|
||||
the provider's remaining legs, so credential selection's `rateLimitedUntil`
|
||||
filter (`src/sse/services/auth.ts:1238`) is honored normally and a
|
||||
remaining leg either picks a different, still-eligible agentrouter
|
||||
connection or fails with no credentials available — it does not force its
|
||||
way back onto the connection this branch just cooled down.
|
||||
Restated quota errors (`额度不足`) do reach a provider rule in production
|
||||
(`agentrouter-user-quota-exhausted`, scope `"connection"`), but `scope` on
|
||||
`ProviderErrorRuleMatch` is currently informational — the persistence path
|
||||
(`checkFallbackError` → `combo.ts`) only consumes `reason` and `cooldownMs`,
|
||||
never `scope`. What actually happens for agentrouter (`passthroughModels:
|
||||
true` → `hasPerModelQuota()` returns `true`) is a **per-model** lockout via
|
||||
`recordModelLockoutFailure()`: the connection itself is never cooled down for
|
||||
this error (`combo.ts` skips `recordProviderCooldown` for 429 when
|
||||
`hasPerModelQuota` is true), so other models on the same account keep being
|
||||
tried — each one burns one call and its own lockout before combo routing
|
||||
moves on. Honoring `scope` end-to-end (so a `"connection"` match actually
|
||||
locks the connection) is tracked as a follow-up.
|
||||
|
||||
### Two-stage design: status restatement, then classification
|
||||
|
||||
@@ -423,15 +380,6 @@ allowlisted providers, the structured error otherwise. Adding a provider to
|
||||
that the default path for every provider not on the list stays
|
||||
byte-for-byte unchanged.
|
||||
|
||||
A rule's `scope` (`model` / `provider` / `connection`) is a separate opt-in
|
||||
from `FULL_TEXT_RULE_PROVIDERS`: `checkFallbackError` only surfaces it as
|
||||
`fallbackResult.ruleScope`, and downstream consumers only honor it as
|
||||
anything other than an informational label, for providers in the
|
||||
`HONORS_RULE_LOCK_SCOPE_PROVIDERS` allowlist in the same file (`gated via
|
||||
honorsRuleLockScope()` — today only `"agentrouter"`). See "Restated quota
|
||||
errors" above for what a `scope: "connection"` match actually does once a
|
||||
provider is on that allowlist.
|
||||
|
||||
### Adding a new quota-misstating gateway
|
||||
|
||||
1. Register one rule array in `statusRestatementRegistry`
|
||||
@@ -447,15 +395,7 @@ provider is on that allowlist.
|
||||
`checkFallbackError` only ever hands the rule the structured
|
||||
`{code, type}` error and a body-text rule will never match live traffic.
|
||||
Rules that match purely on `status`/`headers` (like Opencode's or
|
||||
Minimax's) do not need this opt-in. Separately, if the rule declares
|
||||
`scope: "connection"` and the intent is an actual connection-wide cooldown
|
||||
plus same-request combo skip (not just an informational label), add the
|
||||
provider id to `HONORS_RULE_LOCK_SCOPE_PROVIDERS` in the same file — this
|
||||
is what gates `isAgentrouterConnectionQuotaScope()`-style consumption in
|
||||
`markAccountUnavailable()` (`src/sse/services/auth.ts`) and
|
||||
`applyComboTargetExhaustion()`
|
||||
(`open-sse/services/combo/targetExhaustion.ts`); without it, `scope`
|
||||
still flows through `fallbackResult.ruleScope` but nothing acts on it.
|
||||
Minimax's) do not need this opt-in.
|
||||
3. Add unit tests mirroring `tests/unit/upstream-status-restatement.test.ts`
|
||||
and `tests/unit/agentrouter-error-rules.test.ts` (including the
|
||||
not-permanent / not-creditsExhausted guards, and — if the provider needs
|
||||
|
||||
@@ -55,9 +55,9 @@ The two profiles here are **scale-out options for deployments that hit the SQLit
|
||||
|
||||
**What it adds:**
|
||||
|
||||
| Service | Image | Ports | Notes |
|
||||
| --------- | --------------------------------- | ------ | ----------------------------------------------------------------------- |
|
||||
| `bifrost` | `ghcr.io/maximhq/bifrost:v1.6.11` | `8080` | Go-based Tier-1 router; persistent logs volume `omniroute_bifrost_logs` |
|
||||
| Service | Image | Ports | Notes |
|
||||
| --------- | -------------------------------- | ------ | ----------------------------------------------------------------------- |
|
||||
| `bifrost` | `ghcr.io/maximhq/bifrost:1.5.21` | `8080` | Go-based Tier-1 router; persistent logs volume `omniroute_bifrost_logs` |
|
||||
|
||||
**Activation:** set `BIFROST_BASE_URL=http://bifrost:8080` in `.env.example`. The existing sidecar proxy route at [`src/app/api/v1/relay/chat/completions/bifrost/route.ts`](../../src/app/api/v1/relay/chat/completions/bifrost/route.ts) (added in PR #4381) will pick this up automatically.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (341 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
|
||||
<svg viewBox="0 0 1200 350" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Animated terminal demoing the OmniRoute CLI: omniroute providers list (339 providers registered, anthropic, codex, glm, kimi shown active), omniroute combo list (always-on priority, cost-saver, fusion-panel, context-relay) and omniroute health (healthy, 18412 requests in 24h, p95 412ms, circuit breakers 24 closed, 1 half-open, 0 open), cycling over the 80+ command surface: providers, oauth, keys, combo, nodes, models, cache, compression, cost, usage, quota, health, resilience, telemetry, logs, audit, mcp, a2a, cloud, memory, skills, eval, doctor, repl, tunnel, backup, sync, webhooks, policy, pricing, translator, simulate and more.">
|
||||
<desc>Compact animated terminal cycling three real OmniRoute CLI commands with a typewriter effect and a scrolling subcommand ticker; the first frame shows the completed providers-list screen.</desc>
|
||||
<defs><clipPath id="tickerClip"><rect x="12" y="304" width="1176" height="40"/></clipPath><clipPath id="tw0"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;31;61;92;122;153;184;214;245;245" keyTimes="0;0.012;0.018;0.024;0.030;0.036;0.042;0.048;0.054;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw1"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;26;51;76;102;128;153;178;204;204" keyTimes="0;0.345;0.351;0.357;0.363;0.369;0.375;0.381;0.387;1" dur="18s" repeatCount="indefinite"/></rect></clipPath><clipPath id="tw2"><rect x="64" y="46" height="26" width="0"><animate attributeName="width" calcMode="discrete" values="0;20;41;61;82;102;122;143;163;163" keyTimes="0;0.678;0.684;0.690;0.696;0.702;0.708;0.714;0.720;1" dur="18s" repeatCount="indefinite"/></rect></clipPath></defs>
|
||||
<rect width="1200" height="350" fill="#0d1117"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 341 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 109 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
|
||||
<svg viewBox="0 0 1200 780" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Comparison table: OmniRoute versus 9router, OpenRouter, CLIProxyAPI and LiteLLM across 13 capabilities. OmniRoute is the only one with the full set: 339 providers, 90+ free providers built-in, 19 routing strategies, 12-engine token compression, a built-in MCP server with 105 tools, A2A protocol, persistent memory, guardrails, cloud agents, TLS fingerprint stealth, desktop/Termux/PWA, 43 UI locales and 100% MIT self-hosted. 9router has free providers, RTK compression and translation but no MCP, A2A, memory, guardrails, cloud agents or stealth. OpenRouter is a hosted SaaS with 400+ models, guardrails and a hosted MCP but is not self-hosted and lacks A2A, memory, cloud agents and stealth. CLIProxyAPI is a light OAuth proxy with two routing strategies. LiteLLM has 100+ providers, A2A and extensive guardrails but no memory, compression, free tier, stealth or cloud agents.">
|
||||
<desc>Static-header comparison table where each capability row fades in top to bottom; the OmniRoute column is highlighted and shows a check or a leading value in every row, while competitors show a mix of checks, partials and crosses.</desc>
|
||||
<defs>
|
||||
<pattern id="gC" width="32" height="32" patternUnits="userSpaceOnUse"><path d="M 32 0 L 0 0 0 32" fill="none" stroke="#ffffff" stroke-opacity="0.05" stroke-width="1"/></pattern>
|
||||
@@ -57,7 +57,7 @@
|
||||
</g>
|
||||
<g opacity="0"><animate attributeName="opacity" values="0;1" dur="0.4s" begin="0.51s" fill="freeze"/>
|
||||
<text x="44" y="364" font-size="14.5" fill="#c9d1d9">Built-in MCP server (own tools)</text>
|
||||
<text x="440" y="364" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">109</text>
|
||||
<text x="440" y="364" text-anchor="middle" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="15" font-weight="800" fill="#7ee787">105</text>
|
||||
<use href="#no" x="604" y="359"/>
|
||||
<use href="#mid" x="760" y="359"/>
|
||||
<use href="#no" x="916" y="359"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
@@ -1,4 +1,4 @@
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 341 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 341 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 56 free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 109 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
|
||||
<svg viewBox="0 0 1200 540" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="The OmniRoute promise: one endpoint, 339 providers — never stop building, OmniRoute picks the cheapest one that works. Six pillars. Never hit limits: auto-fallback across 339 providers in milliseconds, quota out means the next provider takes over with zero downtime. Save up to 95 percent of tokens: RTK plus Caveman stacked compression cuts 15 to 95 percent of eligible tokens, about 89 percent average on tool-heavy sessions. Zero dollars to start: 90+ providers with a free tier, 40+ free forever — Qoder, Pollinations, Cloudflare, SiliconFlow — no card needed. Every tool works: 33 coding agents including Claude Code, Codex, Cursor, Cline, Copilot and Antigravity through one config. One endpoint: OpenAI, Claude, Gemini and Responses API translation — point any tool at /v1 and it just works. Production-grade: circuit breakers, TLS stealth, MCP with 105 tools, A2A, memory, guardrails, evals — 25,000+ tests.">
|
||||
<desc>Animated promise card: six pillar tiles fade in in reading order, then a soft colored border highlight sweeps from tile to tile in a continuous cycle.</desc>
|
||||
<defs>
|
||||
<pattern id="gridPaperP" width="32" height="32" patternUnits="userSpaceOnUse">
|
||||
@@ -21,7 +21,7 @@
|
||||
<line x1="150" y1="53" x2="1160" y2="53" stroke="#232b38" stroke-width="1.5"/>
|
||||
</g>
|
||||
<g>
|
||||
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">341 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
|
||||
<text x="40" y="100" font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif" font-size="23" font-weight="600" fill="#c9d1d9">One endpoint. <tspan fill="#a78bfa" font-weight="800">339 providers.</tspan> Never stop building — OmniRoute picks <tspan fill="#7ee787" font-weight="700">the cheapest one that works</tspan>.</text>
|
||||
</g>
|
||||
|
||||
<g font-family="Inter, 'Segoe UI', Arial, Helvetica, system-ui, sans-serif">
|
||||
@@ -38,7 +38,7 @@
|
||||
<line x1="3.9" y1="3.9" x2="18.1" y2="18.1"/>
|
||||
</g>
|
||||
<text x="102" y="170" font-size="18" font-weight="800" fill="#74b9ff">Never hit limits</text>
|
||||
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 341 providers in</text>
|
||||
<text x="66" y="204" font-size="13.5" fill="#a1a1aa">Auto-fallback across 339 providers in</text>
|
||||
<text x="66" y="226" font-size="13.5" fill="#a1a1aa">milliseconds. Quota out? The next provider</text>
|
||||
<text x="66" y="248" font-size="13.5" fill="#a1a1aa">takes over — zero downtime.</text>
|
||||
</g>
|
||||
@@ -73,7 +73,7 @@
|
||||
<circle cx="6.6" cy="6.6" r="1.4" fill="#fdcb6e" stroke="none"/>
|
||||
</g>
|
||||
<text x="862" y="170" font-size="18" font-weight="800" fill="#fdcb6e">$0 to start</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 56 free</text>
|
||||
<text x="826" y="204" font-size="13.5" fill="#a1a1aa">90+ providers with a free tier, 40+ free</text>
|
||||
<text x="826" y="226" font-size="13.5" fill="#a1a1aa">forever — Qoder, Pollinations, Cloudflare,</text>
|
||||
<text x="826" y="248" font-size="13.5" fill="#a1a1aa">SiliconFlow… No card needed.</text>
|
||||
</g>
|
||||
@@ -125,7 +125,7 @@
|
||||
<path d="M 7,11 L 10,14 L 15.4,8.2"/>
|
||||
</g>
|
||||
<text x="862" y="354" font-size="18" font-weight="800" fill="#7ee787">Production-grade</text>
|
||||
<text x="826" y="388" font-size="13.5" fill="#a1a1aa">Circuit breakers, TLS stealth, MCP (109</text>
|
||||
<text x="826" y="388" font-size="13.5" fill="#a1a1aa">Circuit breakers, TLS stealth, MCP (105</text>
|
||||
<text x="826" y="410" font-size="13.5" fill="#a1a1aa">tools), A2A, memory, guardrails, evals —</text>
|
||||
<text x="826" y="432" font-size="13.5" fill="#a1a1aa">25,000+ tests.</text>
|
||||
</g>
|
||||
|
||||
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user