mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Externalize ws / bufferutil / utf-8-validate in serverExternalPackages so the copilot-m365-web WebSocket masking path works at runtime (bundling ws → TypeError: b.mask is not a function → 80s chat timeout). Regression guard in next-config.test.ts. Co-authored-by: diegosouzapw <diegosouza.pw@gmail.com>
This commit is contained in:
58
.env.example
58
.env.example
@@ -73,6 +73,11 @@ DISABLE_SQLITE_AUTO_BACKUP=false
|
||||
# Default: 20128
|
||||
PORT=20128
|
||||
|
||||
# Base path (URL subpath) when serving OmniRoute behind a reverse proxy under a subpath.
|
||||
# Used by: next.config.mjs — sets Next.js `basePath`; auth redirects are basePath-aware.
|
||||
# Default: "" (served at the domain root). Example: /omniroute to serve under https://host/omniroute
|
||||
# OMNIROUTE_BASE_PATH=
|
||||
|
||||
# Split-port mode: serve Dashboard and API on separate ports for network isolation.
|
||||
# Used by: src/lib/runtime/ports.ts — overrides PORT for each service.
|
||||
# API_PORT=20129
|
||||
@@ -413,6 +418,15 @@ NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
||||
# Do not include /v1; if included accidentally it will be normalized away.
|
||||
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128
|
||||
|
||||
# Absolute provider plugin manifest URL advertised to sidecar clients.
|
||||
# Used by: open-sse/config/providerPluginManifestUrl.ts. When unset, OmniRoute
|
||||
# derives the URL from request origin or HOST/PORT using OMNIROUTE_PUBLIC_PROTOCOL.
|
||||
# OMNIROUTE_PROVIDER_MANIFEST_URL=https://omniroute.example.com/api/v1/provider-plugin-manifest
|
||||
|
||||
# Protocol used when deriving provider plugin manifest URLs without a request origin.
|
||||
# Used by: open-sse/config/providerPluginManifestUrl.ts. Defaults to http.
|
||||
# OMNIROUTE_PUBLIC_PROTOCOL=http
|
||||
|
||||
# Max wait time for an async chatgpt-web image to land via the celsius
|
||||
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
|
||||
# upstream queue-deep windows ("Lots of people are creating images right now").
|
||||
@@ -568,6 +582,8 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
||||
# CLI_CONTINUE_BIN=cn
|
||||
# CLI_QODER_BIN=qoder
|
||||
# CLI_QWEN_BIN=qwen
|
||||
# CLI_AUGGIE_BIN=auggie
|
||||
# AUGGIE_BIN=auggie
|
||||
|
||||
# Override the Hermes Agent home directory (where OmniRoute reads/writes the
|
||||
# Hermes CLI config). Matches the env var the Hermes PowerShell installer sets
|
||||
@@ -869,6 +885,8 @@ GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
||||
# QODER_PERSONAL_ACCESS_TOKEN=
|
||||
# QODER_CLI_WORKSPACE=
|
||||
# OMNIROUTE_QODER_WORKSPACE=
|
||||
# Override the Qoder CLI config dir (isolated PAT session, avoids clobbering a browser login).
|
||||
# QODER_CLI_CONFIG_DIR=
|
||||
|
||||
# ── Blackbox Web validated-token override (issue #2252) ──
|
||||
# Used by: open-sse/executors/blackbox-web.ts. Blackbox `/api/chat` rejects
|
||||
@@ -1037,6 +1055,12 @@ CURSOR_USER_AGENT="Cursor/3.4"
|
||||
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
|
||||
# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000
|
||||
|
||||
# ── Firecrawl web-fetch executor ──
|
||||
# Point at a self-hosted Firecrawl instance (defaults to the public cloud API).
|
||||
# When set to a non-cloud base URL, the API key becomes optional.
|
||||
# FIRECRAWL_BASE_URL=https://api.firecrawl.dev
|
||||
# FIRECRAWL_TIMEOUT_MS=30000 # Per-request timeout (default: 30000 = 30s)
|
||||
|
||||
# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ──
|
||||
# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for
|
||||
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
||||
@@ -1442,6 +1466,13 @@ APP_LOG_TO_FILE=true
|
||||
# CLIPROXYAPI_PORT=5544
|
||||
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
|
||||
|
||||
# ── Mux embedded service ──
|
||||
# Override the port where the embedded Mux (coder/mux) agent-orchestration
|
||||
# daemon listens. Always bound to 127.0.0.1 — never configurable to 0.0.0.0.
|
||||
# Rarely needed — defaults to 8322.
|
||||
# Used by: src/lib/services/bootstrap.ts, src/app/api/services/mux/_lib.ts
|
||||
# MUX_SERVICE_PORT=8322
|
||||
|
||||
# ── Local hostnames (Docker networking) ──
|
||||
# Comma-separated additional hostnames treated as "local" for provider routing.
|
||||
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
|
||||
@@ -1464,6 +1495,20 @@ APP_LOG_TO_FILE=true
|
||||
# healthy-result cache window under high concurrency.
|
||||
# PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS=2000
|
||||
|
||||
# Background proxy health scheduler (src/lib/proxyHealth/scheduler.ts).
|
||||
# Periodically probes every registered proxy and (optionally) removes dead ones.
|
||||
# Set "false" to disable the scheduler entirely. Default: enabled.
|
||||
# PROXY_HEALTH_ENABLED=true
|
||||
# Sweep interval in ms (minimum 60000). Default: 600000 (10min).
|
||||
# PROXY_HEALTH_INTERVAL_MS=600000
|
||||
# Reachability probe target for the scheduler and the auto-test endpoint.
|
||||
# Point it at an internal/self-hosted URL to avoid the public default.
|
||||
# PROXY_HEALTH_TEST_URL=https://httpbin.org/ip
|
||||
# Set "true" to let the scheduler auto-remove proxies after repeated failures.
|
||||
# PROXY_AUTO_REMOVE=false
|
||||
# Consecutive failures before an auto-remove fires. Default: 3.
|
||||
# PROXY_AUTO_REMOVE_AFTER=3
|
||||
|
||||
# Allow OAuth and provider validation flows to bypass a pinned proxy and connect
|
||||
# directly when proxy reachability pre-checks fail. Default: false.
|
||||
# Also configurable from Dashboard > Settings > Feature Flags.
|
||||
@@ -1675,6 +1720,15 @@ APP_LOG_TO_FILE=true
|
||||
# FREE_PROXY_IPLOCATE_ENABLED=false
|
||||
# FREE_PROXY_IPLOCATE_BASE_URL=https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols
|
||||
|
||||
# ── Free Proxy Pool (Webshare source) ──
|
||||
# Used by: src/lib/freeProxyProviders/webshare.ts
|
||||
# Paid, per-account proxy list — requires FREE_PROXY_WEBSHARE_API_KEY to activate,
|
||||
# regardless of FREE_PROXY_WEBSHARE_ENABLED.
|
||||
# FREE_PROXY_WEBSHARE_ENABLED=true
|
||||
# FREE_PROXY_WEBSHARE_API_KEY=
|
||||
# FREE_PROXY_WEBSHARE_API_URL=https://proxy.webshare.io/api/v2/proxy/list/
|
||||
# FREE_PROXY_WEBSHARE_MAX=500
|
||||
|
||||
# ── Vercel Relay ──
|
||||
# Used by: src/app/api/settings/proxy/vercel-deploy/route.ts
|
||||
# Hides the "Deploy Relay" button when set to false.
|
||||
@@ -1879,6 +1933,10 @@ QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
||||
# duplicated). Falls back to TS path via X-Bifrost-Fallback header on
|
||||
# timeout/failure. See bin/omniroute for the local-redis companion.
|
||||
# BIFROST_BASE_URL=
|
||||
# Port the supervised Bifrost embedded service binds to (127.0.0.1:<port>), read by
|
||||
# src/lib/services/bootstrap.ts when OmniRoute manages the Bifrost sidecar lifecycle.
|
||||
# Default: 8080.
|
||||
# BIFROST_PORT=8080
|
||||
# API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
|
||||
# unset, the route expects the request to carry a valid OmniRoute API key;
|
||||
# this key is for gateway-side auth only.
|
||||
|
||||
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -624,7 +624,7 @@ jobs:
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
|
||||
test-vitest:
|
||||
name: Vitest (MCP / autoCombo / UI components)
|
||||
@@ -676,7 +676,7 @@ jobs:
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
|
||||
node-26-compat-build:
|
||||
name: Node 26 Compatibility Build
|
||||
@@ -729,7 +729,7 @@ jobs:
|
||||
cache: npm
|
||||
- uses: ./.github/actions/npm-ci-retry
|
||||
- run: npm run check:node-runtime
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
- run: node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/4 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
|
||||
test-coverage-shard:
|
||||
name: Coverage Shard (${{ matrix.shard }}/8)
|
||||
@@ -770,7 +770,7 @@ jobs:
|
||||
--exclude=tests/** \
|
||||
--exclude=**/*.test.* \
|
||||
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \
|
||||
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
--test-shard=${{ matrix.shard }}/8 tests/unit/*.test.ts "tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,memory,runtime,security,services,settings,shared,ui,usage}/**/*.test.ts"
|
||||
- name: Upload raw shard coverage
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v7
|
||||
|
||||
2
.github/workflows/quality.yml
vendored
2
.github/workflows/quality.yml
vendored
@@ -145,4 +145,4 @@ jobs:
|
||||
--import ./tests/_setup/isolateDataDir.ts
|
||||
--test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2
|
||||
tests/unit/*.test.ts
|
||||
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
"tests/unit/{api,auth,authz,build,cli,cli-helper,combo,compression,correctness,cors,dashboard,db,db-adapters,docs,executors,gamification,guardrails,lib,mcp,runtime,security,services,settings,shared,ui}/**/*.test.ts"
|
||||
|
||||
30
CHANGELOG.md
30
CHANGELOG.md
@@ -4,6 +4,34 @@
|
||||
|
||||
---
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(api):** add `/v1/ocr` endpoint (Mistral OCR), an OCR provider category, and Mistral moderation support. (thanks @waguriagentic)
|
||||
- **Discovery tool (Phase 2):** add the `discoveryResults` DB module (CRUD over the `discovery_results` table, migration 074) and wire the opt-in provider-discovery service to persist and read findings through it (`persistDiscoveryResult`, `getDiscoveryResults`, `getDiscoveryResultById`, `markVerified`, `deleteDiscoveryResult`) with `(provider, method, endpoint)` upsert de-duplication. Adds the `/api/discovery/*` HTTP surface — `GET /results`, `GET|DELETE /results/:id`, `POST /scan`, `POST /verify/:id` — under **strict loopback-only** authorization (`/api/discovery/` is in `LOCAL_ONLY_API_PREFIXES` and is NOT manage-scope-bypassable, so the `scan` route's outbound probes can never be reached from a tunnel/remote origin). Adds a **dashboard UI tab** (Tools → Discovery, `/dashboard/discovery`) to run scans and review, verify, or delete findings. The service stays **opt-in / default-off**.
|
||||
- **feat(api):** expose a read-only provider plugin manifest at `GET /api/v1/provider-plugin-manifest` for sidecar/relay discovery. (thanks @KooshaPari)
|
||||
- **feat(sidecar):** advertise the provider manifest URL to Bifrost/CLIProxyAPI via the `X-OmniRoute-Provider-Manifest-Url` header (`OMNIROUTE_PROVIDER_MANIFEST_URL`). (thanks @KooshaPari)
|
||||
- **feat(autoCombo):** add a latency/speed-optimized routing mode (shared `rankBySpeed` scoring core) plus the `omniroute_pick_fastest_model` MCP tool. (thanks @KooshaPari)
|
||||
- **feat(providers):** refresh The Old LLM (Free) model catalog ([#5181](https://github.com/diegosouzapw/OmniRoute/issues/5181)) — seed the current free `/api/chatgpt` tier (GPT-5/5.1/5.2/5.3/5.4, o3/o4-mini, Gemini 3 Pro / 2.5 Pro / 2.0 Flash / 1.5 Flash, Claude 4.6 Opus/Sonnet & 4.5 Haiku, GPT-4o, Grok 4, DeepSeek V3/R1, Sonar Pro) while keeping the legacy alias IDs for saved-preference compatibility. Also fixes a latent routing bug: `mapModel()` now passes known upstream IDs through unchanged, so Gemini/o-series/Grok/DeepSeek/Sonar models no longer silently collapse onto `GPT_5_4`. Regression guard: `tests/unit/theoldllm-model-refresh-5181.test.ts`. (thanks @WslzGmzs)
|
||||
- **feat(resilience):** surface Codex **banked reset credits** per connected account ([#5199](https://github.com/diegosouzapw/OmniRoute/issues/5199)) — the Codex quota parsers (`buildCodexUsageQuotas`, `parseCodexUsageResponse`) now additively read `rate_limit_reset_credits.available_count` (+ optional `rate_limit_reached_type`) from the `/wham/usage` payload OmniRoute already fetches, and the provider-limits dashboard renders a **"Banked Reset Credits"** row when a positive count is present. Display-only and **fail-open** — the field is eligibility-gated, so accounts without it are unaffected (parsers never throw on absent/garbage shapes); redemption (an unofficial mutating endpoint) is intentionally out of scope. Regression guard: `tests/unit/codex-banked-reset-credits-5199.test.ts` (8). (thanks @ofekbetzalel)
|
||||
- **feat(providers):** add sign-up geo-restriction notices for **SenseNova** and **StepFun** ([#5462](https://github.com/diegosouzapw/OmniRoute/issues/5462)) — the provider add-form now warns that SenseNova's console appears to require a Chinese (+86) phone number with no documented international path, and that StepFun's default endpoint is its China platform while a global StepFun Open Platform (`platform.stepfun.ai`, operated by Sparkling AI Pte. Ltd., Singapore) with email/Google/Discord login exists for international users. Informational `notice` only — neither provider is disabled. Regression guard: `tests/unit/regional-provider-cn-notices-5462.test.ts`. (thanks @chirag127)
|
||||
- **feat(usage):** add on-demand period-scoped usage-data reset (Settings → System Storage) with a purge API and time-window selector.
|
||||
- **feat(claude-code):** add an opt-in auto-permission classifier compat mode (off/auto/always) for Claude Code, toggleable from the CLI Code settings.
|
||||
- **feat(providers):** add optional client-identity header profiles for compatible nodes — preset User-Agent/fingerprint headers (e.g. matching a known CLI) merged into the existing customHeaders field.
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **dashboard ("Update now" → Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991))
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PR→release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` — that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959))
|
||||
|
||||
- **API validation:** add a `validatedJsonBody(request, schema)` helper in `src/shared/validation/helpers.ts` that fuses JSON body parsing and Zod validation into a single call, returning either the type-narrowed data or a ready-to-return 400 `NextResponse` with the standard error envelope. Salvaged from the closed refactor PR #5075 (Tier 1 portable helper) with a focused 6-case regression test. Co-authored-by: KooshaPari <KooshaPari@users.noreply.github.com>
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
@@ -74,6 +102,8 @@
|
||||
|
||||
- **providers (CLI profile auto-sync):** opt-in CLI profile auto-sync toggles, including Claude Code auto-sync, so generated CLI profiles can track provider changes automatically. ([#5755](https://github.com/diegosouzapw/OmniRoute/pull/5755) — thanks @diegosouzapw)
|
||||
|
||||
- **feat(minimax):** surface MiniMax M3 `<think>` reasoning as `reasoning_content` on OpenAI-format provider tiers. (thanks @zmf963)
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
- **fix(opencode):** stop fabricating `User-Agent: opencode/local` and `x-opencode-client: cli` headers when the client sends none — the executor-dedup refactor ([#5720](https://github.com/diegosouzapw/OmniRoute/pull/5720)) accidentally re-introduced header fabrication, violating the forward-only contract (inventing opencode-internal values risks upstream rejection). Restored to forward-only: those headers are emitted only when a real client source is present. Regression guard: `tests/unit/opencode-executor.test.ts`. (thanks @diegosouzapw)
|
||||
|
||||
@@ -541,6 +541,9 @@ the stale-enforcement added in Fase 6A.3.
|
||||
19. Never develop on the shared main checkout. Every development task runs in its own git worktree on its own dedicated branch, and you MUST confirm the base branch with the operator (e.g. via `AskUserQuestion`) before creating the worktree/branch — never assume `main` or the currently checked-out branch. A `git checkout` in the shared checkout silently destroys other sessions' uncommitted work. Tear down only the worktrees/branches you created (by name, never `fix/*`/`feat/*` wildcards), leave other sessions' worktrees untouched, and end on the branch you started on (the active `release/vX.Y.Z`, never `main`). See Git Workflow → "Worktree isolation".
|
||||
20. PII redaction/sanitization is **opt-in — never on by default**. OmniRoute proxies for self-hosted/local LLMs where the operator owns the data, so mutating request/response payloads by default would silently corrupt legitimate traffic. The two data-mutating PII feature flags **MUST** keep `defaultValue: "false"` in `src/shared/constants/featureFlagDefinitions.ts`: `PII_REDACTION_ENABLED` (request-side) and `PII_RESPONSE_SANITIZATION` (response + streaming). All three application points — `src/lib/guardrails/piiMasker.ts` (request guardrail), `src/lib/piiSanitizer.ts` (response), `src/lib/streamingPiiTransform.ts` (SSE) — are gated on these flags; with both off the `pii-masker` guardrail still runs but never mutates payloads (data passes through untouched). Flipping either default to `"true"` requires explicit operator approval. The regression guard is `tests/unit/pii-opt-in-default.test.ts` (asserts both definition defaults + behavioral pass-through). Opt-in is per-operator via env or the settings/DB override (`src/lib/db/featureFlags.ts`), never a silent default. See `docs/security/GUARDRAILS.md`.
|
||||
21. **Release-freeze — the release branch is frozen to campaign merges while a `/generate-release` is running.** `/generate-release` opens a marker issue labeled `release-freeze` at the start of reconciliation (Phase 0a) and closes it once the release PR squash-merges to `main`. Before merging **any** PR into the active `release/vX.Y.Z` branch, every campaign workflow (`/review-issues`, `/review-prs`, `/implement-features`, `/green-prs`, `/port-upstream-*`) **MUST** check `gh issue list --repo diegosouzapw/OmniRoute --label release-freeze --state open` — if a freeze is active, **HOLD the merge** (leave the PR ready and open; do NOT merge to the release branch), tell the operator, and resume once the freeze lifts. This is a **coordination signal, not a permission lock**: the release captain and the campaign sessions share the `diegosouzapw` identity, so a GitHub branch-protection lock cannot distinguish them — only this honored marker prevents the mid-release commit races that forced full CHANGELOG re-reconciliation in v3.8.40/v3.8.41 (a parallel campaign advanced `release/vX.Y.Z` by 34 commits mid-run). The release captain's own reconciliation/cycle-open pushes are exempt — they _are_ the release. Fixes that must land during a freeze (a homologation finding) follow the post-merge read-only rule: land on `main` first via `fix/release-vX.Y.Z-*`. **⛔ ONLY `/generate-release` may raise a release-freeze, and ONLY at its Phase 0a (start of generating a new version) — lifted at Phase 12c after the squash-merge to `main`.** No campaign, session, or agent may open a `release-freeze` marker at any other time — a freeze is **never** a mid-development coordination tool. If a session ever believes a freeze is genuinely, unavoidably necessary outside the `/generate-release` flow, it **MUST first ask the operator (`diegosouzapw`) in chat, explicitly alert "estou criando um freeze" and get an explicit yes** — never open, extend, or re-open a `release-freeze` autonomously. Conversely, do **not** close/lift an active `/generate-release` freeze to unblock campaign merges: it protects the captain's single clean CI run and auto-lifts at Phase 12c — closing it early re-triggers the exact commit race it prevents. Verify a freeze is legitimate before acting on it: an open `release-freeze` whose title/body references an **OPEN** release PR (`gh pr view <N> --json state`) is the authorized captain freeze — hold, don't touch.
|
||||
22. **Cross-session safety — this repo is worked by MANY parallel sessions/agents at once; never step on another's in-flight work.** Two absolute bans, both recurring incidents (this rule exists because they keep happening):
|
||||
- **(a) Never `git stash` / `git stash pop` — ANYWHERE in this repo, including inside an isolated worktree, and including inside any subagent you dispatch.** `git stash` operates on the **shared repository object store**, not the per-worktree working tree — so a stash pushed or popped in one session can silently clobber or resurrect another parallel session's uncommitted changes. This is not hypothetical: 2026-07-02 a `#5923` quotaCache change leaked into the unrelated `#2296` worktree via a global `stash pop`, and the same class reincided through a **subagent**. To compare working changes against a base ref **without** stashing, use `git show <ref>:<path>` or `git diff <ref> -- <path>`; to confirm a typecheck/lint error is pre-existing on the base, inspect the base ref directly (`git show origin/release/vX.Y.Z:<path>`) — never stash your tree away to "get it clean". **Put this ban verbatim in the prompt of every subagent that touches git** (agents don't inherit this file's context — the recurrence was a subagent).
|
||||
- **(b) Never merge, push, rebase, or force-push a PR / branch / worktree that another session is actively working.** An open PR whose head is a live fix worktree in `.claude/worktrees/` you did **not** create (e.g. `fix-5852`/`fix-5923` carrying fresh commits, even when they share your `diegosouzapw` identity), or any branch another session owns, is **off-limits — HOLD**, and let the owning session merge it. **Before** merging or pushing to any PR you did not create *this* session, run `git worktree list` to check for a matching in-flight worktree and re-check `gh pr view <N> --json state,headRefOid`. Only the owning session merges its own in-flight PR; mid-flight merges race the owner and re-trigger the exact commit/CHANGELOG races Rule #19 and Rule #21 guard against. (Reinforces Rule #19.)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ export function buildProfileSettings(modelId, baseUrl, cfg) {
|
||||
* behaviorally identical. Writes `<claudeHome>/profiles/<name>/settings.json`
|
||||
* (directory-per-profile); never touches the active/default Claude config.
|
||||
* @param {Array} models
|
||||
* @param {{claudeHome?:string, baseUrl:string, dryRun?:boolean, only?:string}} opts
|
||||
* @param {{claudeHome?:string, baseUrl:string, dryRun?:boolean, only?:string, log?:(line:string)=>void}} opts
|
||||
* @returns {Promise<{written:number, skipped:number, profiles:Array<{name:string, model:string, filePath:string}>}>}
|
||||
*/
|
||||
export async function syncClaudeProfilesFromModels(models, opts = {}) {
|
||||
@@ -64,6 +64,12 @@ export async function syncClaudeProfilesFromModels(models, opts = {}) {
|
||||
const profilesRoot = join(claudeHome, "profiles");
|
||||
const baseUrl = opts.baseUrl;
|
||||
const dryRun = Boolean(opts.dryRun);
|
||||
// Injectable dry-run printer (#5959): under the node:test runner, a child
|
||||
// process writing multi-byte UTF-8 (the "──" box-drawing heading) to stdout
|
||||
// corrupts the runner's V8-serialized event stream ~50% of the time
|
||||
// ("Unable to deserialize cloned data due to invalid or unsupported
|
||||
// version"). Tests inject a collector; the CLI default stays console.log.
|
||||
const log = opts.log ?? console.log;
|
||||
const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
|
||||
|
||||
if (!dryRun && !existsSync(profilesRoot)) {
|
||||
@@ -96,8 +102,8 @@ export async function syncClaudeProfilesFromModels(models, opts = {}) {
|
||||
const content = buildProfileSettings(id, baseUrl, cfg);
|
||||
|
||||
if (dryRun) {
|
||||
console.log(`\n── [dry-run] ${filePath} ──`);
|
||||
console.log(content);
|
||||
log(`\n── [dry-run] ${filePath} ──`);
|
||||
log(content);
|
||||
} else {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(filePath, content, "utf8");
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"_comment": "Catraca de complexidade (check-complexity.mjs, ESLint core rules complexity>=15 e max-lines-per-function>80 sobre src+open-sse+electron+bin via eslint.complexity.config.mjs). Conta total de violacoes; so pode cair. --update ratcheta.",
|
||||
"count": 1995,
|
||||
"count": 2015,
|
||||
"_rebaseline_2026_07_03_6007_sidecar_manifest": "2007->2015. PR #6007 (re-cut) adds sidecar/provider-manifest header wiring and the public manifest URL helper; the feature files themselves introduce 0 new complexity violations (check-complexity flags none in providerPluginManifestUrl.ts). The +8 vs the 2007 release baseline is inherited release/v3.8.44 drift absorbed at merge (check:complexity measures 2015 on the current release tip). Tighten via --update next cycle / at /generate-release Phase 0.",
|
||||
"_rebaseline_2026_07_02_v3844_ci_observed": "2006->2007 (+1). Local Ubuntu measures 2006 on this tree; the GitHub fast-gates runner measures 2007 (same local-vs-CI off-by-one already documented in _rebaseline_2026_06_26_v3838_release_fast_gate). Use the CI-observed value so the gate is deterministic where it actually runs.",
|
||||
"_rebaseline_2026_07_02_v3844_post_5939": "2003->2006 (+3). Inherited drift from the release/v3.8.44 merges after 3a3d618fe (#5809 audio translations et al.), surfaced by PR fix#5959: check:complexity measures 2006 on the pristine base (cbd08ef78) WITH AND WITHOUT this PR's one-line CLI change (verified by reverting the file and re-measuring) — the PR is complexity-net-zero. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_02_v3844_merge_burst": "1995->2003 (+8). Inherited v3.8.44 cycle drift surfaced by PR #5939: check:complexity measures 2003 on BOTH the pristine release tip (3a3d618fe) and this PR's merged HEAD — identical, so all +8 came from the 2026-07-02 merge burst into release/v3.8.44 (#5933 codex schema, #5950 OCR, #5904/#5920 combo, #6000/#6008 executor refactors, etc.) merged while the fast-gates queue was base-red (file-size #5933). PR #5939 itself was verified complexity-net-zero during its own CI cycle (DiscoveryPageClient refactored into hooks/sub-components to stay under max-lines-per-function). Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_02_5798_release_green": "1982->1995 (+13). Inherited v3.8.43 cycle drift surfaced by the release-green unblock #5798 / PR #5896: check:complexity measures 1995 on BOTH the pristine release tip (0d3875a98) and this PR's HEAD — identical, so all +13 came from the 2026-07-01/02 merge burst (providers/usage/dashboard fixes merged via --admin while the fast-gates queue was base-red). This PR touches only gate scripts, docs, baselines and test files — 0 production logic. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_01_v3843_release": "1981->1982 (+1). v3.8.43 cycle drift, surfaced after check:mutation-test-coverage was fixed (it was masked behind that earlier step in the Fast Quality Gates chain). 1982 = the value measured by check:complexity on BOTH fce85136c (release tip) and 6d7060e21 (release + the 5 CI fixes) — identical, so all +1 is inherited cycle drift; the fixes touch only test files + linkify.ts safeHttpHref (cyclomatic ~4, well under the >=15 threshold, 0 new violations) + config JSON. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_06_28_v3840_5237_reconcile": "1980->1981 (+1). Inherited release/v3.8.40 drift surfaced while merging PR #5237 (impersonation-UA refresh) — the +1 is present on the pristine release tip (d8a392a47) WITHOUT #5237's changes, so it is #5222 (antigravity fallback-LRU retry) / #5221 (command-code) growth that merged via --admin without ratcheting complexity (the PR->release fast-gates do not run check:complexity). #5237 itself is complexity-net-zero: its only edits are a single UA constant, a regenerated golden snapshot, and baseline JSONs. Structural reduction tracked in #3501.",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
|
||||
"_rebaseline_2026_07_03_review_prs_release_green": "Release-green unblock (2026-07-03, /review-prs): the quality.yml fast-gates job was base-red for EVERY PR->release from growth inherited via already-merged PRs on the release tip — no offending PR branch left to fix in-place. Prod frozen raised: ApiManagerPageClient.tsx 3017->3058, OAuthModal.tsx 969->989, cliRuntime.ts 1090->1100, webProvidersA.ts 805->809. Test frozen raised: deepseek-web.test.ts 1081->1092. Real sizes (check-file-size.mjs reported). These stay frozen (cannot grow further); structural shrink tracked under decomposition roadmap #3501; the release captain's rebaseline-at-release supersedes this note. Bundled with the #5695 quick-start test regex fix (multi-line <Link> tolerance) in the same release-green PR.",
|
||||
"_rebaseline_2026_07_02_5798_release_green": "Release-green unblock #5798 / PR #5896 (2026-07-02): the quality.yml fast-gates job was base-red for EVERY PR->release (whole queue failing), from growth inherited via already-merged PRs — no offending PR branch left to fix. Prod frozen raised: AddApiKeyModal.tsx 869->905, providerPageHelpers.ts 996->1021, RequestLoggerV2.tsx 1316->1553, src/sse/services/auth.ts 2403->2405, antigravity.ts 1806->1813, base.ts 1502->1536 (1533 inherited + 3 lines from this PR's own typecheck:core fix in resolveBaseUrl), advancedTools.ts 1118->1120, accountFallback.ts 1783->1790, openai-to-kiro.ts 842->853, openai-responses.ts 1035->1092, stream.ts 2710->2727; new-above-cap frozen: webProvidersA.ts 805, tokenHealthCheck.ts 830. Test frozen raised: cc-compatible-provider 1179->1217, translator-openai-to-kiro 999->1088, web-cookie-providers-new 827->845; new-above-cap: response-sanitizer.test.ts 906. These files remain frozen (cannot grow further); the release captain's rebaseline-at-release supersedes this note.",
|
||||
"_rebaseline_2026_06_30_5552_flat_rate_cost": "Issue #5552 own growth: src/app/api/usage/analytics/route.ts 941->942 (+1 = the `flatRateAsZero: true` cost option at the existing computeUsageRowCost chokepoint, so subscription/cookie-web providers show $0 instead of an inflated per-token estimate in analytics). The flat-rate classifier (isFlatRateProvider + the provider-id set) lives in a new leaf src/lib/usage/flatRateProviders.ts (61 LOC, <cap) and the guard in computeCostFromPricing (src/lib/usage/costCalculator.ts, not baselined). Irreducible 1-line wiring at the analytics chokepoint; covered by tests/unit/flat-rate-cost-5552.test.ts.",
|
||||
"_rebaseline_2026_06_30_5483_force_responses_upstream": "PR #5637 (@KooshaPari, #5483) own growth: open-sse/executors/base.ts 1500->1502 (+2 = import + the single `requestCredentials = withForcedResponsesUpstream(...)` const threaded through buildUrl/buildHeaders/applyConfiguredUserAgent/ccRequestDefaults/transformRequest at the existing fetch-loop chokepoint), open-sse/executors/default.ts 876->877 (+1 = the `_omnirouteForceResponsesUpstream` short-circuit in the buildUrl `/responses` vs `/chat/completions` decision), tests/unit/executor-default-base.test.ts 1477->1523 (+46 = the new regression test that asserts a Responses-shaped MCP request routes to /responses for openai-compatible providers). The detection helpers (shouldForceResponsesUpstream/withForcedResponsesUpstream/isRecord, ~50 LOC) were EXTRACTED out of base.ts into a new leaf open-sse/executors/forceResponsesUpstream.ts (60 LOC, <cap) so the frozen god-file only takes the irreducible 2-line wiring. The contributor PR's incidental prettier reformats were reverted to keep the diff scoped to the fix. Covered by tests/unit/executor-default-base.test.ts.",
|
||||
@@ -131,6 +132,8 @@
|
||||
"_rebaseline_2026_06_20_4389_thinking_toolchoice": "Re-baseline base.ts 1387->1399 (#4389): tool_choice-forced thinking guard at the existing Claude wire-image injection chokepoint (effThinking gate avoids the Anthropic 400 when tool_choice forces a tool). Cohesive guard; structural shrink tracked in #3501.",
|
||||
"cap": 800,
|
||||
"frozen": {
|
||||
"_rebaseline_2026_07_02_5816_qoder": "PR #5816 (@AgentKiller45, qoder PAT via qodercli): qoderCli.ts 666->989, new-above-cap frozen (owner-approved baseline freeze). The growth is the legitimate PAT job-token exchange + quota parsing CLI transport (the pure-JS Cosy path 500'd on every PAT request); extracting the spawn/parse helpers now would just add indirection to a contributor PR mid-merge. Test frozen also raised for this PR's coverage growth: providers-page-utils.test.ts 1052->1092. Additionally clears an inherited base-red from the already-merged #5933 (codex json_schema->text.format): translator-openai-responses-req.test.ts 1097->1172 (+75 regression tests, no offending branch left). All remain frozen (cannot grow further); release captain's rebaseline-at-release supersedes.",
|
||||
"open-sse/services/qoderCli.ts": 989,
|
||||
"_rebaseline_pr1043_minimax_tts": "Upstream port decolua/9router#1043 (toanalien) own growth: audioSpeech.ts 965->1061 (+96). Adds MiniMax T2A v2 TTS dispatch (handleMinimaxSpeech + hexToBytes helper) — provider entry was already in audioRegistry (format: minimax-tts) but no handler existed, falling through to the OpenAI-compatible default that fails (T2A has custom shape + hex-encoded audio + base_resp envelope). New branch sits next to the other inline provider branches (xiaomi-mimo, coqui, tortoise, aws-polly) — extracting would just create indirection. Covered by tests/unit/minimax-tts-1043.test.ts (3 tests, GREEN: success, base_resp error, invalid-hex).",
|
||||
"open-sse/config/providerRegistry.ts": 4731,
|
||||
"open-sse/executors/antigravity.ts": 1813,
|
||||
@@ -182,7 +185,7 @@
|
||||
"open-sse/utils/stream.ts": 2727,
|
||||
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385,
|
||||
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1028,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3017,
|
||||
"src/app/(dashboard)/dashboard/api-manager/ApiManagerPageClient.tsx": 3058,
|
||||
"src/app/(dashboard)/dashboard/cache/media/MediaPageClient.tsx": 1105,
|
||||
"src/app/(dashboard)/dashboard/cache/page.tsx": 845,
|
||||
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900,
|
||||
@@ -209,7 +212,7 @@
|
||||
"src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx": 974,
|
||||
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898,
|
||||
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1089,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ProxyRegistryManager.tsx": 1117,
|
||||
"src/app/(dashboard)/dashboard/settings/components/ResilienceTab.tsx": 1183,
|
||||
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
|
||||
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
|
||||
@@ -240,14 +243,14 @@
|
||||
"src/lib/usage/providerLimits.ts": 955,
|
||||
"src/lib/usage/usageHistory.ts": 988,
|
||||
"_rebaseline_2026_06_27_5193_5203_antigravity_oauthmodal": "Antigravity remote-login own growth: OAuthModal.tsx 960->969 (gate units). #5193 (+~4: remote paste instruction shown for all remote incl. Google + its rationale comment) and #5203 (+~5: handleManualSubmit credential-blob branch + button guard; submit logic extracted to oauthBlobSubmit.ts to minimize). Frozen set to the SUM so either merge order passes. Cohesive at the existing manual-submit chokepoint.",
|
||||
"src/shared/components/OAuthModal.tsx": 969,
|
||||
"src/shared/components/OAuthModal.tsx": 989,
|
||||
"src/shared/components/RequestLoggerV2.tsx": 1553,
|
||||
"src/shared/components/analytics/charts.tsx": 1558,
|
||||
"src/shared/constants/cliTools.ts": 875,
|
||||
"src/shared/constants/pricing.ts": 1662,
|
||||
"src/shared/constants/providers.ts": 3276,
|
||||
"src/shared/constants/sidebarVisibility.ts": 1198,
|
||||
"src/shared/services/cliRuntime.ts": 1090,
|
||||
"src/shared/services/cliRuntime.ts": 1100,
|
||||
"src/shared/validation/schemas.ts": 2523,
|
||||
"_rebaseline_2026_06_28_5275_correlation_id_extract": "Extraction of the safe CorrelationId subset of #5275 (hartmark) — request correlation id stored in call_logs (migration 109) and returned via the X-Correlation-Id response header, WITHOUT the combo/resilience or build/lazy-loading changes (those stay in #5275). Own growth: callLogs.ts 975->985 (correlation_id column on CallLogSummaryRow + read/map), usageHistory.ts 983->988 (correlationId metadata normalize), chat.ts 1575->1632 (withCorrelationId response wiring + combo-failure log carrying correlationId), chatHelpers.ts new 811 (withCorrelationId helper + reqId threading; was 791<cap pre-feature). Cohesive request/logging chokepoint wiring; structural shrink of chat.ts tracked in #3501.",
|
||||
"src/sse/handlers/chat.ts": 1647,
|
||||
@@ -261,7 +264,7 @@
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2843,
|
||||
"open-sse/executors/huggingchat.ts": 813,
|
||||
"_rebaseline_2026_07_01_v3843_release_5609": "Rebaseline v3.8.43 (PR #5609 release reconciliation). DRIFT dos 109 commits do ciclo: 8 god-files existentes cresceram (ApiManagerPageClient 2983->3017, combos/page 4594->4608, AddApiKeyModal 868->869, providerPageHelpers 974->996, chat.ts 1635->1647, auth.ts 2401->2403, batchProcessor 828->915, combo.ts 3368->3387) + 2 novos acima do cap (huggingchat.ts 813, tests web-cookie-providers-new 827) + 4 test files cresceram. Modularizacao deferida (blast-radius mid-release); congelado no estado atual p/ o proximo ciclo ratchetar daqui.",
|
||||
"src/lib/providers/validation/webProvidersA.ts": 805,
|
||||
"src/lib/providers/validation/webProvidersA.ts": 809,
|
||||
"src/lib/tokenHealthCheck.ts": 830
|
||||
},
|
||||
"testCap": 800,
|
||||
@@ -281,7 +284,7 @@
|
||||
"tests/unit/db-core-init.test.ts": 877,
|
||||
"tests/unit/db-migration-runner.test.ts": 1491,
|
||||
"tests/unit/db-settings-crud.test.ts": 941,
|
||||
"tests/unit/deepseek-web.test.ts": 1081,
|
||||
"tests/unit/deepseek-web.test.ts": 1092,
|
||||
"tests/unit/executor-antigravity.test.ts": 942,
|
||||
"tests/unit/executor-codex.test.ts": 1347,
|
||||
"tests/unit/executor-default-base.test.ts": 1523,
|
||||
@@ -294,10 +297,10 @@
|
||||
"_rebaseline_2026_06_27_5193_antigravity_test": "#5193 own test growth: oauth-providers-config.test.ts 870->873 (+3: antigravity projectId assertion + 50ms tick for the now fire-and-forget onboarding, matching the no-PKCE/no-openid flow).",
|
||||
"tests/unit/oauth-providers-config.test.ts": 873,
|
||||
"tests/unit/perplexity-web.test.ts": 959,
|
||||
"tests/unit/provider-models-route.test.ts": 1628,
|
||||
"tests/unit/provider-models-route.test.ts": 1752,
|
||||
"tests/unit/provider-validation-specialty.test.ts": 2874,
|
||||
"_rebaseline_pr4613_compatible_provider_groups": "Reconcile #4613 already-merged growth: providers-page-utils.test.ts 1004->1052 (+48, buildCompatibleProviderGroups partition unit test). Fast-gate PR->release does not run check:file-size, so this surfaced post-merge.",
|
||||
"tests/unit/providers-page-utils.test.ts": 1052,
|
||||
"tests/unit/providers-page-utils.test.ts": 1109,
|
||||
"tests/unit/reasoning-cache.test.ts": 980,
|
||||
"tests/unit/route-edge-coverage.test.ts": 1234,
|
||||
"tests/unit/search-handler-extended.test.ts": 1124,
|
||||
@@ -306,14 +309,15 @@
|
||||
"tests/unit/token-refresh-service.test.ts": 1353,
|
||||
"tests/unit/translator-friendly-test-bench.test.tsx": 848,
|
||||
"tests/unit/translator-helper-branches.test.ts": 870,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1097,
|
||||
"tests/unit/translator-openai-responses-req.test.ts": 1172,
|
||||
"tests/unit/translator-openai-to-gemini.test.ts": 1579,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 1088,
|
||||
"tests/unit/translator-openai-to-kiro.test.ts": 1093,
|
||||
"tests/unit/translator-resp-gemini-to-openai.test.ts": 1234,
|
||||
"tests/unit/usage-service-hardening.test.ts": 1633,
|
||||
"tests/unit/vscode-token-routes.test.ts": 1212,
|
||||
"tests/unit/combo-config.test.ts": 881,
|
||||
"tests/unit/web-cookie-providers-new.test.ts": 845,
|
||||
"_rebaseline_2026_07_02_5928_base_red": "web-cookie-providers-new.test.ts 845->850: #5928 (test(security) Kimi Web URL host parse, CodeQL #689) grew the file +5 lines and merged into release/v3.8.44 WITHOUT rebaselining, leaving a fast-gates base-red that blocked every subsequent PR->release. Test growth is legitimate (a security regression test); maintainer absorbs the drift here. Frozen at 850.",
|
||||
"tests/unit/web-cookie-providers-new.test.ts": 850,
|
||||
"tests/unit/response-sanitizer.test.ts": 906
|
||||
},
|
||||
"_rebaseline_2026_06_09": "Re-baseline consciente pre-release v3.8.19: 9 arquivos cresceram durante o ciclo (features mergeadas: RequestLoggerV2 +281 request-logger rework, stream +101, combo +73, chatCore +45, catalog +32 fable-5/catalog-flag, callLogs +4, accountFallback +2, usageHistory novo 840) + core.ts +7 (fix resetAllDbModuleState, PR 3536). A catraca segue valendo destes valores — proximo crescimento falha. Decisao: encolher (esp. RequestLoggerV2/chatCore) e a issue #3501 ficam para o ciclo seguinte.",
|
||||
@@ -338,6 +342,7 @@
|
||||
"_rebaseline_2026_06_13_2743d_skipbreaker": "Re-baseline #2743 gap-d (testar consumer do skipProviderBreaker): combo.ts 5131→5162 (+31). Crescimento = extração do boolean inline da decisão de circuit-breaker para o predicado puro EXPORTADO shouldRecordProviderBreakerFailure() (byte-idêntico) + JSDoc, para torná-lo unit-testável sem o harness completo de combo. Shrink estrutural segue com #3501.",
|
||||
"_rebaseline_2026_06_13_v3825_prettier_reconcile": "Reconciliação tardia: o prettier do pre-commit reformatou 3 arquivos DEPOIS da medição de file-size dos PRs, inflando linhas além do baseline setado — OAuthModal.tsx 956→960 e providers.ts 3146→3147 (#3324), combo.ts 5162→5164 (#2743d). Bumps de reformatação automática (sem lógica nova). LIÇÃO: medir file-size pós-commit (pós-prettier), não antes.",
|
||||
"_rebaseline_2026_06_14_3826_release_drift": "Re-baseline release/v3.8.25 drift already documented from #3809 owner changes: ProxyRegistryManager.tsx 1072→1089, sidebarVisibility.ts 990→1006, schemas.ts 2519→2522. This PR does not touch those source files; updating the frozen values restores Fast Quality Gates on the current release branch.",
|
||||
"_rebaseline_2026_07_03_5918_proxy_batch": "PR #5918 own growth: ProxyRegistryManager.tsx 1089→1117 (+28 = wiring the new batch-select/Test-All proxy management components — checkboxes, batch actions bar, health cells). Cohesive UI wiring for the batch-delete/auto-test feature; the reusable pieces already live in separate leaf components (ProxyBatchActions/ProxyCheckboxCell/ProxyHealthCell/useProxyBatchOperations). Legitimate feature growth, not a quality regression.",
|
||||
"_rebaseline_2026_06_14_3825_combo_stickiness": "Re-baseline #3825 (sessionless combo stickiness + reasoning-aware readiness): combo.ts 5164→5198 (+34, pós-prettier). Crescimento = deriveComboSessionKey() + effectiveSessionId threading nos sites de read/write do pin server-side. streamReadinessPolicy.ts não-frozen (sob cap). Lógica coesa no handler de combo; não-extraível.",
|
||||
"_rebaseline_2026_06_14_r3_3835_kiro_pricing": "PR #3835 own growth: pricing.ts 1470→1508 (+38 = missing Kiro pricing rows, claude-sonnet-4.6 etc., pure data). Also carries inherited release/v3.8.25 drift not yet frozen by prior r2 merges: RequestLoggerV2.tsx 1276→1282 (#3820 resizable log table) and combo.ts 5198→5203 (#3811 round-robin replay-response fix). This PR does not touch RequestLoggerV2/combo.ts source; updating the frozen values restores Fast Quality Gates on the current release branch.",
|
||||
"_rebaseline_2026_06_14_r3_3849_transient_hide": "PR #3849 own growth: providerPageHelpers.ts 939→955 (+16 = expanded JSDoc on the auto-hide policy + transient-failure guard in evaluateTestAllEntry). Cohesive helper logic; not extractable.",
|
||||
@@ -355,6 +360,7 @@
|
||||
"_rebaseline_2026_06_17_4107_pending_reaper": "PR #4107 own growth: usageHistory.ts 854->934 (+80 = orphaned-pending-request reaper — sweepStalePendingRequests() evicts pending details older than 15min + a hard 5000 cap, plus an unref'd 5min sweep timer wired lazily into trackPendingRequest). Fixes an unbounded memory leak where abnormally-terminated requests left payload previews in pendingById forever. Cohesive with the existing pending-request bookkeeping (mirrors the normal removal path: decrement counters + cleanup buckets); not extractable.",
|
||||
"_rebaseline_2026_06_17_4116_combo_hedge_listener": "combo.ts: +9 lines from #4116 (detach per-target listener from shared hedge abort signal to fix a listener leak). Behavior-preserving cleanup; 5289 -> 5298.",
|
||||
"_rebaseline_2026_06_20_4355_gpt5x_pro_pricing": "PR #4355 own growth: pricing.ts 1581->1592 (+11 = pure-data pricing rows for openai gpt-5.5-pro + gpt-5.4-pro, closing the $0 gap that tripped the catalog pricing gate after the #4324 sweep added them to the registry; -pro mirrors its base family tier). provider-models-route.test.ts 1616->1618 (+2 = test-only alignment to the intentional opencode-go discovery behavior: owned_by stamp + T39 two-endpoint fail-path fetchCalls). Both are data/test-only; not extractable.",
|
||||
"_rebaseline_2026_07_02_5899_airforce_v1_discovery": "PR #5904 own growth: provider-models-route.test.ts 1628->1752 (+124 = test-only Rule #18 regression guards for the Api Airforce /v1/v1/models discovery bug (#5899): (a) a baseUrl ending in /v1/chat/completions must probe .../v1/models not the doubled .../v1/v1/models, and the host-guard case http://v1; (b) a REDIRECT_BLOCKED on one candidate must continue to the next endpoint instead of aborting the probe loop. Both guards fail on the pre-fix code. Test-only additions cohesive with the existing provider-models discovery suite (shared seedConnection/callRoute harness); not separately extractable without duplicating the harness.",
|
||||
"_rebaseline_2026_06_19_4293_codex_spark_scope": "PR #4293 (isolate Codex Spark quota scope) own growth, MEASURED on the actual merged tree (release/v3.8.30 + #4293). Production: auth.ts 2219->2279 (+60) threads requestedModel into Codex quota-policy/headroom/preflight/P2C scoring so normal Codex and GPT-5.3-Codex-Spark windows are evaluated independently; chatCore.ts 5116->5125 (+9) passes the failing model scope into Codex 429 failover (markCodexScopeRateLimited) instead of a connection-wide rateLimitedUntil write; accountFallback.ts 1727->1731 (+4) scopes Codex model-lock keys to codex vs spark. Heavy parsing/display logic lives in new leaf helpers under the cap (codexQuotaScopes.ts, codexUsageQuotas.ts, codexFailover.ts). Tests: account-fallback-service 1544->1569, executor-codex 1336->1339, sse-auth 1527->1553, usage-service-hardening 1612->1633 (added Spark-scope regression coverage). Cohesive wiring at existing selection/failover lockout boundaries; not extractable.",
|
||||
"_rebaseline_2026_06_20_4447_openai_gpt41mini_o_mini_pricing": "PR #4447 own growth: pricing.ts 1592->1620 (+28 = pure-data pricing rows closing the null/$0 gap for registry-exposed OpenAI ids gpt-4.1-mini, gpt-4.1-nano, o3-mini, o4-mini that tripped the catalog pricing gate; getPricingForModel does an exact lookup, so a missing key resolves to null. Official OpenAI per-1M prices + the table's derived-field convention (reasoning=output*1.5, cache_creation=input, cached=official). Restore-green for a pre-existing release/v3.8.32 red surfaced by #4432's __RUN_ALL__ run. Cohesive data; not extractable.",
|
||||
"_rebaseline_2026_06_20_web_cookie_validator_shadow_fix": "validation.ts 4518->4522 (+4 = move the generic web-cookie validateWebCookieProvider dispatch from the TOP of validateProviderApiKey to a FALLBACK after SPECIALTY_VALIDATORS, plus a comment, so #4023's generic AUTH_007 ping no longer shadows the rich per-provider validators (grok-web #3474 IP-reputation/Cloudflare, chatgpt-web cf-mitigated, claude/gemini/copilot/qwen/t3-web). Restores provider-validation-specialty.test.ts (112/112) while keeping web-cookie-auth007 (5/5). Behavior fix at an existing dispatch boundary; not extractable.",
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
"_comment": "Catraca de qualidade. 'down' = nao pode aumentar; 'up' = nao pode cair. Atualize via 'npm run quality:ratchet -- --update' (somente quando melhora). Cada valor e um numero REAL medido, nunca um chute. Cobertura entra na Fase 4 a partir de um run de cobertura mergeada no CI.",
|
||||
"metrics": {
|
||||
"eslintWarnings": {
|
||||
"value": 4199,
|
||||
"value": 4256,
|
||||
"_rebaseline_2026_07_03_v3844_review_prs_fix_batch": "4199->4256 (+57). Inherited v3.8.44 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrue unmeasured across the cycle). 4256 = measured by `node scripts/quality/collect-metrics.mjs` on the release tip 72ee80649 during the /review-prs fix-batch round. The round's own merges (#5958 SSE-accept, #5988 deepseek-web, #6013/#5974 retry-after-json, #5975 embeddings-proxy, #5973 non-json-guard) plus the parallel-session merge burst into release/v3.8.44 account for the delta; all `any`-warn-allowed in open-sse/ + tests/. Cyclomatic is already green (2012 < baseline 2015) and needs no bump. Tighten via --require-tighten next cycle.",
|
||||
"_rebaseline_2026_07_02_v3843_release_close": "4158->4199 (+41). v3.8.43 release-close drift measured by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across the ~120 commits merged after the mid-cycle 4158 rebaseline — the compression T02/T05/T06/T07/T08/T10 engine families, memory typed decay, provider adds Ollama/SenseNova, ~55 SSE/translator/kiro/oauth/dashboard fixes, and the god-file decomposition wave). Trust-but-verify: measured 4199 via `npm run lint` on the release-finalize working tree INCLUDING my changes (CHANGELOG/i18n/README docs + kiro pricing data entry + the 3 base-red CODE fixes: opencode fabrication removal, resolveEffectiveKey type-widen, openai-to-claude claudeFinishEmitted flag + 4 test-alignment files + golden snapshot regen) — the code fixes NET-REMOVE lines and add no `any`/unused, and lint reported 4199 both before and after them, so all +41 is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
"direction": "down",
|
||||
"_rebaseline_2026_07_01_v3843_release": "4121->4158 (+37). v3.8.43 cycle drift surfaced by the release-green pre-flight; the Quality Ratchet does NOT run on PR->release fast-gates, so warnings accrued unmeasured across this cycle. 4158 = the value measured by the CI Quality Ratchet on the release tip fce85136c (release PR #5609). Trust-but-verify: the fix/release-v3843-ci-reds branch touches only test files (rtk-mcp-tools de-flake, compression-studio e2e anchor, oauth-error-linkify hardening test) + src/shared/utils/linkify.ts (eslint-clean, 0 warnings) + stryker.conf.json + this baseline -> 0 new warnings, so all +37 is inherited cycle drift (any warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle.",
|
||||
@@ -114,7 +115,10 @@
|
||||
"_rebaseline_2026_06_26_v3837_release": "343->345. v3.8.37 cycle drift surfaced by the release-green pre-flight (the Quality Ratchet does NOT run on PR->release fast-gates, so warnings/complexity accrued unmeasured across this cycle's 76 commits — provider adds DGrid/Pioneer/xAI, headroom proxy lifecycle #4649, ~50 SSE/translator fixes, Engine Combos #5062). Trust-but-verify: this release-finalize working tree touches ONLY CHANGELOG.md, docs/i18n/*/CHANGELOG.md mirrors, and these baselines — 0 production-code change, so all drift is inherited cycle drift (`any` warn-allowed in open-sse/ + tests/). Tighten via --require-tighten next cycle."
|
||||
},
|
||||
"cognitiveComplexity": {
|
||||
"value": 856,
|
||||
"value": 861,
|
||||
"_rebaseline_2026_07_03_v3844_review_prs_fix_batch": "860->861 (+1). Inherited v3.8.44 cycle drift surfaced by the release-green pre-flight during the /review-prs fix-batch round; check:cognitive-complexity measures 861 on the release tip 72ee80649. Negligible +1 from the round's / parallel-session merge burst (cognitive-complexity does NOT run on PR->release fast-gates). Structural shrink tracked in #3501. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_02_v3844_post_5939": "859->860 (+1). Same inherited post-3a3d618fe release drift as the complexity note of this date; this PR touches one CLI line (nullish default, no new function over the threshold) and one test file (not scanned). Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_02_v3844_merge_burst": "856->859 (+3). Inherited v3.8.44 cycle drift surfaced by PR #5939: check:cognitive-complexity measures 859 on BOTH the pristine release tip (3a3d618fe) and this PR's merged HEAD — identical, so the PR is cognitive-net-zero (its DiscoveryPageClient was refactored into hooks/sub-components during its own CI cycle). Drift from the 2026-07-02 merge burst into release/v3.8.44. Tighten via --update next cycle.",
|
||||
"_rebaseline_2026_07_02_5798_release_green": "845->856 (+11). Inherited v3.8.43 cycle drift surfaced by the release-green unblock #5798 / PR #5896: check:cognitive-complexity measures 856 on BOTH the pristine release tip (0d3875a98) and this PR's HEAD — identical, so the PR is cognitive-net-zero (it touches only gate scripts, docs, baselines and test files). Drift from the 2026-07-01/02 merge burst. Tighten via --update next cycle.",
|
||||
"direction": "down",
|
||||
"_rebaseline_2026_07_01_v3843_release": "842->845 (+3). v3.8.43 cycle drift, surfaced after check:mutation-test-coverage was fixed (masked behind it in the Fast Quality Gates chain). 845 = measured by check:cognitive-complexity on BOTH fce85136c and 6d7060e21 (identical) — all +3 is inherited cycle drift; the 5 CI fixes add 0 (safeHttpHref cognitive ~2, under the 15 threshold). Tighten via --update next cycle.",
|
||||
|
||||
@@ -71,6 +71,7 @@ Lookup material — API surface, environment variables, CLI flags, provider cata
|
||||
|
||||
- [API_REFERENCE.md](reference/API_REFERENCE.md) — REST API endpoints and shapes.
|
||||
- [PROVIDER_REFERENCE.md](reference/PROVIDER_REFERENCE.md) — auto-generated provider catalog (do not edit by hand).
|
||||
- [PROVIDER_PLUGIN_MANIFEST.md](reference/PROVIDER_PLUGIN_MANIFEST.md) — sidecar-safe provider plugin contract for Bifrost and CLIProxyAPI migration.
|
||||
- [openapi.yaml](openapi.yaml) — OpenAPI spec for the public API.
|
||||
- [ENVIRONMENT.md](reference/ENVIRONMENT.md) — environment variables reference.
|
||||
- [FEATURE_FLAGS.md](reference/FEATURE_FLAGS.md) — feature flags and their defaults.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Extending the Compression Pipeline"
|
||||
version: 3.8.40
|
||||
lastUpdated: 2026-06-28
|
||||
version: 3.8.44
|
||||
lastUpdated: 2026-07-02
|
||||
---
|
||||
|
||||
# Extending the Compression Pipeline
|
||||
@@ -512,6 +512,62 @@ To drive it from config, set `mode: "stacked"` and provide the step array under
|
||||
|
||||
---
|
||||
|
||||
## Upstream Sync Policy
|
||||
|
||||
OmniRoute's compression engines credit several upstream projects in the README
|
||||
("inspired by RTK, Caveman, LLMLingua-2, Troglodita"). A common contributor
|
||||
question is: **when upstream RTK adds a new tool filter or Caveman adds a rule
|
||||
pack, how does that reach OmniRoute?** This section is the authoritative answer.
|
||||
|
||||
### Vendored copies vs. independent implementations
|
||||
|
||||
| Engine | Relationship to upstream | Location |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
||||
| **RTK** | **Independent reimplementation** (inspired-by, not a copy) | `open-sse/services/compression/engines/rtk/` |
|
||||
| **Caveman** | **Independent reimplementation** (inspired-by) | `open-sse/services/compression/engines/cavemanAdapter.ts` |
|
||||
| **Headroom** | Mostly internal; only the `gcf/` codec is **genuinely vendored** from `gcf-typescript` (MIT, SPDX-marked, generic profile only) | `open-sse/services/compression/engines/headroom/gcf/` |
|
||||
| **LLMLingua-2 / Troglodita** | Inspired-by (drive the `llmlingua` + `session-dedup` engines) | `open-sse/services/compression/engines/llmlingua/`, `session-dedup` |
|
||||
|
||||
Key point: **RTK and Caveman are clean-room TypeScript implementations of the
|
||||
_ideas_ (filter rules, rule packs), not vendored source trees.** There is no
|
||||
upstream copy to `git pull` from — which is exactly why the README says
|
||||
"inspired by" rather than "bundled".
|
||||
|
||||
### How upstream improvements are merged
|
||||
|
||||
There is **no automated upstream-release tracking and no `compression-sync`
|
||||
label** — by design. Because the engines are reimplementations, an upstream RTK
|
||||
filter or Caveman rule pack is not merged as code; it is **re-expressed as a new
|
||||
rule/filter in OmniRoute's own format** (see
|
||||
[COMPRESSION_RULES_FORMAT.md](./COMPRESSION_RULES_FORMAT.md)) and lands ad-hoc via
|
||||
a normal PR. The extension points above (custom engine, language pack, RTK filter)
|
||||
are the sanctioned way to contribute one.
|
||||
|
||||
Recent examples of exactly this flow:
|
||||
|
||||
- RTK filters for Gradle & `dotnet` build output (v3.8.42)
|
||||
- RTK filters for kubectl / docker-build / composer / gh (#2824)
|
||||
- Caveman Indonesian language pack (#3975), plus German / French / Japanese / Chinese packs
|
||||
|
||||
### Headroom (input-compression proxy)
|
||||
|
||||
Headroom is **fully internal** — a pinned vendored `gcf` codec snapshot plus
|
||||
OmniRoute's own `smartcrusher` / `toon` / `tabular` layers. There is no live
|
||||
upstream to track beyond the vendored copy; updates to `gcf` are refreshed
|
||||
manually when the codec changes and re-validated against the compression budget
|
||||
gate (`check:compression-budget`).
|
||||
|
||||
### Proposing an upstream-inspired improvement
|
||||
|
||||
1. **Don't vendor** — re-express the upstream rule/filter in OmniRoute's format.
|
||||
2. Add it via the matching extension point below (language pack, RTK filter, or
|
||||
custom engine).
|
||||
3. Reference the upstream project in the PR description (attribution), not by
|
||||
copying its license-bearing source.
|
||||
4. Include tests and confirm the `check:compression-budget` gate still passes.
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
### Engine Development
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
---
|
||||
title: "Embedded Services"
|
||||
description: "Reference for 9Router and CLIProxyAPI"
|
||||
description: "Reference for 9Router, CLIProxyAPI, Mux, and Bifrost"
|
||||
---
|
||||
|
||||
# Embedded Services
|
||||
|
||||
> **Version:** v3.8.4
|
||||
> **Last updated:** 2026-06-28
|
||||
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI).
|
||||
> **Version:** v3.8.44
|
||||
> **Last updated:** 2026-07-03
|
||||
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI, Mux, Bifrost).
|
||||
|
||||
Embedded services are locally-installed process sidecar tools that OmniRoute installs, supervises, and
|
||||
exposes as first-class routing targets. Unlike external providers (which are reached over the internet
|
||||
@@ -32,18 +32,20 @@ via API keys), embedded services run on the same machine as OmniRoute and commun
|
||||
|
||||
### Why embedded services?
|
||||
|
||||
Two services are embedded as of v3.8.4:
|
||||
Four services are embedded as of v3.8.44:
|
||||
|
||||
| Service | npm package | Default port | Purpose |
|
||||
| --------------- | ---------------------------------------------- | :----------: | ---------------------------------------------------------------------------------------------------- |
|
||||
| **9Router** | `9router` | 20130 | AI router that OmniRoute can use as a sub-provider. Models exposed as `9router/{sub}/{model}` |
|
||||
| **CLIProxyAPI** | `@anthropic/cli-proxy` (via `cliproxy` binary) | auto | Local proxy adapter for Anthropic CLI auth flows. Provides fallback routing when OAuth tokens expire |
|
||||
| Service | npm package | Default port | Purpose |
|
||||
| --------------- | ----------------------------------------------- | :----------: | ------------------------------------------------------------------------------------------------------------------ |
|
||||
| **9Router** | `9router` | 20130 | AI router that OmniRoute can use as a sub-provider. Models exposed as `9router/{sub}/{model}` |
|
||||
| **CLIProxyAPI** | `@anthropic/cli-proxy` (via `cliproxy` binary) | auto | Local proxy adapter for Anthropic CLI auth flows. Provides fallback routing when OAuth tokens expire |
|
||||
| **Mux** | `mux` (headless `mux server`) | 8322 | Local agent-orchestration daemon (coder/mux). Lifecycle-managed only — not a routing target (no LLM proxying). |
|
||||
| **Bifrost** | `@maximhq/bifrost` | 8080 | Go AI-gateway relay backend. When running, auto-selected by the relay route (`/v1/relay/`) |
|
||||
|
||||
Both follow the same supervisory model:
|
||||
All four follow the same supervisory model:
|
||||
|
||||
- OmniRoute installs them under `DATA_DIR/services/{name}/` (isolated from OmniRoute's own `package.json`)
|
||||
- OmniRoute spawns and monitors them as child processes
|
||||
- OmniRoute injects an ephemeral API key into the child's environment and rotates it without downtime
|
||||
- OmniRoute injects an ephemeral API key into the child's environment and rotates it without downtime (where applicable)
|
||||
- All management routes (`/api/services/*`) are **LOCAL_ONLY** — accessible only from loopback (hard rule #17)
|
||||
|
||||
### Key decisions (from design plan)
|
||||
@@ -54,7 +56,7 @@ Both follow the same supervisory model:
|
||||
| Installation mechanism | `npm install {package}` via `execFile` (no shell interpolation) |
|
||||
| Consumption mode | Provider registered as `9router/{sub}/{model}` in routing engine |
|
||||
| API key management | OmniRoute generates, encrypts at-rest (AES-256-GCM), and injects via env |
|
||||
| Dashboard location | `/dashboard/providers/services` (two tabs) |
|
||||
| Dashboard location | `/dashboard/providers/services` (three tabs) |
|
||||
| Auto-start | Toggle per service, default OFF |
|
||||
|
||||
---
|
||||
@@ -64,12 +66,13 @@ Both follow the same supervisory model:
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────────┐
|
||||
│ Layer 1 — UI │
|
||||
│ /dashboard/providers/services (tabs: CLIProxyAPI | 9Router) │
|
||||
│ /dashboard/providers/services (tabs: CLIProxyAPI | 9Router | Mux)│
|
||||
│ Logs live (SSE), Start/Stop/Restart/Update, Settings, Install │
|
||||
│ │
|
||||
│ src/app/(dashboard)/dashboard/providers/services/ │
|
||||
│ ├── page.tsx Shell + tab routing by ?tab= │
|
||||
│ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab│
|
||||
│ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab,│
|
||||
│ │ MuxServiceTab │
|
||||
│ └── components/ ServiceStatusCard, ServiceLifecycleButtons,│
|
||||
│ ServiceLogsPanel, ApiKeyCard, ... │
|
||||
└──────────────────────┬─────────────────────────────────────────────┘
|
||||
@@ -81,6 +84,8 @@ Both follow the same supervisory model:
|
||||
│ rotate-key|status|auto-start|logs} │
|
||||
│ /api/services/cliproxy/{install|start|stop|restart|update| │
|
||||
│ status|auto-start|logs} │
|
||||
│ /api/services/mux/{install|start|stop|restart|update| │
|
||||
│ status|auto-start|logs} │
|
||||
│ /dashboard/providers/services/9router/embed/[...path] │
|
||||
│ (reverse HTTP + WebSocket proxy → 9Router upstream) │
|
||||
│ │
|
||||
@@ -106,7 +111,8 @@ Both follow the same supervisory model:
|
||||
│ modelSync.ts Periodic GET /v1/models → service_models table │
|
||||
│ ringBuffer.ts Circular log buffer (5 MB per service) │
|
||||
│ healthCheck.ts Polling HTTP health probe │
|
||||
│ installers/ ninerouter.ts, cliproxy.ts (installer adapters)│
|
||||
│ installers/ ninerouter.ts, cliproxy.ts, mux.ts │
|
||||
│ (installer adapters) │
|
||||
└──────────────────────┬─────────────────────────────────────────────┘
|
||||
│ OpenAI-compatible HTTP (loopback)
|
||||
┌──────────────────────▼─────────────────────────────────────────────┐
|
||||
@@ -123,6 +129,10 @@ Both follow the same supervisory model:
|
||||
│ open-sse/config/providerRegistry.ts │
|
||||
│ Models stored as "9router/{sub}/{model}" (prefixed). │
|
||||
│ Synced every 5 min by modelSync.ts. │
|
||||
│ │
|
||||
│ Mux is lifecycle-managed ONLY (Layers 1-3) — it is an agent- │
|
||||
│ orchestration daemon, not an LLM proxy, so it has no Layer 4 │
|
||||
│ executor/provider entry and is never a routing target. │
|
||||
└────────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
@@ -139,6 +149,7 @@ Both follow the same supervisory model:
|
||||
| `src/lib/services/healthCheck.ts` | HTTP health probe (configurable interval) |
|
||||
| `src/lib/services/installers/ninerouter.ts` | npm install/update/uninstall for 9Router |
|
||||
| `src/lib/services/installers/cliproxy.ts` | npm install/update/uninstall for CLIProxyAPI |
|
||||
| `src/lib/services/installers/mux.ts` | npm install/update/uninstall for Mux |
|
||||
| `src/app/api/services/9router/_lib.ts` | `getOrInitSupervisor()` helper |
|
||||
| `src/app/api/services/[name]/logs/route.ts` | Shared SSE logs endpoint |
|
||||
| `open-sse/executors/ninerouter.ts` | Provider executor (Layer 4) |
|
||||
@@ -434,12 +445,56 @@ config) and `status` includes fewer fields.
|
||||
| `GET` | `/api/services/cliproxy/status` | Live + DB status (no `apiKeyMasked`) |
|
||||
| `POST` | `/api/services/cliproxy/auto-start` | Toggle auto-start |
|
||||
|
||||
The shared `GET /api/services/{name}/logs` endpoint (see §4.1) works for both
|
||||
services using the `[name]` dynamic segment.
|
||||
The shared `GET /api/services/{name}/logs` endpoint (see §4.1) works for all
|
||||
four services using the `[name]` dynamic segment.
|
||||
|
||||
---
|
||||
|
||||
### 4.3 Reverse proxy (9Router dashboard embed)
|
||||
### 4.3 Mux endpoints (7 routes)
|
||||
|
||||
Mux has the same endpoint shape as CLIProxyAPI — no `rotate-key` route in the API
|
||||
surface (the bearer token is generated the same way as 9Router's via
|
||||
`getOrCreateApiKey("mux")` and injected via the `MUX_SERVER_AUTH_TOKEN` env var, but
|
||||
there is no dedicated rotation endpoint yet). Mux is lifecycle-managed only: unlike
|
||||
9Router, it has no Layer 4 executor and is never registered as a routing provider.
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | -------------------------------- | ------------------------------------- |
|
||||
| `POST` | `/api/services/mux/install` | Install Mux from npm (`npm i mux`) |
|
||||
| `POST` | `/api/services/mux/start` | Start Mux (`mux server`) |
|
||||
| `POST` | `/api/services/mux/stop` | Stop Mux |
|
||||
| `POST` | `/api/services/mux/restart` | Restart Mux |
|
||||
| `POST` | `/api/services/mux/update` | Update to newer npm version |
|
||||
| `GET` | `/api/services/mux/status` | Live + DB status |
|
||||
| `POST` | `/api/services/mux/auto-start` | Toggle auto-start |
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Bifrost endpoints (7 routes)
|
||||
|
||||
Bifrost is a Go AI-gateway relay backend (`@maximhq/bifrost`). It uses the same
|
||||
endpoint shape as CLIProxyAPI (no `rotate-key` — Bifrost manages its own provider
|
||||
keys in `config.json` under its `-app-dir`).
|
||||
|
||||
| Method | Path | Description |
|
||||
| ------ | ---------------------------------- | ------------------------------------------------------ |
|
||||
| `POST` | `/api/services/bifrost/install` | Install Bifrost from npm (`@maximhq/bifrost`) |
|
||||
| `POST` | `/api/services/bifrost/start` | Start Bifrost on port 8080 (default) |
|
||||
| `POST` | `/api/services/bifrost/stop` | Stop Bifrost |
|
||||
| `POST` | `/api/services/bifrost/restart` | Restart Bifrost |
|
||||
| `POST` | `/api/services/bifrost/update` | Update to newer version |
|
||||
| `GET` | `/api/services/bifrost/status` | Live + DB status |
|
||||
| `POST` | `/api/services/bifrost/auto-start` | Toggle auto-start |
|
||||
| `GET` | `/api/services/bifrost/logs` | SSE log tail (via shared `[name]/logs` dynamic route) |
|
||||
|
||||
**Routing wiring:** When `BIFROST_BASE_URL` is unset and the supervised Bifrost
|
||||
instance is running, `getBifrostRoutingConfig()` (in `routingBackend.ts`) automatically
|
||||
uses `http://127.0.0.1:{port}` as the relay base URL. Explicit `BIFROST_BASE_URL` env
|
||||
always takes precedence.
|
||||
|
||||
---
|
||||
|
||||
### 4.4 Reverse proxy (9Router dashboard embed)
|
||||
|
||||
The dashboard embeds the 9Router web UI inside an iframe via an internal reverse
|
||||
proxy at:
|
||||
@@ -486,14 +541,20 @@ matrix.
|
||||
|
||||
### API key injection
|
||||
|
||||
9Router requires an API key for its own HTTP endpoints. OmniRoute:
|
||||
9Router and Mux require an API key/bearer token for their own HTTP endpoints.
|
||||
OmniRoute:
|
||||
|
||||
1. Generates a key via `crypto.randomBytes(32).toString("base64url")` with a
|
||||
service-specific prefix (`nr_` for 9Router).
|
||||
service-specific prefix (`nr_` for 9Router, `mx_` for Mux).
|
||||
2. Encrypts it at-rest using AES-256-GCM (same cipher used for provider credentials).
|
||||
3. Decrypts and injects it as `NINEROUTER_API_KEY` environment variable at spawn time.
|
||||
3. Decrypts and injects it as an environment variable at spawn time —
|
||||
`NINEROUTER_API_KEY` for 9Router, `MUX_SERVER_AUTH_TOKEN` for Mux (never a CLI
|
||||
flag, so the token never appears in `ps`/process listings).
|
||||
4. Never returns the plaintext key in any HTTP response.
|
||||
|
||||
CLIProxyAPI does not require an injected key (it authenticates via the host's
|
||||
existing CLI config).
|
||||
|
||||
### SSRF defense
|
||||
|
||||
The reverse HTTP proxy (`/dashboard/.../embed/[...path]`) is hardcoded to forward
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
|
||||
## [3.8.31] — 2026-06-20
|
||||
|
||||
## [3.8.44] — TBD
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
_TBD_
|
||||
|
||||
### 🔧 Bug Fixes
|
||||
|
||||
_TBD_
|
||||
|
||||
### 📝 Maintenance
|
||||
|
||||
- **chore(release):** release-pipeline hardening — `check:test-masking` (vs `origin/main`) is now a HARD gate in the release-green pre-flight (`validate-release-green.mjs`), so a non-allowlisted net-assert reduction surfaces locally instead of in a ~40-min CI layer on the release PR; plus two reconciliation helpers — `npm run release:contributors` (reproducible `### 🙌 Contributors` table via a parenthetical-group parser) and `npm run release:uncovered` (lists commits with no CHANGELOG bullet). ([#5926](https://github.com/diegosouzapw/OmniRoute/pull/5926) — thanks @diegosouzapw)
|
||||
|
||||
- **chore(ci):** the `check:pr-evidence` FAIL report now tells you that editing the PR body does not re-run the gate (`ci.yml` ignores the `edited` event) — push a commit to re-validate. ([#5944](https://github.com/diegosouzapw/OmniRoute/pull/5944) — thanks @diegosouzapw)
|
||||
|
||||
---
|
||||
|
||||
## [3.8.43] — 2026-07-02
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 3.8.43
|
||||
version: 3.8.44
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
@@ -3444,6 +3444,276 @@ paths:
|
||||
"400":
|
||||
description: Invalid request body
|
||||
|
||||
/api/services/mux/install:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Install Mux from npm
|
||||
description: >-
|
||||
Installs the `mux` npm package (coder/mux — local agent-orchestration
|
||||
daemon) under DATA_DIR/services/mux/. **LOCAL_ONLY** — loopback only.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
default: latest
|
||||
responses:
|
||||
"200":
|
||||
description: Install succeeded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
installedVersion:
|
||||
type: string
|
||||
"400":
|
||||
description: Invalid request body
|
||||
"500":
|
||||
description: npm install failed
|
||||
|
||||
/api/services/mux/start:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Start Mux
|
||||
description: >-
|
||||
Spawns `mux server --host 127.0.0.1 --port <port>`. Idempotent if
|
||||
already running. **LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service started
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServiceStatus"
|
||||
"409":
|
||||
description: Mux is not installed
|
||||
"503":
|
||||
description: Start failed
|
||||
|
||||
/api/services/mux/stop:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Stop Mux
|
||||
description: >-
|
||||
Gracefully stops Mux. Idempotent.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service stopped
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServiceStatus"
|
||||
|
||||
/api/services/mux/restart:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Restart Mux
|
||||
description: >-
|
||||
stop() then start() under the operation lock.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service restarted
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServiceStatus"
|
||||
|
||||
/api/services/mux/update:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Update Mux to a newer npm version
|
||||
description: >-
|
||||
Stops, installs newer version, restarts.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
default: latest
|
||||
responses:
|
||||
"200":
|
||||
description: Update succeeded
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
installedVersion:
|
||||
type: string
|
||||
"500":
|
||||
description: Update failed
|
||||
|
||||
/api/services/mux/status:
|
||||
get:
|
||||
tags: [Embedded Services]
|
||||
summary: Get Mux status
|
||||
description: >-
|
||||
Returns live supervisor state and DB metadata.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Status response
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/ServiceStatus"
|
||||
|
||||
/api/services/mux/auto-start:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Toggle Mux auto-start
|
||||
description: >-
|
||||
When enabled, Mux starts automatically on the next OmniRoute boot.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [enabled]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
responses:
|
||||
"200":
|
||||
description: Auto-start flag updated
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
autoStart:
|
||||
type: boolean
|
||||
"400":
|
||||
description: Invalid request body
|
||||
|
||||
/api/services/bifrost/install:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Install Bifrost
|
||||
description: >-
|
||||
Installs the `@maximhq/bifrost` npm package under DATA_DIR/services/bifrost/.
|
||||
The package downloads the Go binary on first run. Accepts an optional `version`
|
||||
field (semver or `latest`). **LOCAL_ONLY** — loopback only.
|
||||
requestBody:
|
||||
required: false
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
version:
|
||||
type: string
|
||||
default: latest
|
||||
responses:
|
||||
"200":
|
||||
description: Installation result
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
ok:
|
||||
type: boolean
|
||||
installedVersion:
|
||||
type: string
|
||||
installPath:
|
||||
type: string
|
||||
durationMs:
|
||||
type: number
|
||||
|
||||
/api/services/bifrost/start:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Start Bifrost
|
||||
description: Starts the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service status after start
|
||||
"409":
|
||||
description: Bifrost is not installed
|
||||
|
||||
/api/services/bifrost/stop:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Stop Bifrost
|
||||
description: Stops the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service status after stop
|
||||
|
||||
/api/services/bifrost/restart:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Restart Bifrost
|
||||
description: Restarts the supervised Bifrost process. **LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Service status after restart
|
||||
"409":
|
||||
description: Bifrost is not installed
|
||||
|
||||
/api/services/bifrost/update:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Update Bifrost
|
||||
description: >-
|
||||
Updates Bifrost to the latest npm version. Stops the running process,
|
||||
installs the new version, and restarts if it was previously running.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Update result
|
||||
|
||||
/api/services/bifrost/status:
|
||||
get:
|
||||
tags: [Embedded Services]
|
||||
summary: Get Bifrost status
|
||||
description: Returns live and DB status for the supervised Bifrost service. **LOCAL_ONLY** — loopback only.
|
||||
responses:
|
||||
"200":
|
||||
description: Bifrost service status
|
||||
|
||||
/api/services/bifrost/auto-start:
|
||||
post:
|
||||
tags: [Embedded Services]
|
||||
summary: Toggle Bifrost auto-start
|
||||
description: >-
|
||||
When enabled, Bifrost starts automatically on the next OmniRoute boot.
|
||||
**LOCAL_ONLY** — loopback only.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [enabled]
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
responses:
|
||||
"204":
|
||||
description: Auto-start flag updated
|
||||
"400":
|
||||
description: Invalid request body
|
||||
|
||||
/api/services/{name}/logs:
|
||||
get:
|
||||
tags: [Embedded Services]
|
||||
@@ -5264,6 +5534,214 @@ paths:
|
||||
$ref: "#/components/responses/InternalError"
|
||||
"503":
|
||||
description: Generator module not available
|
||||
/api/v1/ocr:
|
||||
post:
|
||||
tags:
|
||||
- Images
|
||||
summary: Document OCR
|
||||
description: >-
|
||||
Mistral OCR–compatible document OCR endpoint. Accepts a JSON body
|
||||
referencing a document/image and returns extracted text. Success
|
||||
responses carry the `X-OmniRoute-*` cost-telemetry headers.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
model:
|
||||
type: string
|
||||
document:
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
description: OCR result with extracted text.
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/audio/translations:
|
||||
post:
|
||||
tags:
|
||||
- Audio
|
||||
summary: Translate audio to English
|
||||
description: >-
|
||||
OpenAI Whisper–compatible audio translation (multipart/form-data).
|
||||
Unlike `/api/v1/audio/transcriptions`, output is always English
|
||||
regardless of the source language. Success responses carry the
|
||||
`X-OmniRoute-*` cost-telemetry headers.
|
||||
security:
|
||||
- BearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
required:
|
||||
- file
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
format: binary
|
||||
model:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: English translation of the audio.
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/providers/suggested-models:
|
||||
get:
|
||||
tags:
|
||||
- Providers
|
||||
summary: Suggested media models
|
||||
description: >-
|
||||
Read-only server-side proxy to the public HuggingFace Hub models search
|
||||
API, used by the dashboard to suggest models for a media provider kind
|
||||
without exposing an HF token client-side. Never accepts or returns
|
||||
credentials.
|
||||
parameters:
|
||||
- name: type
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: Media kind to search for (e.g. `image`, `audio`, `video`).
|
||||
responses:
|
||||
"200":
|
||||
description: List of suggested HuggingFace Hub models.
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/provider-plugin-manifest:
|
||||
get:
|
||||
tags:
|
||||
- Providers
|
||||
summary: Provider plugin manifest
|
||||
description: Returns the manifest describing installed provider plugins.
|
||||
responses:
|
||||
"200":
|
||||
description: Provider plugin manifest.
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/keys/{id}/devices:
|
||||
get:
|
||||
tags:
|
||||
- API Keys
|
||||
summary: List devices for an API key
|
||||
description: >-
|
||||
Lists the distinct devices (masked IP + User-Agent fingerprints)
|
||||
tracked for an API key by the in-memory device tracker. IPs are masked
|
||||
before storage; the route never sees the raw client IP.
|
||||
x-internal: true
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: Distinct devices seen for the API key.
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
"404":
|
||||
$ref: "#/components/responses/NotFound"
|
||||
/api/settings/purge-usage-history:
|
||||
post:
|
||||
tags:
|
||||
- Settings
|
||||
summary: Purge usage history
|
||||
description: Dashboard-only. Purges stored usage-history records.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Usage history purged.
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/oauth/codex/import-token:
|
||||
post:
|
||||
tags:
|
||||
- OAuth
|
||||
summary: Import a Codex connection from a bare access token
|
||||
description: >-
|
||||
Dashboard-only. Creates a Codex (ChatGPT/OpenAI) connection from a raw
|
||||
access token with no refresh token (authType `access_token`).
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Connection imported.
|
||||
"400":
|
||||
$ref: "#/components/responses/BadRequest"
|
||||
"401":
|
||||
$ref: "#/components/responses/Unauthorized"
|
||||
/api/cli-tools/crush-settings:
|
||||
get:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Read Crush CLI OmniRoute config
|
||||
description: Local-only. Reads the OmniRoute provider block in Crush's config.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Current Crush config state.
|
||||
post:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Write Crush CLI OmniRoute config
|
||||
description: Local-only. Registers OmniRoute as an `openai-compat` provider in Crush's config.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Crush config updated.
|
||||
delete:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Remove OmniRoute from Crush CLI config
|
||||
description: Local-only. Removes the OmniRoute provider block from Crush's config.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Crush config entry removed.
|
||||
/api/cli-tools/codewhale-settings:
|
||||
get:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Read CodeWhale CLI OmniRoute config
|
||||
description: >-
|
||||
Local-only. Reads the OmniRoute config block from
|
||||
`~/.codewhale/config.toml` (with `~/.deepseek/config.toml` legacy
|
||||
fallback).
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: Current CodeWhale config state.
|
||||
post:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Write CodeWhale CLI OmniRoute config
|
||||
description: Local-only. Writes the OmniRoute config block in CodeWhale TOML format.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: CodeWhale config updated.
|
||||
delete:
|
||||
tags:
|
||||
- CLI Tools
|
||||
summary: Remove OmniRoute from CodeWhale CLI config
|
||||
description: Local-only. Removes the OmniRoute config block from CodeWhale's config.
|
||||
x-internal: true
|
||||
responses:
|
||||
"200":
|
||||
description: CodeWhale config entry removed.
|
||||
|
||||
components:
|
||||
securitySchemes:
|
||||
|
||||
@@ -18,6 +18,7 @@ Complete reference for all OmniRoute API endpoints.
|
||||
- [Embeddings](#embeddings)
|
||||
- [Image Generation](#image-generation)
|
||||
- [List Models](#list-models)
|
||||
- [Provider Plugin Manifest](#provider-plugin-manifest)
|
||||
- [Compatibility Endpoints](#compatibility-endpoints)
|
||||
- [Files API](#files-api)
|
||||
- [Batches API](#batches-api)
|
||||
@@ -178,6 +179,22 @@ Selecting this id (e.g. in a Claude Code config that always attaches a `thinking
|
||||
|
||||
---
|
||||
|
||||
## Provider Plugin Manifest
|
||||
|
||||
```bash
|
||||
GET /api/v1/provider-plugin-manifest
|
||||
```
|
||||
|
||||
Returns the JSON-safe provider plugin manifest used by Bifrost, CLIProxyAPI, and
|
||||
future sidecar routers. The response is generated from the TypeScript provider
|
||||
registry and intentionally excludes OAuth client secrets, runtime environment
|
||||
resolution, executor functions, request headers, and account data.
|
||||
|
||||
Use this endpoint when a sidecar runs out-of-process and cannot import
|
||||
`open-sse/config/providerPluginManifestRegistry.ts` directly.
|
||||
|
||||
---
|
||||
|
||||
## Compatibility Endpoints
|
||||
|
||||
| Method | Path | Format |
|
||||
|
||||
@@ -12,7 +12,7 @@ OmniRoute integrates with three categories of CLI tools spread across three dedi
|
||||
|
||||
| Page | Route | Concept | Count |
|
||||
| -------------- | ----------------------- | ------------------------------------------------------------------------- | ------------ |
|
||||
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 19 |
|
||||
| **CLI Code's** | `/dashboard/cli-code` | Coding tools you point at OmniRoute (Client → CLI → OmniRoute → Provider) | 20 |
|
||||
| **CLI Agents** | `/dashboard/cli-agents` | Autonomous agents you point at OmniRoute (same flow, broader scope) | 6 |
|
||||
| **ACP Agents** | `/dashboard/acp-agents` | CLIs that OmniRoute spawns as backend via stdio/ACP (reverse flow) | see registry |
|
||||
|
||||
@@ -90,7 +90,7 @@ Entries with `baseUrlSupport: "none"` are **not shown** in the dashboard pages
|
||||
|
||||
---
|
||||
|
||||
## 1. CLI Code's Catalog (19 tools)
|
||||
## 1. CLI Code's Catalog (20 tools)
|
||||
|
||||
Tools that support custom base URL and appear in `/dashboard/cli-code`:
|
||||
|
||||
@@ -107,6 +107,7 @@ Tools that support custom base URL and appear in `/dashboard/cli-code`:
|
||||
| forge | ForgeCode | Antinomy HQ | full | custom | true |
|
||||
| jcode | jcode | 1jehuang (OSS) | full | custom | false |
|
||||
| deepseek-tui | DeepSeek TUI | Hunter Bown (OSS) | full | custom | false |
|
||||
| codewhale | CodeWhale | Hmbown (OSS) | full | custom | false |
|
||||
| opencode | OpenCode | Anomaly (ex-SST) | full | guide | true |
|
||||
| droid | Factory Droid | Factory AI | partial | guide | false |
|
||||
| copilot | GitHub Copilot CLI | GitHub/MS | full | custom | false |
|
||||
@@ -198,7 +199,8 @@ New tools with `configType: "custom"` have dedicated settings API routes:
|
||||
| ------------------------------------------- | ------------------------------ |
|
||||
| `POST /api/cli-tools/forge-settings` | ForgeCode (.forge.toml) |
|
||||
| `POST /api/cli-tools/jcode-settings` | jcode (--base-url flag) |
|
||||
| `POST /api/cli-tools/deepseek-tui-settings` | DeepSeek TUI (OPENAI_BASE_URL) |
|
||||
| `POST /api/cli-tools/deepseek-tui-settings` | DeepSeek TUI (OPENAI_BASE_URL, legacy) |
|
||||
| `POST /api/cli-tools/codewhale-settings` | CodeWhale (OPENAI_BASE_URL, primary + legacy `~/.deepseek` sync) |
|
||||
| `POST /api/cli-tools/smelt-settings` | Smelt |
|
||||
| `POST /api/cli-tools/pi-settings` | Pi coding agent |
|
||||
|
||||
|
||||
@@ -117,6 +117,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
|
||||
| Variable | Default | Source File | Description |
|
||||
| ------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). |
|
||||
| `OMNIROUTE_BASE_PATH` | _(empty = root)_ | `next.config.mjs` | URL subpath for serving OmniRoute behind a reverse proxy under a subpath (sets Next.js `basePath`; auth redirects are basePath-aware). E.g. `/omniroute`. |
|
||||
| `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. |
|
||||
| `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. |
|
||||
| `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. |
|
||||
@@ -264,6 +265,8 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp
|
||||
| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. |
|
||||
| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. |
|
||||
| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | Public-origin resolver, image URLs | Highest-priority browser-facing OmniRoute origin used for public URL generation and non-dashboard browser-origin validation (for example `/v1/chatgpt-web/image/<id>`). Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL but the user's browser must fetch images from a LAN, tunnel, or public origin. Do **not** include `/v1`. |
|
||||
| `OMNIROUTE_PROVIDER_MANIFEST_URL` | _(unset)_ | `open-sse/config/providerPluginManifestUrl.ts` | Absolute provider plugin manifest URL advertised to sidecar clients. When unset, OmniRoute derives `/api/v1/provider-plugin-manifest` from request origin or HOST/PORT. |
|
||||
| `OMNIROUTE_PUBLIC_PROTOCOL` | `http` | `open-sse/config/providerPluginManifestUrl.ts` | Protocol used when deriving the provider plugin manifest URL from HOST/PORT without a request origin. Set to `https` behind a TLS-terminating public proxy when no explicit `OMNIROUTE_PROVIDER_MANIFEST_URL` is set. |
|
||||
| `OMNIROUTE_TRUST_PROXY` | _(unset)_ | `src/server/origin/publicOrigin.ts` | Optional trust mode for forwarded public-origin headers. Unset = do not trust `Forwarded` / `X-Forwarded-*` for security decisions. `true` / `loopback` trusts forwarded host/proto only from a token-stamped loopback proxy. `private` / `lan` also trusts private-LAN proxy peers. Prefer explicit `NEXT_PUBLIC_BASE_URL` in production. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS` | `180000` (3 min) | `open-sse/executors/chatgpt-web.ts` | Max wait time for an async chatgpt-web image to land via the celsius WebSocket. Increase during upstream queue-deep windows. |
|
||||
| `OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB` | `256` | `open-sse/services/chatgptImageCache.ts` | Total in-memory byte budget (MB) for the chatgpt-web image cache serving `/v1/chatgpt-web/image/<id>`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. |
|
||||
@@ -353,6 +356,8 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex,
|
||||
| `CLI_QODER_BIN` | `qoder` | `src/shared/services/cliRuntime.ts` | Custom path to Qoder CLI binary. |
|
||||
| `CLI_QWEN_BIN` | `qwen` | `src/shared/services/cliRuntime.ts` | Custom path to the Qwen Code CLI binary. |
|
||||
| `CLI_DEVIN_BIN` | `devin` | `open-sse/executors/devin-cli.ts` | Custom path to the Devin CLI binary (v3.8.0). Used by the Windsurf/Devin executor. |
|
||||
| `AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Absolute-path override for the Augment (Auggie) CLI binary used by the local `auggie` provider. Falls back to `CLI_AUGGIE_BIN`, then a PATH lookup. |
|
||||
| `CLI_AUGGIE_BIN` | `auggie` | `open-sse/executors/auggie.ts` | Alias override for the Augment (Auggie) CLI binary path (checked after `AUGGIE_BIN`). |
|
||||
| `HERMES_HOME` | `~/.hermes` | `src/lib/cli-helper/config-generator/hermesHome.ts` | 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`). |
|
||||
|
||||
### CLI Profile Auto-Sync
|
||||
@@ -475,6 +480,7 @@ Built-in credentials for **localhost development**. For remote deployments, regi
|
||||
| `QODER_PERSONAL_ACCESS_TOKEN` | Qoder | Direct API key fallback (bypasses OAuth). |
|
||||
| `QODER_CLI_WORKSPACE` | Qoder | Workspace ID for Qoder CLI. |
|
||||
| `OMNIROUTE_QODER_WORKSPACE` | Qoder | Alias for `QODER_CLI_WORKSPACE`. |
|
||||
| `QODER_CLI_CONFIG_DIR` | Qoder | Override the Qoder CLI config dir (isolated PAT session, avoids clobbering a browser login). |
|
||||
| `BLACKBOX_WEB_VALIDATED_TOKEN` | Blackbox Web | Frontend `tk` token to send as `validated` on `/api/chat`. Required when Blackbox enforces token matching; otherwise OmniRoute falls back to a random UUID. See issue #2252. |
|
||||
| `VISION_BRIDGE_BASE_URL` | Vision Bridge guardrail | OpenAI-compatible base URL for non-Anthropic vision-bridge calls. Defaults to the legacy OpenAI URL env or api.openai.com. Point at OmniRoute's `/v1` self-loop or any OpenAI-compat endpoint (Gemini OpenAI-compat, OpenRouter). Issue #2232. |
|
||||
| `VISION_BRIDGE_API_KEY` | Vision Bridge guardrail | API key for the URL above. Overrides per-provider OpenAI / Google env vars for non-Anthropic vision-bridge calls. Anthropic models keep their dedicated Anthropic key path. Issue #2232. |
|
||||
@@ -618,6 +624,8 @@ REQUEST_TIMEOUT_MS (global override)
|
||||
| `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive socket idle timeout. |
|
||||
| `TLS_CLIENT_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | TLS fingerprint proxy (wreq-js) timeout. |
|
||||
| `API_BRIDGE_PROXY_TIMEOUT_MS` | `30000` | Proxy hop timeout for `/v1` bridge requests. |
|
||||
| `FIRECRAWL_BASE_URL` | `https://api.firecrawl.dev` | Point the Firecrawl web-fetch executor at a self-hosted instance (API key optional off-cloud). |
|
||||
| `FIRECRAWL_TIMEOUT_MS` | `30000` | Per-request timeout for the Firecrawl web-fetch executor. |
|
||||
| `API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS` | `300000` | Overall server request timeout for the bridge. |
|
||||
| `API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS` | `60000` | Time to send response headers via the bridge. |
|
||||
| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Bridge keep-alive idle timeout. |
|
||||
@@ -806,6 +814,7 @@ Automatic model pricing data synchronization from external sources.
|
||||
| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). |
|
||||
| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. |
|
||||
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. |
|
||||
| `MUX_SERVICE_PORT` | `8322` | `src/lib/services/bootstrap.ts` | Override the port where the embedded Mux (coder/mux) agent-orchestration daemon listens (always 127.0.0.1). |
|
||||
| `LOCAL_HOSTNAMES` | _(empty)_ | `open-sse/config/providerRegistry.ts` | Comma-separated additional hostnames treated as "local" (Docker service names, etc.). |
|
||||
|
||||
`ENABLE_CC_COMPATIBLE_PROVIDER` is only for third-party relays that accept Claude Code clients
|
||||
@@ -822,6 +831,11 @@ Anthropic-compatible provider instead.
|
||||
| `PROXY_FAST_FAIL_TIMEOUT_MS` | `2000` | `src/lib/proxyHealth.ts` | Fast-fail health check timeout. |
|
||||
| `PROXY_HEALTH_CACHE_TTL_MS` | `30000` | `src/lib/proxyHealth.ts` | Health check result cache TTL. |
|
||||
| `PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS` | `2000` | `src/lib/proxyHealth.ts` | Cache TTL for failed proxy health probes. Keep this shorter than `PROXY_HEALTH_CACHE_TTL_MS` so transient proxy timeouts under high concurrency retry quickly without disabling fast-fail for truly dead proxies. |
|
||||
| `PROXY_HEALTH_ENABLED` | `true` | `src/lib/proxyHealth/scheduler.ts` | Set `false` to disable the background proxy health scheduler that periodically probes registered proxies. |
|
||||
| `PROXY_HEALTH_INTERVAL_MS` | `600000` | `src/lib/proxyHealth/scheduler.ts` | Background health-scheduler sweep interval in ms (minimum `60000`). |
|
||||
| `PROXY_HEALTH_TEST_URL` | `https://httpbin.org/ip` | `src/lib/proxyHealth/scheduler.ts` | Reachability probe target used by the scheduler and the `/api/settings/proxies/auto-test` endpoint. Point it at an internal/self-hosted URL to avoid the public default. |
|
||||
| `PROXY_AUTO_REMOVE` | `false` | `src/lib/proxyHealth/scheduler.ts` | Set `true` to let the scheduler auto-remove proxies after repeated consecutive failures. |
|
||||
| `PROXY_AUTO_REMOVE_AFTER` | `3` | `src/lib/proxyHealth/scheduler.ts` | Consecutive failures before the scheduler auto-removes a proxy (when `PROXY_AUTO_REMOVE=true`). |
|
||||
| `OMNIROUTE_CONTROL_PLANE_PROXY_DIRECT_FALLBACK` | `false` | `src/shared/constants/featureFlagDefinitions.ts` | Allow OAuth and provider validation flows to bypass a pinned proxy and connect directly when proxy reachability pre-checks fail. Effective precedence is Feature Flags DB override > env var > default. |
|
||||
| `RATE_LIMIT_MAX_WAIT_MS` | `120000` (2 min) | `open-sse/services/rateLimitManager.ts` | Max time to wait on a 429 before failing the request. |
|
||||
| `RATE_LIMIT_AUTO_ENABLE` | _(unset)_ | `open-sse/services/rateLimitManager.ts` | Force the auto-enable rate limit safety net on/off regardless of the persisted Dashboard setting. Accepts `true`/`1`/`on` to force on, `false`/`0`/`off` to force off. |
|
||||
@@ -1005,6 +1019,10 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `FREE_PROXY_PROXIFLY_ANONYMITY` | `elite` | `src/lib/freeProxyProviders/proxifly.ts` | Anonymity level filter for Proxifly (`elite`, `anonymous`, `transparent`). |
|
||||
| `FREE_PROXY_IPLOCATE_ENABLED` | `false` | `src/lib/freeProxyProviders/iplocate.ts` | Enable the IPLocate free proxy source. Opt-in only. |
|
||||
| `FREE_PROXY_IPLOCATE_BASE_URL` | `https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols` | `src/lib/freeProxyProviders/iplocate.ts` | IPLocate proxy list base URL override. |
|
||||
| `FREE_PROXY_WEBSHARE_ENABLED` | `true` | `src/lib/freeProxyProviders/webshare.ts` | Enable the Webshare proxy pool source. Set to `false` to disable; also requires `FREE_PROXY_WEBSHARE_API_KEY` to be set. |
|
||||
| `FREE_PROXY_WEBSHARE_API_KEY` | _(none)_ | `src/lib/freeProxyProviders/webshare.ts` | Webshare account API token (`Authorization: Token <key>`). Required — the provider stays disabled without it. |
|
||||
| `FREE_PROXY_WEBSHARE_API_URL` | `https://proxy.webshare.io/api/v2/proxy/list/` | `src/lib/freeProxyProviders/webshare.ts` | Webshare proxy list API URL override. |
|
||||
| `FREE_PROXY_WEBSHARE_MAX` | `500` | `src/lib/freeProxyProviders/webshare.ts` | Maximum proxies imported per Webshare sync. |
|
||||
| `NEXT_PUBLIC_VERCEL_RELAY_ENABLED` | `true` | `src/app/(dashboard)/…/ProxyPoolTab.tsx` | Show/hide the Deploy Vercel Relay button in the Proxy Pool tab. |
|
||||
| `VERCEL_API_BASE` | `https://api.vercel.com` | `src/app/api/settings/proxy/vercel-deploy/route.ts` | Vercel API base URL override (for testing). |
|
||||
| `NEXT_PUBLIC_VERCEL_RELAY_DEFAULT_PROJECT` | `omniroute-relay` | `src/app/(dashboard)/…/VercelRelayModal.tsx` | Default project name pre-filled in the Vercel Relay deploy modal. |
|
||||
@@ -1038,6 +1056,7 @@ Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy),
|
||||
| `PLAYGROUND_IMPROVE_PROMPT_DEFAULT_MODEL` | _(unset)_ | `src/app/(dashboard)/dashboard/playground/` | Default model for the Playground 'improve prompt' action (falls back to the active model when unset). |
|
||||
| `BIFROST_ENABLED` | `1` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Master kill switch for the bifrost sidecar proxy. When set to `0`, the route returns 503 with the `X-Bifrost-Killswitch` header and the operator is bounced to the TS path. Use to disable the sidecar without redeploying (tier-1 router incident, key rotation). |
|
||||
| `BIFROST_BASE_URL` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | When set, the Bifrost sidecar proxy route forwards `/v1/chat/completions` traffic to this Go gateway instead of the TS relay handler. Unset → 503-with-fallback. Trailing slash is stripped. |
|
||||
| `BIFROST_PORT` | `8080` | `src/lib/services/bootstrap.ts` | Port the supervised Bifrost embedded service binds to (`127.0.0.1:<port>`) when OmniRoute manages the Bifrost sidecar lifecycle. Defaults to `8080`. |
|
||||
| `BIFROST_API_KEY` | _(unset)_ | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | API key for the Bifrost gateway (sent as `Authorization: Bearer ...`). If unset, the route expects the request to carry a valid OmniRoute API key; this key is for gateway-side auth only. |
|
||||
| `BIFROST_STREAMING_ENABLED` | `true` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | When true, the Bifrost sidecar route streams responses back via SSE through the gateway rather than the TS streaming executor. Set to `0` to force non-streaming JSON responses through the gateway. |
|
||||
| `BIFROST_TIMEOUT_MS` | `30000` | `src/app/api/v1/relay/chat/completions/bifrost/route.ts` | Per-request timeout when proxying to the Bifrost gateway (ms). On timeout the route returns the TS relay path via the `X-Bifrost-Fallback` header. |
|
||||
|
||||
81
docs/reference/PROVIDER_PLUGIN_MANIFEST.md
Normal file
81
docs/reference/PROVIDER_PLUGIN_MANIFEST.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
title: "Provider Plugin Manifest"
|
||||
version: 3.8.42
|
||||
lastUpdated: 2026-07-01
|
||||
---
|
||||
|
||||
# Provider Plugin Manifest
|
||||
|
||||
`open-sse/config/providerPluginManifest.ts` defines the JSON-safe provider
|
||||
plugin contract. `open-sse/config/providerPluginManifestRegistry.ts` binds that
|
||||
contract to the current provider registry for sidecars such as Bifrost,
|
||||
CLIProxyAPI, or a future Go/Rust router. The TypeScript registry remains the
|
||||
source of truth, but sidecars can consume the manifest without importing
|
||||
executor code, OAuth defaults, headers, or process environment state.
|
||||
|
||||
The same manifest is available over HTTP at
|
||||
`GET /api/v1/provider-plugin-manifest` for sidecars that run out-of-process.
|
||||
|
||||
OmniRoute advertises that URL to Bifrost and CLIProxyAPI via the
|
||||
`X-OmniRoute-Provider-Manifest-Url` request header. Set
|
||||
`OMNIROUTE_PROVIDER_MANIFEST_URL` when the sidecar needs a public or container
|
||||
network URL instead of the local request origin.
|
||||
|
||||
## Goal
|
||||
|
||||
Move provider metadata toward a plugin contract so the hot request path can
|
||||
eventually be owned by a lower-latency sidecar while OmniRoute keeps the
|
||||
TypeScript route as the policy gate and fallback. The manifest is additive: it
|
||||
does not change request routing by itself.
|
||||
|
||||
## Contract
|
||||
|
||||
The manifest contains:
|
||||
|
||||
- provider id and alias
|
||||
- upstream format and executor name
|
||||
- auth type, auth header, and optional auth prefix
|
||||
- static endpoint metadata
|
||||
- sidecar eligibility and explicit reasons when a provider should stay on TS
|
||||
- JSON-safe model metadata such as context length, vision/reasoning flags, and
|
||||
unsupported params
|
||||
- capability tags including `apikey`, `oauth`, `custom-executor`,
|
||||
`passthrough-models`, `responses`, and `sidecar-candidate`
|
||||
|
||||
The manifest intentionally excludes:
|
||||
|
||||
- OAuth client secrets and default secret values
|
||||
- runtime environment resolution
|
||||
- request headers and public credential helpers
|
||||
- dynamic URL builders
|
||||
- executor functions
|
||||
- session pool internals
|
||||
|
||||
## Sidecar Use
|
||||
|
||||
Sidecars should treat `sidecar.eligible` as a conservative candidate signal, not
|
||||
as an unconditional routing decision. The first import target should be
|
||||
API-key, static-endpoint providers using the default executor. Providers with
|
||||
custom web executors, OAuth/session flows, dynamic URL builders, or pool config
|
||||
stay on the TypeScript fallback path until a sidecar implements equivalent
|
||||
behavior and telemetry proves parity.
|
||||
|
||||
Suggested migration phases:
|
||||
|
||||
1. Generate and validate the provider plugin manifest from the TS registry.
|
||||
2. Teach Bifrost or CLIProxyAPI to import the manifest for API-key/static
|
||||
providers.
|
||||
3. Route eligible providers through the sidecar behind `OMNIROUTE_RELAY_BACKEND`
|
||||
while keeping TS fallback enabled.
|
||||
4. Promote providers only when success rate, p99 latency, streaming behavior,
|
||||
and unsupported-param handling match the TS path.
|
||||
5. Add sidecar-native plugins for custom executors one provider family at a
|
||||
time.
|
||||
|
||||
## Why Not Embed Providers Directly In Next
|
||||
|
||||
The Next frontend should not own provider execution. It should call the API
|
||||
boundary. The backend can then decide whether to use the TypeScript executor,
|
||||
Bifrost, CLIProxyAPI, or a future native sidecar. This keeps request signing,
|
||||
allowlist checks, DB policy, and fallback behavior centralized before any
|
||||
sidecar handoff.
|
||||
@@ -75,3 +75,10 @@ For sustained high RPM/RPS and strict success SLO:
|
||||
- `BIFROST_ENABLED=1`
|
||||
- Keep API keys, allowlist, sanitizer, and rate-limit checks enabled in route handlers (they always run before downstream forwarding).
|
||||
- Export fallback metrics from your reverse proxy and request logs so sidecar outages are visible within one minute.
|
||||
|
||||
## Provider plugin contract
|
||||
|
||||
Sidecars should import provider metadata through the JSON-safe provider plugin
|
||||
manifest instead of depending on TypeScript executor internals. See
|
||||
[Provider Plugin Manifest](./PROVIDER_PLUGIN_MANIFEST.md) for the sidecar
|
||||
eligibility contract and migration phases.
|
||||
|
||||
162
docs/security/CORS.md
Normal file
162
docs/security/CORS.md
Normal file
@@ -0,0 +1,162 @@
|
||||
---
|
||||
title: CORS Configuration & Security
|
||||
---
|
||||
|
||||
# CORS Configuration & Security
|
||||
|
||||
OmniRoute controls which **browser origins** may read cross-origin responses
|
||||
from a single, centralized allowlist. The model is **fail-closed by default**:
|
||||
no origin is allowed until you opt one in. This page documents how the allowlist
|
||||
resolves, what `CORS_ALLOW_ALL=true` actually exposes (and, importantly, what it
|
||||
does **not**), how to configure dev vs production safely, and the runtime warning
|
||||
the dashboard shows when a wildcard is live.
|
||||
|
||||
**Source of truth:** `src/server/cors/origins.ts` (`resolveAllowedOrigin`,
|
||||
`applyCorsHeaders`, `getCorsStatus`). The allowlist is applied once, in the
|
||||
middleware (`src/server/authz/pipeline.ts`) — per-route handlers do not set
|
||||
`Access-Control-Allow-Origin` themselves.
|
||||
|
||||
## How an origin is resolved
|
||||
|
||||
For each request the middleware computes the `Access-Control-Allow-Origin` value
|
||||
in this order:
|
||||
|
||||
1. **`CORS_ALLOW_ALL=true`** (or the legacy `CORS_ORIGIN=*`) → echo the caller's
|
||||
`Origin` back (or `*` when there is no `Origin` header), with `Vary: Origin`
|
||||
so caches stay correct.
|
||||
2. Otherwise, the request `Origin` is normalized (lower-cased, trailing slash
|
||||
stripped) and matched against the **merged allowlist**:
|
||||
- env **`CORS_ALLOWED_ORIGINS`** — comma-separated list, and
|
||||
- the runtime **`corsOrigins`** setting (Dashboard → Security → _CORS Allowed
|
||||
Origins_), injected via `setRuntimeAllowedOrigins()` from
|
||||
`src/lib/config/runtimeSettings.ts`.
|
||||
3. No match → **no `Access-Control-Allow-Origin` header is emitted**. The browser
|
||||
blocks the cross-origin read. This is the intended fail-closed default.
|
||||
|
||||
| Env var | Meaning |
|
||||
| ---------------------- | ------------------------------------------------------------------------------------ |
|
||||
| `CORS_ALLOWED_ORIGINS` | CSV of exact origins to allow (recommended). |
|
||||
| `CORS_ALLOW_ALL` | `true`/`1` → echo any origin (wildcard). Dev only. |
|
||||
| `CORS_ORIGIN` | Legacy. `*` behaves like `CORS_ALLOW_ALL`; a single value is added to the allowlist. |
|
||||
|
||||
## Threat model — what `CORS_ALLOW_ALL=true` really exposes
|
||||
|
||||
The generic OWASP warning ("wildcard CORS = any site can call your API") is worth
|
||||
taking seriously, but OmniRoute's exposure is **narrower than the generic case**,
|
||||
because of one concrete implementation fact:
|
||||
|
||||
> **The central `applyCorsHeaders()` never emits
|
||||
> `Access-Control-Allow-Credentials`.** A browser will not expose a _credentialed_
|
||||
> (cookie-bearing) cross-origin response unless the server sends
|
||||
> `Access-Control-Allow-Credentials: true`. OmniRoute's shared CORS path never
|
||||
> does.
|
||||
|
||||
What that means per surface, even with `CORS_ALLOW_ALL=true`:
|
||||
|
||||
| Surface | Auth mechanism | Effect of wildcard CORS |
|
||||
| ----------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Dashboard / MANAGEMENT `/api/*` | Cookie session | Origin is echoed, but **without `Allow-Credentials`** the browser **blocks** the credentialed read. A malicious cross-origin site **cannot read** your authenticated dashboard responses, and the session cookie is not exposed. |
|
||||
| Client API `/v1/*`, `/v1beta/*` | Bearer / `x-api-key` header | Already permissive **by design** (`relaxForTokenAuth`): browsers never auto-attach `Authorization`/`x-api-key`, so an attacker's page cannot supply your key. `CORS_ALLOW_ALL` does not widen this. |
|
||||
| Public read-only (`/api/health`, …) | None | Non-sensitive; wildcard is harmless. |
|
||||
|
||||
So the **residual** exposure of `CORS_ALLOW_ALL=true` is limited to: (a)
|
||||
non-credentialed cross-origin **reads** of already-unauthenticated data, and (b)
|
||||
letting CORS **preflight pass** on management routes — which still require auth
|
||||
that a cross-origin page cannot provide. It is **not** a session-hijack or
|
||||
credential-theft vector on the shared CORS path.
|
||||
|
||||
### One genuine exception — `/api/v1/agents/`
|
||||
|
||||
The Cloud-Agent routes (`/api/v1/agents/{health,credentials,tasks,tasks/[id]}`) set
|
||||
their **own** CORS headers
|
||||
(`src/lib/cloudAgent/api.ts`, `getCloudAgentCorsHeaders`) and **do** emit
|
||||
`Access-Control-Allow-Origin: <origin>|*` together with
|
||||
`Access-Control-Allow-Credentials: true`. This is the single surface where
|
||||
origin-echo and credentials coexist, and it is **independent of
|
||||
`CORS_ALLOW_ALL`**. These routes are management-authenticated
|
||||
(`requireManagementAuth`); operators who expose the dashboard off-host should be
|
||||
aware that this is the one place a cross-origin credentialed read is permitted by
|
||||
the response headers. Tightening it to an explicit allowlist is tracked
|
||||
separately from this CORS guidance.
|
||||
|
||||
## Production checklist
|
||||
|
||||
- **Never set `CORS_ALLOW_ALL=true` in production.** Leave it unset.
|
||||
- Set an **explicit** origin list — either the env var or the Security-tab field:
|
||||
|
||||
```bash
|
||||
CORS_ALLOWED_ORIGINS="https://app.example.com, https://admin.example.com"
|
||||
```
|
||||
|
||||
- If OmniRoute runs behind a reverse proxy / tunnel (nginx, Caddy, Cloudflare
|
||||
Tunnel, Tailscale), CORS is **not** your only control — the loopback route
|
||||
guard still protects spawn-capable routes (see
|
||||
[ROUTE_GUARD_TIERS](./ROUTE_GUARD_TIERS.md)). Do not forge
|
||||
`X-Forwarded-For: 127.0.0.1` to "fix" a 403; that re-opens the RCE class the
|
||||
route guard closes.
|
||||
- Confirm the runtime state: the dashboard shows a **persistent amber banner**
|
||||
under Dashboard → Security → Authorization Inventory whenever
|
||||
`CORS_ALLOW_ALL=true` is live, and `/api/settings/authz-inventory` returns a
|
||||
`cors: { allowAll, allowedOrigins }` envelope monitoring tools can poll.
|
||||
|
||||
## Development convenience — allow specific local origins
|
||||
|
||||
You rarely need the wildcard even in dev. Allow just the dev servers you use:
|
||||
|
||||
```bash
|
||||
# Vite (5173) + Next.js (3000) dev servers calling a local OmniRoute
|
||||
CORS_ALLOWED_ORIGINS="http://localhost:5173, http://localhost:3000"
|
||||
```
|
||||
|
||||
Origins are matched case-insensitively with the trailing slash ignored, so
|
||||
`http://localhost:3000` and `http://localhost:3000/` are equivalent. The same CSV
|
||||
can be set at runtime in **Dashboard → Security → CORS Allowed Origins** without a
|
||||
restart.
|
||||
|
||||
## API keys vs cookie sessions
|
||||
|
||||
- **Bearer / `x-api-key` (the `/v1/*` inference surface):** browsers never attach
|
||||
these automatically. CORS is not a meaningful barrier here — the API key is the
|
||||
barrier — which is why that surface is intentionally permissive so browser and
|
||||
Electron clients can read responses they are already entitled to.
|
||||
- **Cookie session (the dashboard):** protected by the fail-closed default **and**
|
||||
by the absence of `Access-Control-Allow-Credentials` on the shared path. Keep
|
||||
management/dashboard origins out of any permissive config; they must stay exactly
|
||||
fail-closed.
|
||||
|
||||
## Example: reverse proxy in front of OmniRoute
|
||||
|
||||
CORS is enforced by OmniRoute itself, so the proxy generally should **not** add or
|
||||
rewrite `Access-Control-*` headers (double headers break browsers). Terminate TLS
|
||||
and forward — let OmniRoute answer preflight:
|
||||
|
||||
```nginx
|
||||
# nginx — forward to OmniRoute; do NOT inject Access-Control-* here
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:20128;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
# Do NOT set X-Forwarded-For to 127.0.0.1 — it defeats the loopback route guard.
|
||||
}
|
||||
```
|
||||
|
||||
Set the allowed browser origins in OmniRoute (`CORS_ALLOWED_ORIGINS` or the
|
||||
Security tab), not in the proxy.
|
||||
|
||||
## Source files
|
||||
|
||||
| Concern | File |
|
||||
| ----------------------------------------------- | -------------------------------------------------------------------- |
|
||||
| Allowlist resolution + `getCorsStatus()` | `src/server/cors/origins.ts` |
|
||||
| Middleware application (single source of truth) | `src/server/authz/pipeline.ts` |
|
||||
| Settings → runtime origin injection | `src/lib/config/runtimeSettings.ts` |
|
||||
| Runtime status for the dashboard | `src/app/api/settings/authz-inventory/route.ts` |
|
||||
| Dashboard warning banner | `src/app/(dashboard)/dashboard/settings/components/AuthzSection.tsx` |
|
||||
| CORS Allowed Origins field | `src/app/(dashboard)/dashboard/settings/components/SecurityTab.tsx` |
|
||||
| Cloud-Agent per-route CORS (the exception) | `src/lib/cloudAgent/api.ts` |
|
||||
|
||||
## See also
|
||||
|
||||
- [Route Guard Tiers](./ROUTE_GUARD_TIERS.md) — loopback enforcement for
|
||||
spawn-capable routes (a separate, complementary control).
|
||||
- [Authorization Guide](../architecture/AUTHZ_GUIDE.md) — the full auth pipeline.
|
||||
4
electron/package-lock.json
generated
4
electron/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.8.43",
|
||||
"version": "3.8.44",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.8.43",
|
||||
"version": "3.8.44",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"electron-updater": "^6.8.9"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.8.43",
|
||||
"version": "3.8.44",
|
||||
"description": "OmniRoute Desktop Application",
|
||||
"main": "main.js",
|
||||
"author": {
|
||||
|
||||
@@ -94,6 +94,13 @@ function readTimeoutMs(...values) {
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
// Opt-in subpath deployment behind a reverse proxy (e.g. nginx/Caddy serving
|
||||
// OmniRoute under https://host/omniroute/). Empty by default so root-path
|
||||
// deployments are unaffected. Next.js strips this prefix from `pathname`
|
||||
// before route matching, so authz classification (classifyRoute/isLocalOnlyPath)
|
||||
// keeps operating on un-prefixed paths — see src/server/authz/pipeline.ts for
|
||||
// the two redirect call sites that re-add it via `request.nextUrl.basePath`.
|
||||
basePath: process.env.OMNIROUTE_BASE_PATH || "",
|
||||
distDir,
|
||||
// Turbopack config: redirect native modules to stubs at build time
|
||||
turbopack: {
|
||||
@@ -207,6 +214,13 @@ const nextConfig = {
|
||||
"tough-cookie",
|
||||
"@ngrok/ngrok",
|
||||
"@huggingface/transformers",
|
||||
// copilot-m365-web.ts imports 'ws' as a client-side WebSocket. When bundled,
|
||||
// ws cannot resolve its 'bufferutil' native addon (frame masking) and throws
|
||||
// TypeError: b.mask is not a function on the first outgoing frame, causing
|
||||
// every chat request to time out at the stream-readiness watchdog. (#6062)
|
||||
"ws",
|
||||
"bufferutil",
|
||||
"utf-8-validate",
|
||||
"child_process",
|
||||
"fs",
|
||||
"path",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*
|
||||
* Defines providers that support audio endpoints:
|
||||
* - /v1/audio/transcriptions (Whisper API)
|
||||
* - /v1/audio/translations (Whisper translate-to-English API)
|
||||
* - /v1/audio/speech (TTS API)
|
||||
*/
|
||||
|
||||
@@ -160,6 +161,30 @@ export const AUDIO_TRANSCRIPTION_PROVIDERS: Record<string, AudioProvider> = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Providers that expose an OpenAI-Whisper-compatible /audio/translations
|
||||
* endpoint (translate-to-English). This is a narrower surface than
|
||||
* transcription: only Whisper-family models support it, and there is no
|
||||
* `language` input — output is always English regardless of source audio.
|
||||
*/
|
||||
export const AUDIO_TRANSLATION_PROVIDERS: Record<string, AudioProvider> = {
|
||||
openai: {
|
||||
id: "openai",
|
||||
baseUrl: "https://api.openai.com/v1/audio/translations",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [{ id: "whisper-1", name: "Whisper 1" }],
|
||||
},
|
||||
|
||||
groq: {
|
||||
id: "groq",
|
||||
baseUrl: "https://api.groq.com/openai/v1/audio/translations",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [{ id: "whisper-large-v3", name: "Whisper Large v3" }],
|
||||
},
|
||||
};
|
||||
|
||||
export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
|
||||
vertex: {
|
||||
id: "vertex",
|
||||
@@ -403,6 +428,13 @@ export function getTranscriptionProvider(providerId: string): AudioProvider | nu
|
||||
return AUDIO_TRANSCRIPTION_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get translation provider config by ID
|
||||
*/
|
||||
export function getTranslationProvider(providerId: string): AudioProvider | null {
|
||||
return AUDIO_TRANSLATION_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get speech provider config by ID
|
||||
*/
|
||||
@@ -480,6 +512,13 @@ export function parseSpeechModel(modelStr: string | null, dynamicProviders?: Aud
|
||||
return parseAudioModel(modelStr, AUDIO_SPEECH_PROVIDERS, dynamicProviders);
|
||||
}
|
||||
|
||||
export function parseTranslationModel(
|
||||
modelStr: string | null,
|
||||
dynamicProviders?: AudioProvider[]
|
||||
) {
|
||||
return parseAudioModel(modelStr, AUDIO_TRANSLATION_PROVIDERS, dynamicProviders);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all audio models as a flat list
|
||||
*/
|
||||
|
||||
@@ -93,6 +93,17 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
|
||||
],
|
||||
},
|
||||
|
||||
"vercel-ai-gateway": {
|
||||
id: "vercel-ai-gateway",
|
||||
baseUrl: "https://ai-gateway.vercel.sh/v1/embeddings",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{ id: "text-embedding-3-small", name: "Text Embedding 3 Small", dimensions: 1536 },
|
||||
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large", dimensions: 3072 },
|
||||
],
|
||||
},
|
||||
|
||||
upstage: {
|
||||
id: "upstage",
|
||||
baseUrl: "https://api.upstage.ai/v1/embeddings",
|
||||
|
||||
@@ -175,6 +175,20 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "2048x2048"],
|
||||
},
|
||||
|
||||
"vercel-ai-gateway": {
|
||||
id: "vercel-ai-gateway",
|
||||
alias: "vag",
|
||||
baseUrl: "https://ai-gateway.vercel.sh/v1/images/generations",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "openai",
|
||||
models: [
|
||||
{ id: "gpt-image-1", name: "GPT Image 1" },
|
||||
{ id: "black-forest-labs/flux-1.1-pro", name: "FLUX 1.1 Pro" },
|
||||
],
|
||||
supportedSizes: ["1024x1024", "1024x1792", "1792x1024"],
|
||||
},
|
||||
|
||||
together: {
|
||||
id: "together",
|
||||
baseUrl: "https://api.together.xyz/v1/images/generations",
|
||||
@@ -549,6 +563,35 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
supportedSizes: ["1024x1024", "1024x1280", "1280x1024"],
|
||||
},
|
||||
|
||||
// NVIDIA NIM image generation (FLUX models). Distinct from the NVIDIA *chat* entry
|
||||
// (open-sse/config/providers/registry/nvidia/index.ts, host integrate.api.nvidia.com,
|
||||
// OpenAI-compatible) — image generation lives on ai.api.nvidia.com/v1/genai/<model>
|
||||
// with a native NIM body per model, so it gets a dedicated `nvidia-nim` format/handler
|
||||
// (handleNvidiaNimImageGeneration) rather than reusing the OpenAI image path.
|
||||
// Ported from upstream 9router#1195.
|
||||
nvidia: {
|
||||
id: "nvidia",
|
||||
baseUrl: "https://ai.api.nvidia.com/v1/genai",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "nvidia-nim",
|
||||
models: [
|
||||
{ id: "black-forest-labs/flux.1-dev", name: "FLUX.1 Dev", inputModalities: ["text", "image"] },
|
||||
{ id: "black-forest-labs/flux.1-schnell", name: "FLUX.1 Schnell" },
|
||||
{
|
||||
id: "black-forest-labs/flux.1-kontext-dev",
|
||||
name: "FLUX.1 Kontext Dev (Edit)",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
{
|
||||
id: "black-forest-labs/flux.2-klein-4b",
|
||||
name: "FLUX.2 Klein 4B",
|
||||
inputModalities: ["text", "image"],
|
||||
},
|
||||
],
|
||||
supportedSizes: ["1024x1024", "768x1344", "512x512"],
|
||||
},
|
||||
|
||||
// SenseNova (商汤日日新) Text-to-Image on the free Token Plan. OpenAI-compatible
|
||||
// `/v1/images/generations`, so the generic OpenAI image handler routes it — same
|
||||
// SenseNova api-key/connection as the chat provider. (9router#2233)
|
||||
@@ -561,6 +604,27 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
models: [{ id: "sensenova-u1-fast", name: "SenseNova U1 Fast" }],
|
||||
supportedSizes: ["1024x1024"],
|
||||
},
|
||||
|
||||
// HuggingFace Hub Inference API text-to-image task. Returns raw image bytes
|
||||
// (not JSON), so it uses a dedicated "huggingface-image" format handled by
|
||||
// handleHuggingFaceImageGeneration. Same base URL convention as the HF
|
||||
// STT/TTS entries in audioRegistry.ts. Model list is deliberately small —
|
||||
// the dashboard's "suggested models" chip row (GET
|
||||
// /api/v1/providers/suggested-models) surfaces additional HF Hub models
|
||||
// beyond this seed list.
|
||||
huggingface: {
|
||||
id: "huggingface",
|
||||
baseUrl: "https://api-inference.huggingface.co/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "huggingface-image",
|
||||
models: [
|
||||
{ id: "black-forest-labs/FLUX.1-dev", name: "FLUX.1 Dev (HF)" },
|
||||
{ id: "black-forest-labs/FLUX.1-schnell", name: "FLUX.1 Schnell (HF)" },
|
||||
{ id: "stabilityai/stable-diffusion-xl-base-1.0", name: "Stable Diffusion XL (HF)" },
|
||||
],
|
||||
supportedSizes: ["1024x1024"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,14 +17,12 @@
|
||||
* still declared explicitly via `serviceKinds` on the provider entry; callers
|
||||
* union the two sources.
|
||||
*/
|
||||
import {
|
||||
AUDIO_TRANSCRIPTION_PROVIDERS,
|
||||
AUDIO_SPEECH_PROVIDERS,
|
||||
} from "./audioRegistry.ts";
|
||||
import { AUDIO_TRANSCRIPTION_PROVIDERS, AUDIO_SPEECH_PROVIDERS } from "./audioRegistry.ts";
|
||||
import { VIDEO_PROVIDERS } from "./videoRegistry.ts";
|
||||
import { MUSIC_PROVIDERS } from "./musicRegistry.ts";
|
||||
import { IMAGE_PROVIDERS } from "./imageRegistry.ts";
|
||||
import { EMBEDDING_PROVIDERS } from "./embeddingRegistry.ts";
|
||||
import { OCR_PROVIDERS } from "./ocrRegistry.ts";
|
||||
|
||||
/** Media kinds whose provider membership is defined by a backend registry. */
|
||||
export const MEDIA_KIND_REGISTRIES = {
|
||||
@@ -34,6 +32,7 @@ export const MEDIA_KIND_REGISTRIES = {
|
||||
music: MUSIC_PROVIDERS,
|
||||
image: IMAGE_PROVIDERS,
|
||||
embedding: EMBEDDING_PROVIDERS,
|
||||
ocr: OCR_PROVIDERS,
|
||||
} as const satisfies Record<string, Record<string, unknown>>;
|
||||
|
||||
export type RegistryMediaKind = keyof typeof MEDIA_KIND_REGISTRIES;
|
||||
|
||||
@@ -5,7 +5,25 @@
|
||||
* Follows OpenAI's moderation API format.
|
||||
*/
|
||||
|
||||
export const MODERATION_PROVIDERS = {
|
||||
export interface ModerationModel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface ModerationProvider {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
authType: string;
|
||||
authHeader: string;
|
||||
models: ModerationModel[];
|
||||
}
|
||||
|
||||
export interface ParsedModerationModel {
|
||||
provider: string | null;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export const MODERATION_PROVIDERS: Record<string, ModerationProvider> = {
|
||||
openai: {
|
||||
id: "openai",
|
||||
baseUrl: "https://api.openai.com/v1/moderations",
|
||||
@@ -16,22 +34,29 @@ export const MODERATION_PROVIDERS = {
|
||||
{ id: "text-moderation-latest", name: "Text Moderation Latest" },
|
||||
],
|
||||
},
|
||||
mistral: {
|
||||
id: "mistral",
|
||||
baseUrl: "https://api.mistral.ai/v1/moderations",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [{ id: "mistral-moderation-latest", name: "Mistral Moderation" }],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get moderation provider config by ID
|
||||
* Get moderation provider config by ID.
|
||||
*/
|
||||
export function getModerationProvider(providerId) {
|
||||
export function getModerationProvider(providerId: string): ModerationProvider | null {
|
||||
return MODERATION_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse moderation model string
|
||||
* Parse a moderation model string.
|
||||
*/
|
||||
export function parseModerationModel(modelStr) {
|
||||
export function parseModerationModel(modelStr: string | null | undefined): ParsedModerationModel {
|
||||
if (!modelStr) return { provider: null, model: null };
|
||||
|
||||
for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) {
|
||||
for (const providerId of Object.keys(MODERATION_PROVIDERS)) {
|
||||
if (modelStr.startsWith(providerId + "/")) {
|
||||
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
|
||||
}
|
||||
@@ -47,10 +72,10 @@ export function parseModerationModel(modelStr) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all moderation models as a flat list
|
||||
* Get all moderation models as a flat list.
|
||||
*/
|
||||
export function getAllModerationModels() {
|
||||
const models = [];
|
||||
export function getAllModerationModels(): Array<{ id: string; name: string; provider: string }> {
|
||||
const models: Array<{ id: string; name: string; provider: string }> = [];
|
||||
for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) {
|
||||
for (const model of config.models) {
|
||||
models.push({
|
||||
|
||||
82
open-sse/config/ocrRegistry.ts
Normal file
82
open-sse/config/ocrRegistry.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/**
|
||||
* OCR Provider Registry
|
||||
*
|
||||
* Defines providers that support the /v1/ocr endpoint.
|
||||
* Follows Mistral's OCR API format.
|
||||
*/
|
||||
|
||||
export interface OcrModel {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface OcrProvider {
|
||||
id: string;
|
||||
baseUrl: string;
|
||||
authType: string;
|
||||
authHeader: string;
|
||||
models: OcrModel[];
|
||||
}
|
||||
|
||||
export interface ParsedOcrModel {
|
||||
provider: string | null;
|
||||
model: string | null;
|
||||
}
|
||||
|
||||
export const OCR_PROVIDERS: Record<string, OcrProvider> = {
|
||||
mistral: {
|
||||
id: "mistral",
|
||||
baseUrl: "https://api.mistral.ai/v1/ocr",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [{ id: "mistral-ocr-latest", name: "Mistral OCR" }],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Get OCR provider config by ID.
|
||||
*/
|
||||
export function getOcrProvider(providerId: string): OcrProvider | null {
|
||||
return OCR_PROVIDERS[providerId] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an OCR model string.
|
||||
*
|
||||
* Accepts either a "provider/model" prefixed string or a bare model id that
|
||||
* matches one of the registered OCR models.
|
||||
*/
|
||||
export function parseOcrModel(modelStr: string | null | undefined): ParsedOcrModel {
|
||||
if (!modelStr) return { provider: null, model: null };
|
||||
|
||||
for (const providerId of Object.keys(OCR_PROVIDERS)) {
|
||||
if (modelStr.startsWith(providerId + "/")) {
|
||||
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
|
||||
}
|
||||
}
|
||||
|
||||
for (const [providerId, config] of Object.entries(OCR_PROVIDERS)) {
|
||||
if (config.models.some((m) => m.id === modelStr)) {
|
||||
return { provider: providerId, model: modelStr };
|
||||
}
|
||||
}
|
||||
|
||||
return { provider: null, model: modelStr };
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all OCR models as a flat list.
|
||||
*/
|
||||
export function getAllOcrModels(): Array<{ id: string; name: string; provider: string }> {
|
||||
const models: Array<{ id: string; name: string; provider: string }> = [];
|
||||
for (const [providerId, config] of Object.entries(OCR_PROVIDERS)) {
|
||||
for (const model of config.models) {
|
||||
models.push({
|
||||
id: `${providerId}/${model.id}`,
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
});
|
||||
}
|
||||
}
|
||||
return models;
|
||||
}
|
||||
186
open-sse/config/providerPluginManifest.ts
Normal file
186
open-sse/config/providerPluginManifest.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import type { RegistryEntry, RegistryModel } from "./providers/shared.ts";
|
||||
|
||||
export type ProviderPluginCapability =
|
||||
| "apikey"
|
||||
| "custom-executor"
|
||||
| "oauth"
|
||||
| "passthrough-models"
|
||||
| "responses"
|
||||
| "sidecar-candidate";
|
||||
|
||||
export interface ProviderPluginModel {
|
||||
id: string;
|
||||
name: string;
|
||||
contextLength?: number;
|
||||
maxOutputTokens?: number;
|
||||
toolCalling?: boolean;
|
||||
supportsReasoning?: boolean;
|
||||
supportsVision?: boolean;
|
||||
unsupportedParams?: readonly string[];
|
||||
targetFormat?: string;
|
||||
}
|
||||
|
||||
export interface ProviderPluginManifestEntry {
|
||||
id: string;
|
||||
alias?: string;
|
||||
format: string;
|
||||
executor: string;
|
||||
auth: {
|
||||
type: string;
|
||||
header: string;
|
||||
prefix?: string;
|
||||
};
|
||||
endpoints: {
|
||||
baseUrl?: string;
|
||||
baseUrls?: string[];
|
||||
responsesBaseUrl?: string;
|
||||
chatPath?: string;
|
||||
modelsUrl?: string;
|
||||
};
|
||||
capabilities: ProviderPluginCapability[];
|
||||
passthroughModels: boolean;
|
||||
defaultContextLength?: number;
|
||||
timeoutMs?: number;
|
||||
models: ProviderPluginModel[];
|
||||
sidecar: {
|
||||
eligible: boolean;
|
||||
reasons: string[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProviderPluginManifest {
|
||||
schemaVersion: 1;
|
||||
generatedFrom: "open-sse/config/providers";
|
||||
providers: ProviderPluginManifestEntry[];
|
||||
}
|
||||
|
||||
const SIDECAR_COMPATIBLE_EXECUTORS = new Set(["default"]);
|
||||
|
||||
function compactObject<T extends Record<string, unknown>>(value: T): Partial<T> {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).filter(([, entryValue]) => entryValue !== undefined),
|
||||
) as Partial<T>;
|
||||
}
|
||||
|
||||
function mapModel(model: RegistryModel): ProviderPluginModel {
|
||||
return compactObject({
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
contextLength: model.contextLength,
|
||||
maxOutputTokens: model.maxOutputTokens,
|
||||
toolCalling: model.toolCalling,
|
||||
supportsReasoning: model.supportsReasoning,
|
||||
supportsVision: model.supportsVision,
|
||||
unsupportedParams: model.unsupportedParams,
|
||||
targetFormat: model.targetFormat,
|
||||
}) as ProviderPluginModel;
|
||||
}
|
||||
|
||||
function sidecarEligibility(entry: RegistryEntry): { eligible: boolean; reasons: string[] } {
|
||||
const reasons: string[] = [];
|
||||
|
||||
if (!SIDECAR_COMPATIBLE_EXECUTORS.has(entry.executor)) {
|
||||
reasons.push(`custom executor: ${entry.executor}`);
|
||||
}
|
||||
if (entry.authType !== "apikey" && entry.authType !== "optional" && entry.authType !== "none") {
|
||||
reasons.push(`auth type requires TS handling: ${entry.authType}`);
|
||||
}
|
||||
if (!entry.baseUrl && !entry.baseUrls?.length && !entry.responsesBaseUrl) {
|
||||
reasons.push("no static upstream endpoint");
|
||||
}
|
||||
if (typeof entry.urlBuilder === "function") {
|
||||
reasons.push("dynamic URL builder");
|
||||
}
|
||||
if (entry.oauth) {
|
||||
reasons.push("oauth metadata");
|
||||
}
|
||||
if (entry.poolConfig) {
|
||||
reasons.push("session pool config");
|
||||
}
|
||||
|
||||
return {
|
||||
eligible: reasons.length === 0,
|
||||
reasons,
|
||||
};
|
||||
}
|
||||
|
||||
function capabilitiesFor(entry: RegistryEntry, eligible: boolean): ProviderPluginCapability[] {
|
||||
const capabilities = new Set<ProviderPluginCapability>();
|
||||
|
||||
if (entry.authType === "apikey" || entry.authType === "optional") {
|
||||
capabilities.add("apikey");
|
||||
}
|
||||
if (entry.authType === "oauth" || entry.oauth) {
|
||||
capabilities.add("oauth");
|
||||
}
|
||||
if (entry.responsesBaseUrl) {
|
||||
capabilities.add("responses");
|
||||
}
|
||||
if (entry.passthroughModels) {
|
||||
capabilities.add("passthrough-models");
|
||||
}
|
||||
if (entry.executor !== "default") {
|
||||
capabilities.add("custom-executor");
|
||||
}
|
||||
if (eligible) {
|
||||
capabilities.add("sidecar-candidate");
|
||||
}
|
||||
|
||||
return [...capabilities].sort();
|
||||
}
|
||||
|
||||
export function createProviderPluginManifestEntry(
|
||||
entry: RegistryEntry,
|
||||
): ProviderPluginManifestEntry {
|
||||
const sidecar = sidecarEligibility(entry);
|
||||
|
||||
return {
|
||||
id: entry.id,
|
||||
...(entry.alias ? { alias: entry.alias } : {}),
|
||||
format: entry.format,
|
||||
executor: entry.executor,
|
||||
auth: compactObject({
|
||||
type: entry.authType,
|
||||
header: entry.authHeader,
|
||||
prefix: entry.authPrefix,
|
||||
}) as ProviderPluginManifestEntry["auth"],
|
||||
endpoints: compactObject({
|
||||
baseUrl: entry.baseUrl,
|
||||
baseUrls: entry.baseUrls,
|
||||
responsesBaseUrl: entry.responsesBaseUrl,
|
||||
chatPath: entry.chatPath,
|
||||
modelsUrl: entry.modelsUrl,
|
||||
}) as ProviderPluginManifestEntry["endpoints"],
|
||||
capabilities: capabilitiesFor(entry, sidecar.eligible),
|
||||
passthroughModels: entry.passthroughModels === true,
|
||||
...(typeof entry.defaultContextLength === "number"
|
||||
? { defaultContextLength: entry.defaultContextLength }
|
||||
: {}),
|
||||
...(typeof entry.timeoutMs === "number" ? { timeoutMs: entry.timeoutMs } : {}),
|
||||
models: (entry.models ?? []).map(mapModel),
|
||||
sidecar,
|
||||
};
|
||||
}
|
||||
|
||||
export function generateProviderPluginManifestFromRegistry(
|
||||
registry: Record<string, RegistryEntry>,
|
||||
): ProviderPluginManifest {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
generatedFrom: "open-sse/config/providers",
|
||||
providers: Object.values(registry)
|
||||
.map(createProviderPluginManifestEntry)
|
||||
.sort((a, b) => a.id.localeCompare(b.id)),
|
||||
};
|
||||
}
|
||||
|
||||
export function getProviderPluginManifestEntryFromRegistry(
|
||||
registry: Record<string, RegistryEntry>,
|
||||
provider: string,
|
||||
): ProviderPluginManifestEntry | null {
|
||||
const entry =
|
||||
registry[provider] ||
|
||||
Object.values(registry).find((candidate) => candidate.alias === provider);
|
||||
|
||||
return entry ? createProviderPluginManifestEntry(entry) : null;
|
||||
}
|
||||
31
open-sse/config/providerPluginManifestRegistry.ts
Normal file
31
open-sse/config/providerPluginManifestRegistry.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { REGISTRY } from "./providers/index.ts";
|
||||
import {
|
||||
generateProviderPluginManifestFromRegistry,
|
||||
getProviderPluginManifestEntryFromRegistry,
|
||||
type ProviderPluginManifestEntry,
|
||||
} from "./providerPluginManifest.ts";
|
||||
|
||||
export function generateProviderPluginManifest() {
|
||||
return generateProviderPluginManifestFromRegistry(REGISTRY);
|
||||
}
|
||||
|
||||
export function getProviderPluginManifestEntry(provider: string) {
|
||||
return getProviderPluginManifestEntryFromRegistry(REGISTRY, provider);
|
||||
}
|
||||
|
||||
export function getProviderPluginManifestEntryForModel(
|
||||
model: string | undefined,
|
||||
): ProviderPluginManifestEntry | null {
|
||||
if (!model) return null;
|
||||
|
||||
const providerPrefix = model.includes("/") ? model.split("/", 1)[0] : "";
|
||||
if (providerPrefix) {
|
||||
const prefixed = getProviderPluginManifestEntry(providerPrefix);
|
||||
if (prefixed) return prefixed;
|
||||
}
|
||||
|
||||
const manifest = generateProviderPluginManifest();
|
||||
return manifest.providers.find((provider) =>
|
||||
provider.models.some((candidate) => candidate.id === model),
|
||||
) ?? null;
|
||||
}
|
||||
26
open-sse/config/providerPluginManifestUrl.ts
Normal file
26
open-sse/config/providerPluginManifestUrl.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export const PROVIDER_PLUGIN_MANIFEST_HEADER = "X-OmniRoute-Provider-Manifest-Url";
|
||||
export const PROVIDER_PLUGIN_MANIFEST_PATH = "/api/v1/provider-plugin-manifest";
|
||||
|
||||
function trimTrailingSlash(value: string): string {
|
||||
return value.replace(/\/$/, "");
|
||||
}
|
||||
|
||||
export function resolveProviderPluginManifestUrl(origin?: string | null): string {
|
||||
const configured = process.env.OMNIROUTE_PROVIDER_MANIFEST_URL?.trim();
|
||||
if (configured) return configured;
|
||||
|
||||
if (origin) {
|
||||
return `${trimTrailingSlash(origin)}${PROVIDER_PLUGIN_MANIFEST_PATH}`;
|
||||
}
|
||||
|
||||
const host = process.env.HOST || "127.0.0.1";
|
||||
const port = process.env.PORT || process.env.DASHBOARD_PORT || process.env.API_PORT || "20128";
|
||||
const protocol = process.env.OMNIROUTE_PUBLIC_PROTOCOL || "http";
|
||||
return `${protocol}://${host}:${port}${PROVIDER_PLUGIN_MANIFEST_PATH}`;
|
||||
}
|
||||
|
||||
export function getProviderPluginManifestHeader(origin?: string | null): Record<string, string> {
|
||||
return {
|
||||
[PROVIDER_PLUGIN_MANIFEST_HEADER]: resolveProviderPluginManifestUrl(origin),
|
||||
};
|
||||
}
|
||||
@@ -41,6 +41,8 @@ import { yiProvider } from "./registry/yi/index.ts";
|
||||
import { deepseekProvider } from "./registry/deepseek/index.ts";
|
||||
import { deepseek_webProvider } from "./registry/deepseek/web/index.ts";
|
||||
import { dgridProvider } from "./registry/dgrid/index.ts";
|
||||
import { baiProvider } from "./registry/bai/index.ts";
|
||||
import { qiniuProvider } from "./registry/qiniu/index.ts";
|
||||
import { kimi_coding_apikeyProvider } from "./registry/kimi/coding-apikey/index.ts";
|
||||
import { kimi_codingProvider } from "./registry/kimi/coding/index.ts";
|
||||
import { kimiProvider } from "./registry/kimi/index.ts";
|
||||
@@ -49,6 +51,9 @@ import { groqProvider } from "./registry/groq/index.ts";
|
||||
import { inference_netProvider } from "./registry/inference-net/index.ts";
|
||||
import { llm7Provider } from "./registry/llm7/index.ts";
|
||||
import { cerebrasProvider } from "./registry/cerebras/index.ts";
|
||||
import { charmHyperProvider } from "./registry/charm-hyper/index.ts";
|
||||
import { nubeProvider } from "./registry/nube/index.ts";
|
||||
import { clinepassProvider } from "./registry/clinepass/index.ts";
|
||||
import { sparkdeskProvider } from "./registry/sparkdesk/index.ts";
|
||||
import { nlpcloudProvider } from "./registry/nlpcloud/index.ts";
|
||||
import { nvidiaProvider } from "./registry/nvidia/index.ts";
|
||||
@@ -78,6 +83,7 @@ import { leonardoProvider } from "./registry/leonardo/index.ts";
|
||||
import { grok_webProvider } from "./registry/grok-web/index.ts";
|
||||
import { kieProvider } from "./registry/kie/index.ts";
|
||||
import { monsterapiProvider } from "./registry/monsterapi/index.ts";
|
||||
import { modelscopeProvider } from "./registry/modelscope/index.ts";
|
||||
import { sensenovaProvider } from "./registry/sensenova/index.ts";
|
||||
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
|
||||
import { lambda_aiProvider } from "./registry/lambda-ai/index.ts";
|
||||
@@ -141,6 +147,7 @@ import { kilo_gatewayProvider } from "./registry/kilo-gateway/index.ts";
|
||||
import { bailian_coding_planProvider } from "./registry/bailian-coding-plan/index.ts";
|
||||
import { gigachatProvider } from "./registry/gigachat/index.ts";
|
||||
import { devin_cliProvider } from "./registry/devin-cli/index.ts";
|
||||
import { auggieProvider } from "./registry/auggie/index.ts";
|
||||
import { chutesProvider } from "./registry/chutes/index.ts";
|
||||
import { factoryProvider } from "./registry/factory/index.ts";
|
||||
import { databricksProvider } from "./registry/databricks/index.ts";
|
||||
@@ -171,6 +178,8 @@ import { grok_cliProvider } from "./registry/grok-cli/index.ts";
|
||||
import { codebuddy_cnProvider } from "./registry/codebuddy-cn/index.ts";
|
||||
import { pioneerProvider } from "./registry/pioneer/index.ts";
|
||||
import { zenmux_freeProvider } from "./registry/zenmux-free/index.ts";
|
||||
import { sumopodProvider } from "./registry/sumopod/index.ts";
|
||||
import { x5labProvider } from "./registry/x5lab/index.ts";
|
||||
|
||||
export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
aimlapi: aimlapiProvider,
|
||||
@@ -212,6 +221,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
deepseek: deepseekProvider,
|
||||
"deepseek-web": deepseek_webProvider,
|
||||
dgrid: dgridProvider,
|
||||
bai: baiProvider,
|
||||
qiniu: qiniuProvider,
|
||||
"kimi-coding-apikey": kimi_coding_apikeyProvider,
|
||||
"kimi-coding": kimi_codingProvider,
|
||||
kimi: kimiProvider,
|
||||
@@ -220,6 +231,9 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"inference-net": inference_netProvider,
|
||||
llm7: llm7Provider,
|
||||
cerebras: cerebrasProvider,
|
||||
"charm-hyper": charmHyperProvider,
|
||||
nube: nubeProvider,
|
||||
clinepass: clinepassProvider,
|
||||
sparkdesk: sparkdeskProvider,
|
||||
nlpcloud: nlpcloudProvider,
|
||||
nvidia: nvidiaProvider,
|
||||
@@ -249,6 +263,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"grok-web": grok_webProvider,
|
||||
kie: kieProvider,
|
||||
monsterapi: monsterapiProvider,
|
||||
modelscope: modelscopeProvider,
|
||||
sensenova: sensenovaProvider,
|
||||
hyperbolic: hyperbolicProvider,
|
||||
"lambda-ai": lambda_aiProvider,
|
||||
@@ -311,6 +326,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"bailian-coding-plan": bailian_coding_planProvider,
|
||||
gigachat: gigachatProvider,
|
||||
"devin-cli": devin_cliProvider,
|
||||
auggie: auggieProvider,
|
||||
chutes: chutesProvider,
|
||||
factory: factoryProvider,
|
||||
databricks: databricksProvider,
|
||||
@@ -344,4 +360,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
"codebuddy-cn": codebuddy_cnProvider,
|
||||
pioneer: pioneerProvider,
|
||||
"zenmux-free": zenmux_freeProvider,
|
||||
sumopod: sumopodProvider,
|
||||
x5lab: x5labProvider,
|
||||
};
|
||||
|
||||
@@ -38,6 +38,13 @@ export const anthropicProvider: RegistryEntry = {
|
||||
},
|
||||
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" },
|
||||
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" },
|
||||
{
|
||||
id: "claude-sonnet-5",
|
||||
name: "Claude Sonnet 5",
|
||||
contextLength: 1048576,
|
||||
// Sonnet 5 rejects non-default sampling params with a 400 (adaptive-only).
|
||||
unsupportedParams: ["temperature", "top_p", "top_k"],
|
||||
},
|
||||
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
|
||||
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.6" },
|
||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },
|
||||
|
||||
38
open-sse/config/providers/registry/auggie/index.ts
Normal file
38
open-sse/config/providers/registry/auggie/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// Augment / Auggie CLI — local no-auth provider. The executor spawns the
|
||||
// user's local `auggie` binary (auth handled entirely by `auggie login`);
|
||||
// OmniRoute never stores credentials for this connection.
|
||||
export const auggieProvider: RegistryEntry = {
|
||||
id: "auggie",
|
||||
alias: "aug",
|
||||
format: "openai",
|
||||
executor: "auggie",
|
||||
baseUrl: "auggie://cli/stdio",
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
defaultContextLength: 200000,
|
||||
models: [
|
||||
// Claude
|
||||
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", contextLength: 200000 },
|
||||
{
|
||||
id: "claude-sonnet-4.6-thinking",
|
||||
name: "Claude Sonnet 4.6 Thinking",
|
||||
contextLength: 200000,
|
||||
},
|
||||
{ id: "claude-opus-4.6", name: "Claude Opus 4.6", contextLength: 200000 },
|
||||
{ id: "claude-haiku-4.5", name: "Claude Haiku 4.5", contextLength: 200000 },
|
||||
// Gemini
|
||||
{ id: "gemini-3.1-pro", name: "Gemini 3.1 Pro", contextLength: 1000000 },
|
||||
{ id: "gemini-3.0-flash", name: "Gemini 3 Flash", contextLength: 1000000 },
|
||||
// GPT-5.x
|
||||
{ id: "gpt-5.5-high", name: "GPT-5.5 High", contextLength: 200000 },
|
||||
{ id: "gpt-5.5-medium", name: "GPT-5.5 Medium", contextLength: 200000 },
|
||||
{ id: "gpt-5.4-high", name: "GPT-5.4 High", contextLength: 200000 },
|
||||
{ id: "gpt-5.4-medium", name: "GPT-5.4 Medium", contextLength: 200000 },
|
||||
// Kimi
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6", contextLength: 131000 },
|
||||
// Prism (Augment's in-house model)
|
||||
{ id: "prism", name: "Augment Prism", contextLength: 200000 },
|
||||
],
|
||||
};
|
||||
14
open-sse/config/providers/registry/bai/index.ts
Normal file
14
open-sse/config/providers/registry/bai/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const baiProvider: RegistryEntry = {
|
||||
id: "bai",
|
||||
alias: "bai",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.b.ai/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://api.b.ai/v1/models",
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -12,6 +12,7 @@ export const blackboxProvider: RegistryEntry = {
|
||||
models: [
|
||||
{ id: "claude-fable-5", name: "Claude Fable 5" },
|
||||
{ id: "claude-opus-4.8", name: "Claude Opus 4.8" },
|
||||
{ id: "claude-sonnet-5", name: "Claude Sonnet 5" },
|
||||
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
|
||||
{ id: "gpt-5.5", name: "GPT-5.5" },
|
||||
{ id: "gpt-5.4-pro", name: "GPT-5.4 Pro" },
|
||||
|
||||
14
open-sse/config/providers/registry/charm-hyper/index.ts
Normal file
14
open-sse/config/providers/registry/charm-hyper/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const charmHyperProvider: RegistryEntry = {
|
||||
id: "charm-hyper",
|
||||
alias: "charm-hyper",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://hyper.charm.land/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://hyper.charm.land/v1/models",
|
||||
models: [{ id: "hyper/auto", name: "Charm Hyper Auto" }],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -65,6 +65,18 @@ export const claudeProvider: RegistryEntry = {
|
||||
contextLength: 200000,
|
||||
maxOutputTokens: 64000,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-5",
|
||||
name: "Claude Sonnet 5",
|
||||
contextLength: 1000000,
|
||||
maxOutputTokens: 128000,
|
||||
// Sonnet 5 is the first Sonnet-tier model to support xhigh effort — do NOT copy
|
||||
// the `supportsXHighEffort: false` from the older claude-sonnet-4-6/4-5 entries.
|
||||
supportsXHighEffort: true,
|
||||
// Sonnet 5 rejects non-default temperature/top_p/top_k with a 400 (adaptive-only;
|
||||
// reasoning steered by output_config.effort). Mirrors the Opus/Fable entries.
|
||||
unsupportedParams: ["temperature", "top_p", "top_k"],
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-6",
|
||||
name: "Claude 4.6 Sonnet",
|
||||
|
||||
42
open-sse/config/providers/registry/clinepass/index.ts
Normal file
42
open-sse/config/providers/registry/clinepass/index.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// ClinePass — Cline's $9.99/mo BYOK API-key gateway (https://cline.bot). Distinct
|
||||
// from the OAuth `cline` provider: same host (api.cline.bot) but a plain Bearer
|
||||
// API key and the `cline-pass/*` model namespace. Responses are wrapped in a
|
||||
// {success, data} envelope — unwrapped by open-sse/utils/clinepassEnvelope.ts.
|
||||
export const clinepassProvider: RegistryEntry = {
|
||||
id: "clinepass",
|
||||
alias: "clinepass",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.cline.bot/api/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
extraHeaders: {
|
||||
"HTTP-Referer": "https://cline.bot",
|
||||
"X-Title": "Cline",
|
||||
},
|
||||
models: [
|
||||
{ id: "cline-pass/glm-5.2", name: "GLM-5.2 (ClinePass)" },
|
||||
{ id: "cline-pass/kimi-k2.7-code", name: "Kimi K2.7 Code (ClinePass)" },
|
||||
{ id: "cline-pass/kimi-k2.6", name: "Kimi K2.6 (ClinePass)" },
|
||||
{
|
||||
id: "cline-pass/deepseek-v4-pro",
|
||||
name: "DeepSeek V4 Pro (ClinePass)",
|
||||
supportsReasoning: true,
|
||||
maxOutputTokens: 50000,
|
||||
},
|
||||
{
|
||||
id: "cline-pass/deepseek-v4-flash",
|
||||
name: "DeepSeek V4 Flash (ClinePass)",
|
||||
supportsReasoning: true,
|
||||
maxOutputTokens: 50000,
|
||||
},
|
||||
{ id: "cline-pass/mimo-v2.5", name: "MiMo-V2.5 (ClinePass)" },
|
||||
{ id: "cline-pass/mimo-v2.5-pro", name: "MiMo-V2.5-Pro (ClinePass)" },
|
||||
{ id: "cline-pass/minimax-m3", name: "MiniMax M3 (ClinePass)", supportsVision: true },
|
||||
{ id: "cline-pass/qwen3.7-max", name: "Qwen3.7 Max (ClinePass)" },
|
||||
{ id: "cline-pass/qwen3.7-plus", name: "Qwen3.7 Plus (ClinePass)" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -14,13 +14,13 @@ export const grok_cliProvider: RegistryEntry = {
|
||||
{
|
||||
id: "grok-build",
|
||||
name: "Grok Build",
|
||||
contextLength: 128000,
|
||||
contextLength: 256000,
|
||||
unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"],
|
||||
},
|
||||
{
|
||||
id: "grok-composer-2.5-fast",
|
||||
name: "Grok Composer 2.5 Fast",
|
||||
contextLength: 128000,
|
||||
contextLength: 200000,
|
||||
unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -14,8 +14,14 @@ export const kimi_webProvider: RegistryEntry = {
|
||||
authType: "apikey",
|
||||
authHeader: "cookie",
|
||||
models: [
|
||||
{ id: "kimi-default", name: "Kimi Default" },
|
||||
{ id: "kimi-k2.6", name: "Kimi K2.6 (Thinking)" },
|
||||
{ id: "kimi-128k", name: "Kimi 128K (Long Context)" },
|
||||
// Model ids are the `key` field from www.kimi.com's
|
||||
// `/apiv2/kimi.gateway.config.v1.ConfigService/GetAvailableModels` response.
|
||||
// Agent / Agent-Swarm variants (`k2d6-agent`, `k2d6-agent-ultra`) are
|
||||
// intentionally NOT exposed — they need a different scenario
|
||||
// (`SCENARIO_OK_COMPUTER`) plus `kimiPlusId` / `agentMode` fields, which
|
||||
// the executor does not yet shape. Use `kimi-coding` (api.kimi.com) for
|
||||
// agentic flows.
|
||||
{ id: "k2d6", name: "K2.6 Instant" },
|
||||
{ id: "k2d6-thinking", name: "K2.6 Thinking", supportsReasoning: true },
|
||||
],
|
||||
};
|
||||
|
||||
24
open-sse/config/providers/registry/modelscope/index.ts
Normal file
24
open-sse/config/providers/registry/modelscope/index.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
// ModelScope (Alibaba 魔搭) — OpenAI-compatible API-Inference, ported from upstream
|
||||
// 9router PR #1764 (@tn5052). The upstream PR hardcoded `https://api-inference.modelscope.ai/...`
|
||||
// (`.ai` TLD) and a static 5-model list. Both were dropped here after verification:
|
||||
//
|
||||
// - baseUrl: ModelScope's own API-Inference docs (modelscope.cn/docs/model-service/API-Inference)
|
||||
// and third-party integration guides (e.g. Alibaba Cloud Model Studio compatibility docs)
|
||||
// consistently confirm the production domain is `api-inference.modelscope.cn` — the `.cn` TLD,
|
||||
// not `.ai`. Using the unverified `.ai` domain would have shipped a broken provider.
|
||||
// - models: passthrough + empty static seed instead of copying the PR's 5-model snapshot, since
|
||||
// ModelScope hosts a large and fast-moving open-model catalog — `modelsUrl` keeps the list live.
|
||||
export const modelscopeProvider: RegistryEntry = {
|
||||
id: "modelscope",
|
||||
alias: "ms",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api-inference.modelscope.cn/v1/chat/completions",
|
||||
modelsUrl: "https://api-inference.modelscope.cn/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
passthroughModels: true,
|
||||
models: [],
|
||||
};
|
||||
17
open-sse/config/providers/registry/nube/index.ts
Normal file
17
open-sse/config/providers/registry/nube/index.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const nubeProvider: RegistryEntry = {
|
||||
id: "nube",
|
||||
alias: "nube",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://ai.nube.sh/api/v1/chat/completions",
|
||||
modelsUrl: "https://ai.nube.sh/api/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
// Nube.sh is an OpenAI-compatible LiteLLM gateway (BYOK). Its live catalog is only
|
||||
// reachable with a valid key (/api/v1/models returns 401 unauthenticated), so we ship
|
||||
// no hardcoded model IDs and rely on passthrough + live enumeration via modelsUrl.
|
||||
passthroughModels: true,
|
||||
models: [],
|
||||
};
|
||||
15
open-sse/config/providers/registry/qiniu/index.ts
Normal file
15
open-sse/config/providers/registry/qiniu/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const qiniuProvider: RegistryEntry = {
|
||||
id: "qiniu",
|
||||
alias: "qiniu",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.qnaigc.com/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://api.qnaigc.com/v1/models",
|
||||
defaultContextLength: 128000,
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -18,6 +18,8 @@ export const qoderProvider: RegistryEntry = {
|
||||
},
|
||||
models: [
|
||||
{ id: "qoder-rome-30ba3b", name: "Qoder ROME" },
|
||||
{ id: "glm-5.2", name: "GLM-5.2" },
|
||||
{ id: "minimax-m3", name: "MiniMax M3", supportsVision: true },
|
||||
{ id: "qwen3-coder-plus", name: "Qwen3 Coder Plus" },
|
||||
{ id: "qwen3-max", name: "Qwen3 Max" },
|
||||
{ id: "qwen3-vl-plus", name: "Qwen3 Vision Plus", supportsVision: true },
|
||||
|
||||
15
open-sse/config/providers/registry/sumopod/index.ts
Normal file
15
open-sse/config/providers/registry/sumopod/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const sumopodProvider: RegistryEntry = {
|
||||
id: "sumopod",
|
||||
alias: "sumopod",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://ai.sumopod.com/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://ai.sumopod.com/v1/models",
|
||||
defaultContextLength: 128000,
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -11,15 +11,41 @@ export const theoldllmProvider: RegistryEntry = {
|
||||
authType: "none",
|
||||
authHeader: "none",
|
||||
defaultContextLength: 200000,
|
||||
// Catalog seed. `passthroughModels: true` means live /api/chatgpt discovery is
|
||||
// authoritative; this list is the curated display/fallback set. The upstream IDs
|
||||
// (GPT_5_*, gemini_*, CLAUDE_4_*, openrouter_*, etc.) mirror the site's free
|
||||
// "chatgpt" tier and MUST match `CHATGPT_UPSTREAM_MODELS` in the executor so they
|
||||
// route unchanged. Legacy alias IDs (GPT_4o, claude_opus_4, …) are kept for
|
||||
// backward compatibility with saved model preferences (mapped in the executor).
|
||||
models: [
|
||||
// ── Current free tier (refreshed for #5181) ──
|
||||
{ id: "GPT_5_4", name: "GPT-5.4 (The Old LLM 🆓)", contextLength: 400000 },
|
||||
{ id: "GPT_5_3", name: "GPT-5.3 (The Old LLM 🆓)", contextLength: 400000 },
|
||||
{ id: "GPT_5_2", name: "GPT-5.2 (The Old LLM 🆓)", contextLength: 400000 },
|
||||
{ id: "GPT_5_1", name: "GPT-5.1 (The Old LLM 🆓)", contextLength: 400000 },
|
||||
{ id: "GPT_5", name: "GPT-5 (The Old LLM 🆓)", contextLength: 400000 },
|
||||
{ id: "GPT_o4_mini", name: "o4-mini (The Old LLM 🆓)" },
|
||||
{ id: "GPT_o3_mini", name: "o3-mini (The Old LLM 🆓)" },
|
||||
{ id: "gemini_3_pro", name: "Gemini 3 Pro (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
{ id: "gemini_2_5_pro", name: "Gemini 2.5 Pro (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
{ id: "gemini_2_0_flash", name: "Gemini 2.0 Flash (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
{ id: "gemini_1_5_flash", name: "Gemini 1.5 Flash (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
{ id: "CLAUDE_4_6_OPUS", name: "Claude 4.6 Opus (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "CLAUDE_4_6_SONNET", name: "Claude 4.6 Sonnet (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "CLAUDE_4_5_HAIKU", name: "Claude 4.5 Haiku (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "openrouter_gpt_4_o", name: "GPT-4o (The Old LLM 🆓)" },
|
||||
{ id: "openrouter_gpt_4_o_mini", name: "GPT-4o mini (The Old LLM 🆓)" },
|
||||
{ id: "openrouter_grok_4", name: "Grok 4 (The Old LLM 🆓)" },
|
||||
{ id: "together_deepseek_v3", name: "DeepSeek V3 (The Old LLM 🆓)" },
|
||||
{ id: "openrouter_deepseek_r1", name: "DeepSeek R1 (The Old LLM 🆓)" },
|
||||
{ id: "sonar-pro", name: "Sonar Pro (The Old LLM 🆓)" },
|
||||
// ── Legacy alias IDs (kept for saved-preference backward compatibility) ──
|
||||
{ id: "GPT_4o", name: "GPT-4o (The Old LLM 🆓)" },
|
||||
{ id: "claude_opus_4", name: "Claude Opus 4 (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "claude_sonnet_4", name: "Claude Sonnet 4 (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "claude_haiku_3_5", name: "Claude Haiku 3.5 (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "deepseek_v4", name: "DeepSeek V4 (The Old LLM 🆓)", contextLength: 200000 },
|
||||
{ id: "gemini_3_flash", name: "Gemini 3 Flash (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
{ id: "gemini_3_pro", name: "Gemini 3 Pro (The Old LLM 🆓)", contextLength: 1000000 },
|
||||
],
|
||||
passthroughModels: true,
|
||||
};
|
||||
|
||||
15
open-sse/config/providers/registry/x5lab/index.ts
Normal file
15
open-sse/config/providers/registry/x5lab/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { RegistryEntry } from "../../shared.ts";
|
||||
|
||||
export const x5labProvider: RegistryEntry = {
|
||||
id: "x5lab",
|
||||
alias: "x5lab",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://api.x5lab.dev/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
modelsUrl: "https://api.x5lab.dev/v1/models",
|
||||
defaultContextLength: 128000,
|
||||
models: [],
|
||||
passthroughModels: true,
|
||||
};
|
||||
@@ -4,7 +4,7 @@ export const xaiProvider: RegistryEntry = {
|
||||
id: "xai",
|
||||
alias: "xai",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
executor: "xai",
|
||||
baseUrl: "https://api.x.ai/v1/chat/completions",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
|
||||
@@ -46,7 +46,13 @@ import {
|
||||
} from "../services/cloudCodeThinking.ts";
|
||||
import { buildGeminiTools } from "../translator/helpers/geminiToolsSanitizer.ts";
|
||||
import { DEFAULT_SAFETY_SETTINGS } from "../translator/helpers/geminiHelper.ts";
|
||||
import { normalizeOpenAICompatibleFinishReasonString } from "../utils/finishReason.ts";
|
||||
import {
|
||||
type AntigravityCollectedStream,
|
||||
processAntigravitySSEText,
|
||||
flushAntigravitySSEText,
|
||||
} from "./antigravity/sseCollect.ts";
|
||||
// processAntigravitySSEPayload re-exported for external importers (tests).
|
||||
export { processAntigravitySSEPayload } from "./antigravity/sseCollect.ts";
|
||||
import {
|
||||
applyAntigravityClientProfileHeaders,
|
||||
removeHeaderCaseInsensitive,
|
||||
@@ -155,70 +161,6 @@ function serializeAntigravityRequest(
|
||||
return applyFingerprint(provider, { ...headers }, serializedBody);
|
||||
}
|
||||
|
||||
type AntigravityCollectedStream = {
|
||||
textContent: string;
|
||||
finishReason: string;
|
||||
toolCalls: Array<{
|
||||
id: string;
|
||||
index: number;
|
||||
type: "function";
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
usage: Record<string, unknown> | null;
|
||||
remainingCredits: Array<{ creditType: string; creditAmount: string }> | null;
|
||||
};
|
||||
|
||||
function stripZeroWidth(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return value.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => stripZeroWidth(item));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
||||
key,
|
||||
stripZeroWidth(item),
|
||||
])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseAntigravityTextualToolCall(text: unknown): { name: string; args: unknown } | null {
|
||||
if (typeof text !== "string") return null;
|
||||
const normalized = text.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
const match = normalized.match(
|
||||
/^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/
|
||||
);
|
||||
if (!match) return null;
|
||||
const name = match[1]?.trim();
|
||||
const rawArgs = match[2]?.trim();
|
||||
if (!name || !rawArgs) return null;
|
||||
try {
|
||||
return { name, args: stripZeroWidth(JSON.parse(rawArgs)) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function addAntigravityTextualToolCall(
|
||||
collected: AntigravityCollectedStream,
|
||||
parsed: { name: string; args: unknown }
|
||||
): void {
|
||||
collected.toolCalls.push({
|
||||
id: `${parsed.name}-${Date.now()}-${collected.toolCalls.length}`,
|
||||
index: collected.toolCalls.length,
|
||||
type: "function",
|
||||
function: {
|
||||
name: parsed.name,
|
||||
arguments: JSON.stringify(parsed.args || {}),
|
||||
},
|
||||
});
|
||||
collected.finishReason = "tool_calls";
|
||||
}
|
||||
|
||||
type AntigravityRequestEnvelope = Record<string, unknown> & {
|
||||
project: string;
|
||||
model?: string;
|
||||
@@ -372,84 +314,6 @@ export function markConnectionQuotaExhausted(connectionId: string, retryAfterMs:
|
||||
* Accumulate one Antigravity SSE `data:` payload into `collected`. Exported for unit
|
||||
* tests (the markdown / candidate-parts extraction branches). @internal
|
||||
*/
|
||||
export function processAntigravitySSEPayload(
|
||||
payload: string,
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
if (!payload || payload === "[DONE]") return;
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
const markdown =
|
||||
typeof parsed?.markdown === "string"
|
||||
? parsed.markdown
|
||||
: typeof parsed?.response?.markdown === "string"
|
||||
? parsed.response.markdown
|
||||
: null;
|
||||
if (markdown) {
|
||||
collected.textContent += markdown;
|
||||
}
|
||||
const candidate = parsed?.response?.candidates?.[0];
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (typeof part.text === "string" && !part.thought && !part.thoughtSignature) {
|
||||
const textualToolCall = parseAntigravityTextualToolCall(part.text);
|
||||
if (textualToolCall) {
|
||||
addAntigravityTextualToolCall(collected, textualToolCall);
|
||||
} else {
|
||||
collected.textContent += part.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidate?.finishReason) {
|
||||
collected.finishReason = normalizeOpenAICompatibleFinishReasonString(
|
||||
String(candidate.finishReason).toLowerCase()
|
||||
);
|
||||
}
|
||||
if (parsed?.response?.usageMetadata) {
|
||||
const um = parsed.response.usageMetadata;
|
||||
collected.usage = {
|
||||
prompt_tokens: um.promptTokenCount || 0,
|
||||
completion_tokens: um.candidatesTokenCount || 0,
|
||||
total_tokens: um.totalTokenCount || 0,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(parsed?.remainingCredits)) {
|
||||
collected.remainingCredits = parsed.remainingCredits;
|
||||
}
|
||||
} catch {
|
||||
log?.debug?.("SSE_PARSE", `Skipping malformed SSE line: ${payload.slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
|
||||
function processAntigravitySSEText(
|
||||
text: string,
|
||||
partialLine: { value: string },
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
partialLine.value += text;
|
||||
const lines = partialLine.value.split("\n");
|
||||
partialLine.value = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
processAntigravitySSEPayload(trimmed.slice(5).trim(), collected, log);
|
||||
}
|
||||
}
|
||||
|
||||
function flushAntigravitySSEText(
|
||||
partialLine: { value: string },
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
const trimmed = partialLine.value.trim();
|
||||
partialLine.value = "";
|
||||
if (!trimmed.startsWith("data:")) return;
|
||||
processAntigravitySSEPayload(trimmed.slice(5).trim(), collected, log);
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip provider prefixes (e.g. "antigravity/model" → "model").
|
||||
|
||||
148
open-sse/executors/antigravity/sseCollect.ts
Normal file
148
open-sse/executors/antigravity/sseCollect.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
// Pure SSE-payload -> collected-stream parsing for the Antigravity executor.
|
||||
// Extracted verbatim from antigravity.ts (no host state, no fetch/auth).
|
||||
import { normalizeOpenAICompatibleFinishReasonString } from "../../utils/finishReason.ts";
|
||||
|
||||
export type AntigravityCollectedStream = {
|
||||
textContent: string;
|
||||
finishReason: string;
|
||||
toolCalls: Array<{
|
||||
id: string;
|
||||
index: number;
|
||||
type: "function";
|
||||
function: { name: string; arguments: string };
|
||||
}>;
|
||||
usage: Record<string, unknown> | null;
|
||||
remainingCredits: Array<{ creditType: string; creditAmount: string }> | null;
|
||||
};
|
||||
|
||||
export function stripZeroWidth(value: unknown): unknown {
|
||||
if (typeof value === "string") {
|
||||
return value.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => stripZeroWidth(item));
|
||||
}
|
||||
if (value && typeof value === "object") {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value as Record<string, unknown>).map(([key, item]) => [
|
||||
key,
|
||||
stripZeroWidth(item),
|
||||
])
|
||||
);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseAntigravityTextualToolCall(
|
||||
text: unknown
|
||||
): { name: string; args: unknown } | null {
|
||||
if (typeof text !== "string") return null;
|
||||
const normalized = text.replace(/[\u200B-\u200D\uFEFF]/g, "");
|
||||
const match = normalized.match(
|
||||
/^[\s\S]*?\[Tool call:\s*([^\]\n]+)\]\s*\nArguments:\s*([\s\S]+?)\s*$/
|
||||
);
|
||||
if (!match) return null;
|
||||
const name = match[1]?.trim();
|
||||
const rawArgs = match[2]?.trim();
|
||||
if (!name || !rawArgs) return null;
|
||||
try {
|
||||
return { name, args: stripZeroWidth(JSON.parse(rawArgs)) };
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function addAntigravityTextualToolCall(
|
||||
collected: AntigravityCollectedStream,
|
||||
parsed: { name: string; args: unknown }
|
||||
): void {
|
||||
collected.toolCalls.push({
|
||||
id: `${parsed.name}-${Date.now()}-${collected.toolCalls.length}`,
|
||||
index: collected.toolCalls.length,
|
||||
type: "function",
|
||||
function: {
|
||||
name: parsed.name,
|
||||
arguments: JSON.stringify(parsed.args || {}),
|
||||
},
|
||||
});
|
||||
collected.finishReason = "tool_calls";
|
||||
}
|
||||
|
||||
export function processAntigravitySSEPayload(
|
||||
payload: string,
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
if (!payload || payload === "[DONE]") return;
|
||||
try {
|
||||
const parsed = JSON.parse(payload);
|
||||
const markdown =
|
||||
typeof parsed?.markdown === "string"
|
||||
? parsed.markdown
|
||||
: typeof parsed?.response?.markdown === "string"
|
||||
? parsed.response.markdown
|
||||
: null;
|
||||
if (markdown) {
|
||||
collected.textContent += markdown;
|
||||
}
|
||||
const candidate = parsed?.response?.candidates?.[0];
|
||||
if (candidate?.content?.parts) {
|
||||
for (const part of candidate.content.parts) {
|
||||
if (typeof part.text === "string" && !part.thought && !part.thoughtSignature) {
|
||||
const textualToolCall = parseAntigravityTextualToolCall(part.text);
|
||||
if (textualToolCall) {
|
||||
addAntigravityTextualToolCall(collected, textualToolCall);
|
||||
} else {
|
||||
collected.textContent += part.text;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (candidate?.finishReason) {
|
||||
collected.finishReason = normalizeOpenAICompatibleFinishReasonString(
|
||||
String(candidate.finishReason).toLowerCase()
|
||||
);
|
||||
}
|
||||
if (parsed?.response?.usageMetadata) {
|
||||
const um = parsed.response.usageMetadata;
|
||||
collected.usage = {
|
||||
prompt_tokens: um.promptTokenCount || 0,
|
||||
completion_tokens: um.candidatesTokenCount || 0,
|
||||
total_tokens: um.totalTokenCount || 0,
|
||||
};
|
||||
}
|
||||
if (Array.isArray(parsed?.remainingCredits)) {
|
||||
collected.remainingCredits = parsed.remainingCredits;
|
||||
}
|
||||
} catch {
|
||||
log?.debug?.("SSE_PARSE", `Skipping malformed SSE line: ${payload.slice(0, 80)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function processAntigravitySSEText(
|
||||
text: string,
|
||||
partialLine: { value: string },
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
partialLine.value += text;
|
||||
const lines = partialLine.value.split("\n");
|
||||
partialLine.value = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("data:")) continue;
|
||||
processAntigravitySSEPayload(trimmed.slice(5).trim(), collected, log);
|
||||
}
|
||||
}
|
||||
|
||||
export function flushAntigravitySSEText(
|
||||
partialLine: { value: string },
|
||||
collected: AntigravityCollectedStream,
|
||||
log?: { debug?: (scope: string, message: string) => void }
|
||||
) {
|
||||
const trimmed = partialLine.value.trim();
|
||||
partialLine.value = "";
|
||||
if (!trimmed.startsWith("data:")) return;
|
||||
processAntigravitySSEPayload(trimmed.slice(5).trim(), collected, log);
|
||||
}
|
||||
569
open-sse/executors/auggie.ts
Normal file
569
open-sse/executors/auggie.ts
Normal file
@@ -0,0 +1,569 @@
|
||||
/**
|
||||
* AuggieExecutor — routes completions through the local Augment CLI ("auggie")
|
||||
* binary via a one-shot stdin/stdout text pipe (no JSON-RPC / ACP protocol).
|
||||
*
|
||||
* Flow:
|
||||
* 1. Flatten the OpenAI-shaped `messages[]` into a single prompt string.
|
||||
* 2. Spawn `auggie --print --quiet --model <model>` and pipe the prompt on stdin.
|
||||
* 3. Relay stdout chunks as OpenAI-compatible SSE deltas (stream=true) or
|
||||
* buffer them into a single chat.completion JSON body (stream=false).
|
||||
* 4. Kill the subprocess on abort / stream close.
|
||||
*
|
||||
* Authentication:
|
||||
* None. Auggie delegates auth entirely to the user's local `auggie login`
|
||||
* session — OmniRoute never sees or stores credentials for this provider.
|
||||
* The connection is registered `noAuth: true` and `refreshCredentials()` is
|
||||
* a no-op (nothing to refresh).
|
||||
*
|
||||
* Binary discovery:
|
||||
* 1. AUGGIE_BIN / CLI_AUGGIE_BIN env var (absolute path override)
|
||||
* 2. PATH lookup ("auggie" / "auggie.cmd")
|
||||
* 3. %LOCALAPPDATA%\auggie\bin\auggie.exe (Windows installer)
|
||||
* 4. ~/.local/share/auggie/bin/auggie (Linux installer)
|
||||
* 5. ~/.auggie/bin/auggie (alternate installer layout)
|
||||
*/
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import fs from "node:fs";
|
||||
import { BaseExecutor, type ExecuteInput, type ProviderCredentials } from "./base.ts";
|
||||
import { buildErrorBody, errorResponse, sanitizeErrorMessage } from "../utils/error.ts";
|
||||
import { auggieProvider } from "../config/providers/registry/auggie/index.ts";
|
||||
|
||||
const AUGGIE_URL = "auggie://cli/stdio";
|
||||
|
||||
// ─── Model allowlist (argument-injection defense) ─────────────────────────────
|
||||
// The `model` value is forwarded straight into the `auggie` argv, so it is an
|
||||
// untrusted-input sink. We only ever pass a model that is declared in the
|
||||
// registry entry — this closes flag-smuggling (a `model` starting with "-" would
|
||||
// otherwise be parsed by auggie as an option) and unknown-model passthrough.
|
||||
const AUGGIE_MODEL_ALLOWLIST: ReadonlySet<string> = new Set(
|
||||
auggieProvider.models.map((m) => m.id)
|
||||
);
|
||||
const DEFAULT_AUGGIE_MODEL = auggieProvider.models[0]?.id ?? "claude-sonnet-4.6";
|
||||
|
||||
type AuggieModelResolution = { ok: true; model: string } | { ok: false; error: string };
|
||||
|
||||
/**
|
||||
* Validate + resolve the requested model against the registry allowlist.
|
||||
* Rejects flag-smuggling (leading "-") and any id not declared in the registry.
|
||||
* An empty/absent model resolves to the registry's first (default) model.
|
||||
*/
|
||||
export function resolveAuggieModel(model: unknown): AuggieModelResolution {
|
||||
const requested = typeof model === "string" ? model.trim() : "";
|
||||
if (!requested) return { ok: true, model: DEFAULT_AUGGIE_MODEL };
|
||||
if (requested.startsWith("-")) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Invalid Auggie model "${requested}": model must not start with "-".`,
|
||||
};
|
||||
}
|
||||
if (!AUGGIE_MODEL_ALLOWLIST.has(requested)) {
|
||||
return {
|
||||
ok: false,
|
||||
error: `Unknown Auggie model "${requested}". Supported models: ${[
|
||||
...AUGGIE_MODEL_ALLOWLIST,
|
||||
].join(", ")}.`,
|
||||
};
|
||||
}
|
||||
return { ok: true, model: requested };
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the auggie argv. `model` MUST already be allowlist-validated via
|
||||
* resolveAuggieModel(). The trailing `--` marks the end of options so no
|
||||
* later positional value can be reinterpreted as a flag.
|
||||
*/
|
||||
function buildAuggieArgs(model: string): string[] {
|
||||
return ["--print", "--quiet", "--model", model, "--"];
|
||||
}
|
||||
|
||||
// ─── Binary discovery ────────────────────────────────────────────────────────
|
||||
|
||||
export function resolveAuggieBin(): string {
|
||||
// 1. Explicit override
|
||||
const envBin = (process.env.AUGGIE_BIN || process.env.CLI_AUGGIE_BIN || "").trim();
|
||||
if (envBin) return envBin;
|
||||
|
||||
const isWin = process.platform === "win32";
|
||||
|
||||
// 2. Windows installer default: %LOCALAPPDATA%\auggie\bin\auggie.exe
|
||||
if (isWin) {
|
||||
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), "AppData", "Local");
|
||||
const winPath = path.join(localAppData, "auggie", "bin", "auggie.exe");
|
||||
if (fs.existsSync(winPath)) return winPath;
|
||||
}
|
||||
|
||||
// 3. Linux/macOS installer paths
|
||||
const home = os.homedir();
|
||||
for (const candidate of [
|
||||
path.join(home, ".local", "share", "auggie", "bin", "auggie"),
|
||||
path.join(home, ".auggie", "bin", "auggie"),
|
||||
]) {
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
}
|
||||
|
||||
// Fallback — rely on PATH
|
||||
return isWin ? "auggie.cmd" : "auggie";
|
||||
}
|
||||
|
||||
// ─── Multi-turn message → single prompt builder ───────────────────────────────
|
||||
|
||||
type OpenAIMsg = { role?: string; content?: unknown };
|
||||
|
||||
export function buildAuggiePrompt(messages: OpenAIMsg[]): string {
|
||||
const lines: string[] = [];
|
||||
for (const m of messages) {
|
||||
const role = String(m.role || "user");
|
||||
let text = "";
|
||||
if (typeof m.content === "string") {
|
||||
text = m.content;
|
||||
} else if (Array.isArray(m.content)) {
|
||||
for (const p of m.content) {
|
||||
if (p && typeof p === "object" && (p as Record<string, unknown>).type === "text") {
|
||||
text += String((p as Record<string, unknown>).text || "");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!text.trim()) continue;
|
||||
if (role === "system") {
|
||||
lines.push(`[System]\n${text}`);
|
||||
} else if (role === "assistant") {
|
||||
lines.push(`[Assistant]\n${text}`);
|
||||
} else {
|
||||
lines.push(`[User]\n${text}`);
|
||||
}
|
||||
}
|
||||
return lines.join("\n\n") || "(empty)";
|
||||
}
|
||||
|
||||
function isEnoentLike(message: string): boolean {
|
||||
return message.includes("ENOENT") || message.includes("not found");
|
||||
}
|
||||
|
||||
export type AuggieCliVersionCheck = { ok: boolean; version?: string; error?: string };
|
||||
|
||||
/**
|
||||
* Spawn `auggie --version` to confirm the local CLI is installed and runnable.
|
||||
* Used by the provider "Test Connection" flow — auggie has no API key, so this
|
||||
* is the only real signal we can give the operator before their first chat call.
|
||||
*/
|
||||
export function checkAuggieCliVersion(timeoutMs = 5000): Promise<AuggieCliVersionCheck> {
|
||||
const bin = resolveAuggieBin();
|
||||
return new Promise((resolve) => {
|
||||
let settled = false;
|
||||
const settle = (result: AuggieCliVersionCheck) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
let child: ReturnType<typeof spawn>;
|
||||
try {
|
||||
// No `shell` option — fixed argv, no cmd.exe interpretation.
|
||||
child = spawn(bin, ["--version"], {
|
||||
env: process.env,
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
settle({ ok: false, error: isEnoentLike(message) ? cliNotFoundMessage(bin) : message });
|
||||
return;
|
||||
}
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
if (!child.killed) child.kill("SIGKILL");
|
||||
settle({ ok: false, error: "Auggie CLI version check timed out" });
|
||||
}, timeoutMs);
|
||||
|
||||
let stdout = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString("utf8");
|
||||
});
|
||||
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||
clearTimeout(timer);
|
||||
const message = err?.message || String(err);
|
||||
settle({ ok: false, error: isEnoentLike(message) ? cliNotFoundMessage(bin) : message });
|
||||
});
|
||||
child.on("close", (code) => {
|
||||
clearTimeout(timer);
|
||||
if (code === 0 && stdout.trim()) {
|
||||
settle({ ok: true, version: stdout.trim().slice(0, 200) });
|
||||
} else {
|
||||
settle({ ok: false, error: `Auggie CLI exited with code ${code}` });
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function cliNotFoundMessage(bin: string): string {
|
||||
return sanitizeErrorMessage(
|
||||
`Auggie CLI not found: ${bin}. Install it and run "auggie login", or set AUGGIE_BIN to an absolute path.`
|
||||
);
|
||||
}
|
||||
|
||||
// ─── AuggieExecutor ─────────────────────────────────────────────────────────
|
||||
|
||||
export class AuggieExecutor extends BaseExecutor {
|
||||
constructor() {
|
||||
super("auggie", { id: "auggie", baseUrl: "" });
|
||||
}
|
||||
|
||||
buildUrl(): string {
|
||||
return AUGGIE_URL;
|
||||
}
|
||||
|
||||
buildHeaders(): Record<string, string> {
|
||||
return {};
|
||||
}
|
||||
|
||||
transformRequest(): unknown {
|
||||
return null;
|
||||
}
|
||||
|
||||
/** No-op — auggie has no OmniRoute-managed credentials to refresh. */
|
||||
async refreshCredentials(
|
||||
_credentials: ProviderCredentials
|
||||
): Promise<Partial<ProviderCredentials> | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
async execute({
|
||||
model,
|
||||
body,
|
||||
stream,
|
||||
signal,
|
||||
log,
|
||||
}: ExecuteInput): Promise<{
|
||||
response: Response;
|
||||
url: string;
|
||||
headers: Record<string, string>;
|
||||
transformedBody: unknown;
|
||||
}> {
|
||||
const b = (body ?? {}) as Record<string, unknown>;
|
||||
const messages: OpenAIMsg[] = Array.isArray(b.messages) ? (b.messages as OpenAIMsg[]) : [];
|
||||
const promptText = buildAuggiePrompt(messages);
|
||||
const auggieBin = resolveAuggieBin();
|
||||
const wantsStream = stream !== false;
|
||||
|
||||
// Argument-injection defense: never forward an unvalidated model into the argv.
|
||||
const modelResolution = resolveAuggieModel(model);
|
||||
if (!modelResolution.ok) {
|
||||
const response = wantsStream
|
||||
? buildAuggieSseError(modelResolution.error)
|
||||
: errorResponse(400, modelResolution.error);
|
||||
return { response, url: AUGGIE_URL, headers: {}, transformedBody: { error: true } };
|
||||
}
|
||||
const safeModel = modelResolution.model;
|
||||
|
||||
log?.info?.(
|
||||
"AUGGIE",
|
||||
`auggie --print → model=${safeModel}, bin=${auggieBin}, stream=${wantsStream}`
|
||||
);
|
||||
|
||||
const response = wantsStream
|
||||
? this.runStreaming(auggieBin, safeModel, promptText, signal, log)
|
||||
: await this.runNonStreaming(auggieBin, safeModel, promptText, signal, log);
|
||||
|
||||
return {
|
||||
response,
|
||||
url: AUGGIE_URL,
|
||||
headers: {},
|
||||
transformedBody: { model: safeModel, promptLength: promptText.length },
|
||||
};
|
||||
}
|
||||
|
||||
private spawnAuggie(auggieBin: string, model: string, promptText: string) {
|
||||
// No `shell` option: argv is passed directly to the OS loader, so no cmd.exe
|
||||
// metacharacter interpretation is possible even on Windows. `model` is already
|
||||
// allowlist-validated by resolveAuggieModel() before reaching here.
|
||||
const child = spawn(auggieBin, buildAuggieArgs(model), {
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
try {
|
||||
child.stdin.write(promptText);
|
||||
child.stdin.end();
|
||||
} catch {
|
||||
/* ignore write errors — 'error'/'close' handlers surface the failure */
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
private runStreaming(
|
||||
auggieBin: string,
|
||||
model: string,
|
||||
promptText: string,
|
||||
signal: AbortSignal | null | undefined,
|
||||
log: ExecuteInput["log"]
|
||||
): Response {
|
||||
const responseId = `chatcmpl-auggie-${Date.now()}`;
|
||||
const created = Math.floor(Date.now() / 1000);
|
||||
|
||||
const sseStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
const enc = new TextEncoder();
|
||||
const emit = (data: string) => controller.enqueue(enc.encode(data));
|
||||
let closed = false;
|
||||
let roleEmitted = false;
|
||||
let finished = false;
|
||||
|
||||
const finish = () => {
|
||||
if (finished) return;
|
||||
finished = true;
|
||||
if (!closed) {
|
||||
closed = true;
|
||||
try {
|
||||
controller.close();
|
||||
} catch {
|
||||
/* already closed */
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const emitDelta = (delta: string) => {
|
||||
if (!delta) return;
|
||||
if (!roleEmitted) {
|
||||
emit(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [
|
||||
{ index: 0, delta: { role: "assistant", content: "" }, finish_reason: null },
|
||||
],
|
||||
})}\n\n`
|
||||
);
|
||||
roleEmitted = true;
|
||||
}
|
||||
emit(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: { content: delta }, finish_reason: null }],
|
||||
})}\n\n`
|
||||
);
|
||||
};
|
||||
|
||||
const emitError = (message: string) => {
|
||||
emit(`data: ${JSON.stringify(buildErrorBody(502, message))}\n\n`);
|
||||
emit("data: [DONE]\n\n");
|
||||
finish();
|
||||
};
|
||||
|
||||
const emitStop = () => {
|
||||
emit(
|
||||
`data: ${JSON.stringify({
|
||||
id: responseId,
|
||||
object: "chat.completion.chunk",
|
||||
created,
|
||||
model,
|
||||
choices: [{ index: 0, delta: {}, finish_reason: "stop" }],
|
||||
})}\n\n`
|
||||
);
|
||||
emit("data: [DONE]\n\n");
|
||||
finish();
|
||||
};
|
||||
|
||||
let child: ReturnType<typeof spawn>;
|
||||
try {
|
||||
// No `shell` option — argv goes straight to the OS loader (no cmd.exe
|
||||
// interpretation). `model` is already allowlist-validated upstream.
|
||||
child = spawn(auggieBin, buildAuggieArgs(model), {
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"],
|
||||
});
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
emitError(isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
child.stdin.write(promptText);
|
||||
child.stdin.end();
|
||||
} catch {
|
||||
/* ignore — error/close handlers below surface failures */
|
||||
}
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
if (!child.killed) child.kill("SIGTERM");
|
||||
finish();
|
||||
});
|
||||
}
|
||||
|
||||
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||
const message = err?.message || String(err);
|
||||
emitError(isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message));
|
||||
});
|
||||
|
||||
let stderrTail = "";
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
emitDelta(chunk.toString("utf8"));
|
||||
});
|
||||
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderrTail = (stderrTail + chunk.toString("utf8")).slice(-2000);
|
||||
log?.debug?.("AUGGIE", `stderr: ${chunk.toString("utf8").slice(0, 200)}`);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (finished) return;
|
||||
if (code !== 0) {
|
||||
emitError(
|
||||
sanitizeErrorMessage(
|
||||
`Auggie CLI exited with code ${code}${stderrTail ? `: ${stderrTail}` : ""}`
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
emitStop();
|
||||
});
|
||||
},
|
||||
cancel() {
|
||||
// Stream cancelled by the consumer — nothing extra to clean up here;
|
||||
// the abort-signal listener above (if provided) handles process kill.
|
||||
},
|
||||
});
|
||||
|
||||
return new Response(sseStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private runNonStreaming(
|
||||
auggieBin: string,
|
||||
model: string,
|
||||
promptText: string,
|
||||
signal: AbortSignal | null | undefined,
|
||||
log: ExecuteInput["log"]
|
||||
): Promise<Response> {
|
||||
return new Promise((resolve) => {
|
||||
let child: ReturnType<typeof spawn>;
|
||||
try {
|
||||
child = this.spawnAuggie(auggieBin, model, promptText);
|
||||
} catch (err) {
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
resolve(
|
||||
buildAuggieErrorResponse(
|
||||
isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let stdout = "";
|
||||
let stderrTail = "";
|
||||
let settled = false;
|
||||
|
||||
const settle = (response: Response) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
if (signal) {
|
||||
signal.addEventListener("abort", () => {
|
||||
if (!child.killed) child.kill("SIGTERM");
|
||||
settle(buildAuggieErrorResponse(sanitizeErrorMessage("Auggie CLI request aborted")));
|
||||
});
|
||||
}
|
||||
|
||||
child.stdout?.on("data", (chunk: Buffer) => {
|
||||
stdout += chunk.toString("utf8");
|
||||
});
|
||||
child.stderr?.on("data", (chunk: Buffer) => {
|
||||
stderrTail = (stderrTail + chunk.toString("utf8")).slice(-2000);
|
||||
log?.debug?.("AUGGIE", `stderr: ${chunk.toString("utf8").slice(0, 200)}`);
|
||||
});
|
||||
|
||||
child.on("error", (err: NodeJS.ErrnoException) => {
|
||||
const message = err?.message || String(err);
|
||||
settle(
|
||||
buildAuggieErrorResponse(
|
||||
isEnoentLike(message) ? cliNotFoundMessage(auggieBin) : sanitizeErrorMessage(message)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
child.on("close", (code) => {
|
||||
if (code !== 0) {
|
||||
settle(
|
||||
buildAuggieErrorResponse(
|
||||
sanitizeErrorMessage(
|
||||
`Auggie CLI exited with code ${code}${stderrTail ? `: ${stderrTail}` : ""}`
|
||||
)
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
settle(buildChatCompletionResponse(model, promptText, stdout));
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildChatCompletionResponse(model: string, promptText: string, content: string): Response {
|
||||
const trimmed = content.trim();
|
||||
const body = {
|
||||
id: `chatcmpl-auggie-${Date.now()}`,
|
||||
object: "chat.completion",
|
||||
created: Math.floor(Date.now() / 1000),
|
||||
model,
|
||||
choices: [
|
||||
{
|
||||
index: 0,
|
||||
message: { role: "assistant", content: trimmed },
|
||||
finish_reason: "stop",
|
||||
},
|
||||
],
|
||||
usage: {
|
||||
prompt_tokens: Math.ceil(promptText.length / 4),
|
||||
completion_tokens: Math.ceil(trimmed.length / 4),
|
||||
total_tokens: Math.ceil((promptText.length + trimmed.length) / 4),
|
||||
estimated: true,
|
||||
},
|
||||
};
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
|
||||
function buildAuggieErrorResponse(message: string): Response {
|
||||
return errorResponse(502, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a one-shot SSE error Response (single sanitized error event + [DONE]).
|
||||
* Used for pre-spawn rejections on the streaming path (e.g. an invalid model)
|
||||
* where no subprocess is ever started.
|
||||
*/
|
||||
function buildAuggieSseError(message: string): Response {
|
||||
const enc = new TextEncoder();
|
||||
const sseStream = new ReadableStream<Uint8Array>({
|
||||
start(controller) {
|
||||
controller.enqueue(enc.encode(`data: ${JSON.stringify(buildErrorBody(400, message))}\n\n`));
|
||||
controller.enqueue(enc.encode("data: [DONE]\n\n"));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
return new Response(sseStream, {
|
||||
status: 200,
|
||||
headers: {
|
||||
"Content-Type": "text/event-stream",
|
||||
"Cache-Control": "no-cache",
|
||||
Connection: "keep-alive",
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -66,6 +66,26 @@ import {
|
||||
stripProxyToolPrefix,
|
||||
} from "./claudeIdentity.ts";
|
||||
import { withForcedResponsesUpstream } from "./forceResponsesUpstream.ts";
|
||||
import {
|
||||
mergeUpstreamExtraHeaders,
|
||||
setUserAgentHeader,
|
||||
applyConfiguredUserAgent,
|
||||
stripStainlessHeadersForOpenAICompat,
|
||||
} from "./base/headers.ts";
|
||||
// Header helpers extracted to a pure leaf; re-exported for external importers
|
||||
// (executors + tests) that import them from "./base.ts".
|
||||
export {
|
||||
mergeUpstreamExtraHeaders,
|
||||
getCustomUserAgent,
|
||||
setUserAgentHeader,
|
||||
applyConfiguredUserAgent,
|
||||
isOpenAICompatibleEndpoint,
|
||||
stripStainlessHeadersForOpenAICompat,
|
||||
} from "./base/headers.ts";
|
||||
import { sanitizeReasoningEffortForProvider } from "./base/reasoningEffort.ts";
|
||||
// Reasoning-effort sanitation extracted to a pure leaf; re-exported for external
|
||||
// importers (mimoThinking service + tests) that import it from "./base.ts".
|
||||
export { sanitizeReasoningEffortForProvider } from "./base/reasoningEffort.ts";
|
||||
|
||||
/**
|
||||
* Sanitizes a custom API path to prevent path traversal attacks.
|
||||
@@ -155,95 +175,6 @@ export type CountTokensInput = {
|
||||
signal?: AbortSignal | null;
|
||||
};
|
||||
|
||||
/** Apply model-level extra upstream headers (e.g. Authentication, X-Custom-Auth). */
|
||||
export function mergeUpstreamExtraHeaders(
|
||||
headers: Record<string, string>,
|
||||
extra?: Record<string, string> | null
|
||||
): void {
|
||||
if (!extra) return;
|
||||
for (const [k, v] of Object.entries(extra)) {
|
||||
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
|
||||
if (k.toLowerCase() === "user-agent") {
|
||||
setUserAgentHeader(headers, v);
|
||||
continue;
|
||||
}
|
||||
headers[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomUserAgent(providerSpecificData?: JsonRecord | null): string | null {
|
||||
const customUserAgent =
|
||||
typeof providerSpecificData?.customUserAgent === "string"
|
||||
? providerSpecificData.customUserAgent.trim()
|
||||
: "";
|
||||
return customUserAgent || null;
|
||||
}
|
||||
|
||||
export function setUserAgentHeader(headers: Record<string, string>, userAgent: string): void {
|
||||
headers["User-Agent"] = userAgent;
|
||||
if ("user-agent" in headers) {
|
||||
headers["user-agent"] = userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyConfiguredUserAgent(
|
||||
headers: Record<string, string>,
|
||||
providerSpecificData?: JsonRecord | null
|
||||
): void {
|
||||
const customUserAgent = getCustomUserAgent(providerSpecificData);
|
||||
if (customUserAgent) {
|
||||
setUserAgentHeader(headers, customUserAgent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the outbound request targets an OpenAI-compatible endpoint
|
||||
* (a `openai-compatible-*` provider, or a Chat Completions / Responses URL).
|
||||
* Used to scope the X-Stainless strip narrowly so genuine SDK-spoofing paths
|
||||
* (e.g. Claude Code compat, which legitimately ADDS X-Stainless-*) are untouched.
|
||||
*/
|
||||
export function isOpenAICompatibleEndpoint(provider: string, url: string): boolean {
|
||||
if (provider?.startsWith?.("openai-compatible-")) return true;
|
||||
return url.includes("/v1/chat/completions") || url.includes("/v1/responses");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip OpenAI SDK (`X-Stainless-*`) metadata headers and normalize an SDK-derived
|
||||
* User-Agent for OpenAI-compatible passthrough requests. Some upstream gateways
|
||||
* 403 on these SDK-identifying headers. Only applied to OpenAI-compatible endpoints —
|
||||
* other providers (Claude/Claude Code compat) may legitimately send X-Stainless-*.
|
||||
*
|
||||
* Mutates `headers` in place and returns the list of stripped header keys (for logging).
|
||||
*/
|
||||
export function stripStainlessHeadersForOpenAICompat(
|
||||
headers: Record<string, string>,
|
||||
provider: string,
|
||||
url: string
|
||||
): string[] {
|
||||
if (!isOpenAICompatibleEndpoint(provider, url)) return [];
|
||||
|
||||
const strippedKeys: string[] = [];
|
||||
for (const key of Object.keys(headers)) {
|
||||
if (key.toLowerCase().startsWith("x-stainless-")) {
|
||||
delete headers[key];
|
||||
strippedKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize User-Agent: SDK-based clients send verbose product strings that some
|
||||
// upstreams block. Replace with a clean browser-like UA only when it looks SDK-derived.
|
||||
const ua = (headers["User-Agent"] || headers["user-agent"] || "").toLowerCase();
|
||||
if (
|
||||
ua.includes("openai") &&
|
||||
(ua.includes("node") || ua.includes("axios") || ua.includes("undici"))
|
||||
) {
|
||||
setUserAgentHeader(headers, "Mozilla/5.0 (compatible; OpenAI Compatible)");
|
||||
}
|
||||
|
||||
return strippedKeys;
|
||||
}
|
||||
|
||||
export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
|
||||
const controller = new AbortController();
|
||||
|
||||
@@ -272,161 +203,6 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
|
||||
return thinking?.type === "enabled" || thinking?.type === "adaptive";
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize reasoning_effort for providers that don't accept all values.
|
||||
*
|
||||
* The claude→openai translator may emit reasoning_effort=max/xhigh when the
|
||||
* client sends output_config.effort=max on a Claude-shape request. Combined with
|
||||
* runtime alias remapping (e.g. claude-opus-4-6 → mimo/mimo-v2.5-pro), this
|
||||
* routes xhigh to OpenAI-shape providers that don't accept the value:
|
||||
*
|
||||
* xiaomi-mimo : low|medium|high only — 400 literal_error on xhigh
|
||||
* mistral : devstral models reject reasoning_effort entirely
|
||||
* github : claude/haiku/oswe models reject reasoning_effort entirely
|
||||
*
|
||||
* Each rejection burns a combo fallback attempt before reaching a working
|
||||
* provider. Apply provider-aware sanitation here (after transformRequest, so
|
||||
* reintroductions by per-provider transforms are also caught) before fetch.
|
||||
* xhigh support is opt-out: pass through unchanged unless the registry marks
|
||||
* a model as unsupported. Literal max support is provider-specific and
|
||||
* intentionally separate: some upstreams accept max even when they do not
|
||||
* accept xhigh. For OpenAI-shape providers, max normalizes to xhigh by default
|
||||
* and falls back to high only for explicit xhigh opt-outs.
|
||||
*/
|
||||
const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
|
||||
// GitHub Copilot Claude routing is granular (upstream port: decolua/9router#791):
|
||||
// ✅ Pass through — Claude Opus 4.6, Claude Sonnet 4.6. Copilot routes both to
|
||||
// Anthropic's chat/completions surface, which honors reasoning_effort and
|
||||
// emits visible reasoning tokens (verified upstream: 3× token increase
|
||||
// between low/medium/high).
|
||||
// ❌ Strip — Claude Haiku 4.5 and Claude Opus 4.7 (rejected upstream by
|
||||
// Copilot's Claude backend), older Claude variants, all `haiku`-named
|
||||
// models, and the `oswe-*` family (Raptor) which still rejects
|
||||
// reasoning_effort.
|
||||
// Order matters: the opt-in check must run BEFORE the broad Claude/haiku/oswe strip.
|
||||
const GITHUB_REASONING_EFFORT_OPT_IN_PATTERN = /claude[-_.]?(?:opus|sonnet)[-_.]?4[-_.]6/i;
|
||||
const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i;
|
||||
|
||||
function supportsMaxEffortForProvider(provider: string, model: string): boolean {
|
||||
const isClaude =
|
||||
(provider === PROVIDER_CLAUDE || isClaudeCodeCompatible(provider)) &&
|
||||
supportsClaudeMaxEffort(model);
|
||||
// opencode-go proxies DeepSeek with the native DeepSeek API contract, which
|
||||
// accepts {high, max} literally. Without this opt-in, max would be
|
||||
// normalized to xhigh (the OmniRoute-internal top tier) and rejected by the
|
||||
// upstream. Scoped to opencode-go deliberately: OpenRouter's DeepSeek path
|
||||
// (pi#4055) is the documented inverse and expects xhigh, not max.
|
||||
// Ollama Cloud also accepts literal max (for example GLM 5.2 supports
|
||||
// low|medium|high|max|none) and rejects xhigh.
|
||||
const isOpencodeGoDeepSeek =
|
||||
provider === "opencode-go" && model.toLowerCase().includes("deepseek");
|
||||
const isOllamaCloud = provider === "ollama-cloud";
|
||||
return isClaude || isOpencodeGoDeepSeek || isOllamaCloud;
|
||||
}
|
||||
|
||||
export function sanitizeReasoningEffortForProvider(
|
||||
body: unknown,
|
||||
provider: string,
|
||||
model: string | undefined,
|
||||
log?: { info?: (tag: string, msg: string) => void } | null
|
||||
): unknown {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
||||
const b = body as Record<string, unknown>;
|
||||
const reasoning =
|
||||
b.reasoning && typeof b.reasoning === "object" && !Array.isArray(b.reasoning)
|
||||
? (b.reasoning as Record<string, unknown>)
|
||||
: null;
|
||||
const hasTopLevelReasoningEffort = Object.prototype.hasOwnProperty.call(b, "reasoning_effort");
|
||||
const effort = b.reasoning_effort ?? reasoning?.effort;
|
||||
if (effort === undefined) return body;
|
||||
const effortStr = typeof effort === "string" ? effort.toLowerCase() : "";
|
||||
const modelStr = model || "";
|
||||
|
||||
const githubOptIn =
|
||||
provider === "github" && GITHUB_REASONING_EFFORT_OPT_IN_PATTERN.test(modelStr);
|
||||
const rejecting =
|
||||
(provider === "mistral" && MISTRAL_NO_REASONING_EFFORT_PATTERN.test(modelStr)) ||
|
||||
(provider === "github" && !githubOptIn && GITHUB_NO_REASONING_EFFORT_PATTERN.test(modelStr));
|
||||
if (rejecting) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: removed unsupported reasoning_effort`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
delete next.reasoning_effort;
|
||||
if (reasoning) {
|
||||
const r = { ...reasoning };
|
||||
delete r.effort;
|
||||
if (Object.keys(r).length === 0) delete next.reasoning;
|
||||
else next.reasoning = r;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
// Native DeepSeek (api.deepseek.com) — V4 thinking mode accepts reasoning_effort
|
||||
// ONLY as {high, max} (its own top tier is literally "max"). OmniRoute's internal
|
||||
// scale is low|medium|high|xhigh where xhigh is the top, so map onto DeepSeek's
|
||||
// vocabulary: xhigh → max (top→top), low|medium → high (below the enum floor).
|
||||
// high/max pass through unchanged. Without this, the claude→openai translator's
|
||||
// xhigh (and max-normalized-to-xhigh below) reaches DeepSeek as an unknown value,
|
||||
// silently dropping the client's requested effort. This is the INVERSE of the
|
||||
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max (pi#4055).
|
||||
if (provider === "deepseek") {
|
||||
const mapped =
|
||||
effortStr === "xhigh" ? "max" : effortStr === "low" || effortStr === "medium" ? "high" : null;
|
||||
if (mapped && mapped !== effortStr) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`deepseek/${modelStr}: normalized reasoning_effort ${effortStr} → ${mapped}`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) next.reasoning_effort = mapped;
|
||||
if (reasoning) next.reasoning = { ...reasoning, effort: mapped };
|
||||
return next;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
const supportsXHigh = supportsXHighEffort(provider, modelStr);
|
||||
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh;
|
||||
const supportsXHighForMax = supportsXHigh;
|
||||
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);
|
||||
const shouldNormalizeMaxToXHigh = effortStr === "max" && !supportsMax && supportsXHighForMax;
|
||||
const shouldDowngradeMax = effortStr === "max" && !supportsMax && !supportsXHighForMax;
|
||||
|
||||
if (shouldNormalizeMaxToXHigh) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: normalized reasoning_effort max → xhigh`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) {
|
||||
next.reasoning_effort = "xhigh";
|
||||
}
|
||||
if (reasoning) {
|
||||
next.reasoning = { ...reasoning, effort: "xhigh" };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (shouldDowngradeXHigh || shouldDowngradeMax) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: downgraded reasoning_effort ${effortStr} → high`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) {
|
||||
next.reasoning_effort = "high";
|
||||
}
|
||||
if (reasoning) {
|
||||
next.reasoning = { ...reasoning, effort: "high" };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip the OmniRoute provider prefix from versioned built-in tool model
|
||||
* fields (e.g. `cc/claude-opus-4-8` → `claude-opus-4-8`). Versioned built-in
|
||||
|
||||
93
open-sse/executors/base/headers.ts
Normal file
93
open-sse/executors/base/headers.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
// Pure upstream header helpers (User-Agent, extra headers, OpenAI-compat stripping).
|
||||
// Extracted verbatim from base.ts. Module-private JsonRecord kept local to avoid a cycle.
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
/** Apply model-level extra upstream headers (e.g. Authentication, X-Custom-Auth). */
|
||||
export function mergeUpstreamExtraHeaders(
|
||||
headers: Record<string, string>,
|
||||
extra?: Record<string, string> | null
|
||||
): void {
|
||||
if (!extra) return;
|
||||
for (const [k, v] of Object.entries(extra)) {
|
||||
if (typeof k === "string" && k.length > 0 && typeof v === "string") {
|
||||
if (k.toLowerCase() === "user-agent") {
|
||||
setUserAgentHeader(headers, v);
|
||||
continue;
|
||||
}
|
||||
headers[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getCustomUserAgent(providerSpecificData?: JsonRecord | null): string | null {
|
||||
const customUserAgent =
|
||||
typeof providerSpecificData?.customUserAgent === "string"
|
||||
? providerSpecificData.customUserAgent.trim()
|
||||
: "";
|
||||
return customUserAgent || null;
|
||||
}
|
||||
|
||||
export function setUserAgentHeader(headers: Record<string, string>, userAgent: string): void {
|
||||
headers["User-Agent"] = userAgent;
|
||||
if ("user-agent" in headers) {
|
||||
headers["user-agent"] = userAgent;
|
||||
}
|
||||
}
|
||||
|
||||
export function applyConfiguredUserAgent(
|
||||
headers: Record<string, string>,
|
||||
providerSpecificData?: JsonRecord | null
|
||||
): void {
|
||||
const customUserAgent = getCustomUserAgent(providerSpecificData);
|
||||
if (customUserAgent) {
|
||||
setUserAgentHeader(headers, customUserAgent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when the outbound request targets an OpenAI-compatible endpoint
|
||||
* (a `openai-compatible-*` provider, or a Chat Completions / Responses URL).
|
||||
* Used to scope the X-Stainless strip narrowly so genuine SDK-spoofing paths
|
||||
* (e.g. Claude Code compat, which legitimately ADDS X-Stainless-*) are untouched.
|
||||
*/
|
||||
export function isOpenAICompatibleEndpoint(provider: string, url: string): boolean {
|
||||
if (provider?.startsWith?.("openai-compatible-")) return true;
|
||||
return url.includes("/v1/chat/completions") || url.includes("/v1/responses");
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip OpenAI SDK (`X-Stainless-*`) metadata headers and normalize an SDK-derived
|
||||
* User-Agent for OpenAI-compatible passthrough requests. Some upstream gateways
|
||||
* 403 on these SDK-identifying headers. Only applied to OpenAI-compatible endpoints —
|
||||
* other providers (Claude/Claude Code compat) may legitimately send X-Stainless-*.
|
||||
*
|
||||
* Mutates `headers` in place and returns the list of stripped header keys (for logging).
|
||||
*/
|
||||
export function stripStainlessHeadersForOpenAICompat(
|
||||
headers: Record<string, string>,
|
||||
provider: string,
|
||||
url: string
|
||||
): string[] {
|
||||
if (!isOpenAICompatibleEndpoint(provider, url)) return [];
|
||||
|
||||
const strippedKeys: string[] = [];
|
||||
for (const key of Object.keys(headers)) {
|
||||
if (key.toLowerCase().startsWith("x-stainless-")) {
|
||||
delete headers[key];
|
||||
strippedKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Normalize User-Agent: SDK-based clients send verbose product strings that some
|
||||
// upstreams block. Replace with a clean browser-like UA only when it looks SDK-derived.
|
||||
const ua = (headers["User-Agent"] || headers["user-agent"] || "").toLowerCase();
|
||||
if (
|
||||
ua.includes("openai") &&
|
||||
(ua.includes("node") || ua.includes("axios") || ua.includes("undici"))
|
||||
) {
|
||||
setUserAgentHeader(headers, "Mozilla/5.0 (compatible; OpenAI Compatible)");
|
||||
}
|
||||
|
||||
return strippedKeys;
|
||||
}
|
||||
160
open-sse/executors/base/reasoningEffort.ts
Normal file
160
open-sse/executors/base/reasoningEffort.ts
Normal file
@@ -0,0 +1,160 @@
|
||||
// Provider-aware reasoning_effort sanitation (xhigh/max normalization + reject strip).
|
||||
// Extracted verbatim from base.ts. Deps are config/services only (no host import → no cycle).
|
||||
import { PROVIDER_CLAUDE } from "../../services/systemTransforms.ts";
|
||||
import { isClaudeCodeCompatible } from "../../services/provider.ts";
|
||||
import { supportsClaudeMaxEffort, supportsXHighEffort } from "../../config/providerModels.ts";
|
||||
|
||||
/**
|
||||
* Sanitize reasoning_effort for providers that don't accept all values.
|
||||
*
|
||||
* The claude→openai translator may emit reasoning_effort=max/xhigh when the
|
||||
* client sends output_config.effort=max on a Claude-shape request. Combined with
|
||||
* runtime alias remapping (e.g. claude-opus-4-6 → mimo/mimo-v2.5-pro), this
|
||||
* routes xhigh to OpenAI-shape providers that don't accept the value:
|
||||
*
|
||||
* xiaomi-mimo : low|medium|high only — 400 literal_error on xhigh
|
||||
* mistral : devstral models reject reasoning_effort entirely
|
||||
* github : claude/haiku/oswe models reject reasoning_effort entirely
|
||||
*
|
||||
* Each rejection burns a combo fallback attempt before reaching a working
|
||||
* provider. Apply provider-aware sanitation here (after transformRequest, so
|
||||
* reintroductions by per-provider transforms are also caught) before fetch.
|
||||
* xhigh support is opt-out: pass through unchanged unless the registry marks
|
||||
* a model as unsupported. Literal max support is provider-specific and
|
||||
* intentionally separate: some upstreams accept max even when they do not
|
||||
* accept xhigh. For OpenAI-shape providers, max normalizes to xhigh by default
|
||||
* and falls back to high only for explicit xhigh opt-outs.
|
||||
*/
|
||||
export const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i;
|
||||
// GitHub Copilot Claude routing is granular (upstream port: decolua/9router#791):
|
||||
// ✅ Pass through — Claude Opus 4.6, Claude Sonnet 4.6. Copilot routes both to
|
||||
// Anthropic's chat/completions surface, which honors reasoning_effort and
|
||||
// emits visible reasoning tokens (verified upstream: 3× token increase
|
||||
// between low/medium/high).
|
||||
// ❌ Strip — Claude Haiku 4.5 and Claude Opus 4.7 (rejected upstream by
|
||||
// Copilot's Claude backend), older Claude variants, all `haiku`-named
|
||||
// models, and the `oswe-*` family (Raptor) which still rejects
|
||||
// reasoning_effort.
|
||||
// Order matters: the opt-in check must run BEFORE the broad Claude/haiku/oswe strip.
|
||||
export const GITHUB_REASONING_EFFORT_OPT_IN_PATTERN = /claude[-_.]?(?:opus|sonnet)[-_.]?4[-_.]6/i;
|
||||
export const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i;
|
||||
|
||||
export function supportsMaxEffortForProvider(provider: string, model: string): boolean {
|
||||
const isClaude =
|
||||
(provider === PROVIDER_CLAUDE || isClaudeCodeCompatible(provider)) &&
|
||||
supportsClaudeMaxEffort(model);
|
||||
// opencode-go proxies DeepSeek with the native DeepSeek API contract, which
|
||||
// accepts {high, max} literally. Without this opt-in, max would be
|
||||
// normalized to xhigh (the OmniRoute-internal top tier) and rejected by the
|
||||
// upstream. Scoped to opencode-go deliberately: OpenRouter's DeepSeek path
|
||||
// (pi#4055) is the documented inverse and expects xhigh, not max.
|
||||
// Ollama Cloud also accepts literal max (for example GLM 5.2 supports
|
||||
// low|medium|high|max|none) and rejects xhigh.
|
||||
const isOpencodeGoDeepSeek =
|
||||
provider === "opencode-go" && model.toLowerCase().includes("deepseek");
|
||||
const isOllamaCloud = provider === "ollama-cloud";
|
||||
return isClaude || isOpencodeGoDeepSeek || isOllamaCloud;
|
||||
}
|
||||
|
||||
export function sanitizeReasoningEffortForProvider(
|
||||
body: unknown,
|
||||
provider: string,
|
||||
model: string | undefined,
|
||||
log?: { info?: (tag: string, msg: string) => void } | null
|
||||
): unknown {
|
||||
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
||||
const b = body as Record<string, unknown>;
|
||||
const reasoning =
|
||||
b.reasoning && typeof b.reasoning === "object" && !Array.isArray(b.reasoning)
|
||||
? (b.reasoning as Record<string, unknown>)
|
||||
: null;
|
||||
const hasTopLevelReasoningEffort = Object.prototype.hasOwnProperty.call(b, "reasoning_effort");
|
||||
const effort = b.reasoning_effort ?? reasoning?.effort;
|
||||
if (effort === undefined) return body;
|
||||
const effortStr = typeof effort === "string" ? effort.toLowerCase() : "";
|
||||
const modelStr = model || "";
|
||||
|
||||
const githubOptIn =
|
||||
provider === "github" && GITHUB_REASONING_EFFORT_OPT_IN_PATTERN.test(modelStr);
|
||||
const rejecting =
|
||||
(provider === "mistral" && MISTRAL_NO_REASONING_EFFORT_PATTERN.test(modelStr)) ||
|
||||
(provider === "github" && !githubOptIn && GITHUB_NO_REASONING_EFFORT_PATTERN.test(modelStr));
|
||||
if (rejecting) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: removed unsupported reasoning_effort`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
delete next.reasoning_effort;
|
||||
if (reasoning) {
|
||||
const r = { ...reasoning };
|
||||
delete r.effort;
|
||||
if (Object.keys(r).length === 0) delete next.reasoning;
|
||||
else next.reasoning = r;
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
// Native DeepSeek (api.deepseek.com) — V4 thinking mode accepts reasoning_effort
|
||||
// ONLY as {high, max} (its own top tier is literally "max"). OmniRoute's internal
|
||||
// scale is low|medium|high|xhigh where xhigh is the top, so map onto DeepSeek's
|
||||
// vocabulary: xhigh → max (top→top), low|medium → high (below the enum floor).
|
||||
// high/max pass through unchanged. Without this, the claude→openai translator's
|
||||
// xhigh (and max-normalized-to-xhigh below) reaches DeepSeek as an unknown value,
|
||||
// silently dropping the client's requested effort. This is the INVERSE of the
|
||||
// OpenRouter-DeepSeek path, whose normalized API expects xhigh, not max (pi#4055).
|
||||
if (provider === "deepseek") {
|
||||
const mapped =
|
||||
effortStr === "xhigh" ? "max" : effortStr === "low" || effortStr === "medium" ? "high" : null;
|
||||
if (mapped && mapped !== effortStr) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`deepseek/${modelStr}: normalized reasoning_effort ${effortStr} → ${mapped}`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) next.reasoning_effort = mapped;
|
||||
if (reasoning) next.reasoning = { ...reasoning, effort: mapped };
|
||||
return next;
|
||||
}
|
||||
return body;
|
||||
}
|
||||
|
||||
const supportsXHigh = supportsXHighEffort(provider, modelStr);
|
||||
const shouldDowngradeXHigh = effortStr === "xhigh" && !supportsXHigh;
|
||||
const supportsXHighForMax = supportsXHigh;
|
||||
const supportsMax = supportsMaxEffortForProvider(provider, modelStr);
|
||||
const shouldNormalizeMaxToXHigh = effortStr === "max" && !supportsMax && supportsXHighForMax;
|
||||
const shouldDowngradeMax = effortStr === "max" && !supportsMax && !supportsXHighForMax;
|
||||
|
||||
if (shouldNormalizeMaxToXHigh) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: normalized reasoning_effort max → xhigh`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) {
|
||||
next.reasoning_effort = "xhigh";
|
||||
}
|
||||
if (reasoning) {
|
||||
next.reasoning = { ...reasoning, effort: "xhigh" };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
if (shouldDowngradeXHigh || shouldDowngradeMax) {
|
||||
log?.info?.(
|
||||
"REASONING_SANITIZE",
|
||||
`${provider}/${modelStr}: downgraded reasoning_effort ${effortStr} → high`
|
||||
);
|
||||
const next: Record<string, unknown> = { ...b };
|
||||
if (hasTopLevelReasoningEffort) {
|
||||
next.reasoning_effort = "high";
|
||||
}
|
||||
if (reasoning) {
|
||||
next.reasoning = { ...reasoning, effort: "high" };
|
||||
}
|
||||
return next;
|
||||
}
|
||||
|
||||
return body;
|
||||
}
|
||||
@@ -32,6 +32,7 @@ import {
|
||||
__resetChatGptImageCacheForTesting,
|
||||
type ChatGptImageConversationContext,
|
||||
} from "../services/chatgptImageCache.ts";
|
||||
import { isThinkingCapableModel, resolveChatGptModel } from "./chatgpt-web/models.ts";
|
||||
|
||||
// ─── Constants ──────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -84,52 +85,6 @@ function deviceIdFor(cookie: string): string {
|
||||
// ChatGPT's backend routes use dash-form slugs (e.g. "gpt-5-5-pro"). The slug
|
||||
// catalog comes from /backend-api/models on a logged-in account;
|
||||
// "gpt-5-4-t-mini" is ChatGPT's abbreviated slug for "GPT-5.4 Thinking Mini".
|
||||
const MODEL_MAP: Record<string, string> = {
|
||||
// ChatGPT backend slugs are also accepted directly for power users / tests.
|
||||
"gpt-5-5-pro": "gpt-5-5-pro",
|
||||
"gpt-5-5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5-5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5-5": "gpt-5-5",
|
||||
"gpt-5-4-pro": "gpt-5-4-pro",
|
||||
"gpt-5-4-thinking": "gpt-5-4-thinking",
|
||||
"gpt-5-4-t-mini": "gpt-5-4-t-mini",
|
||||
"gpt-5-3": "gpt-5-3",
|
||||
"gpt-5-3-mini": "gpt-5-3-mini",
|
||||
|
||||
// Public OmniRoute dot-form ids exposed by the provider catalog.
|
||||
"gpt-5.5-pro": "gpt-5-5-pro",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5.5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5.5": "gpt-5-5",
|
||||
"gpt-5.4-pro": "gpt-5-4-pro",
|
||||
"gpt-5.4-thinking": "gpt-5-4-thinking",
|
||||
"gpt-5.4-thinking-mini": "gpt-5-4-t-mini",
|
||||
"gpt-5.3-instant": "gpt-5-3-instant",
|
||||
"gpt-5.3": "gpt-5-3",
|
||||
"gpt-5.3-mini": "gpt-5-3-mini",
|
||||
o3: "o3",
|
||||
};
|
||||
|
||||
const MODEL_FORCED_EFFORT: Record<string, "standard" | "extended"> = {
|
||||
"gpt-5-5-pro": "standard",
|
||||
"gpt-5-5-pro-extended": "extended",
|
||||
"gpt-5.5-pro": "standard",
|
||||
"gpt-5.5-pro-extended": "extended",
|
||||
};
|
||||
|
||||
/** Set of chatgpt.com slugs that the user_last_used_model_config endpoint
|
||||
* accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a
|
||||
* new thinking entry there automatically extends this set. Includes the
|
||||
* abbreviated slug `gpt-5-4-t-mini` (no literal "thinking" substring) — the
|
||||
* reason this set exists at all rather than a substring match.
|
||||
*
|
||||
* Derived from MODEL_MAP keys (always dot-form) that contain "thinking" or
|
||||
* are the `o3` reasoning model; the values are the chatgpt.com-side slugs. */
|
||||
const THINKING_CAPABLE_SLUGS: ReadonlySet<string> = new Set(
|
||||
Object.entries(MODEL_MAP)
|
||||
.filter(([k]) => k.includes("thinking") || k === "o3")
|
||||
.map(([, v]) => v)
|
||||
);
|
||||
|
||||
// ─── Browser-like default headers ──────────────────────────────────────────
|
||||
|
||||
@@ -472,90 +427,6 @@ const thinkingEffortCache = new Map<string, number>();
|
||||
const THINKING_EFFORT_TTL_MS = 5 * 60 * 1000;
|
||||
const THINKING_EFFORT_CACHE_MAX = 400;
|
||||
|
||||
/** chatgpt.com only exposes the thinking-effort toggle on dedicated thinking
|
||||
* models and the o-series. PATCHing for a non-thinking surface is a no-op
|
||||
* (the server accepts it but the routing-time read picks the wrong knob).
|
||||
*
|
||||
* Three branches because the input can arrive in three shapes:
|
||||
* 1. OmniRoute dot-form id (`gpt-5.4-thinking-mini`) — every thinking
|
||||
* variant carries the literal "thinking" substring here.
|
||||
* 2. Resolved chatgpt.com slug containing "thinking" (`gpt-5-5-thinking`).
|
||||
* 3. Resolved chatgpt.com slug that drops the substring under abbreviation
|
||||
* (`gpt-5-4-t-mini`). Looked up via THINKING_CAPABLE_SLUGS, which is
|
||||
* derived from MODEL_MAP itself so adding a new abbreviated thinking
|
||||
* mapping automatically extends the check.
|
||||
*
|
||||
* Branch 3 also catches the case where a caller passes the chatgpt.com slug
|
||||
* directly as the `model` field (no MODEL_MAP translation needed), which
|
||||
* would otherwise silently bypass the PATCH. */
|
||||
function isThinkingCapableModel(modelId: string, slug: string): boolean {
|
||||
return (
|
||||
modelId.includes("thinking") ||
|
||||
modelId === "o3" ||
|
||||
slug.includes("thinking") ||
|
||||
THINKING_CAPABLE_SLUGS.has(slug) ||
|
||||
THINKING_CAPABLE_SLUGS.has(modelId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Map either a chatgpt.com-native value (`standard`/`extended`) or the
|
||||
* OpenAI Chat Completions `reasoning_effort` field to the value the
|
||||
* `user_last_used_model_config` endpoint expects.
|
||||
*
|
||||
* minimal | low | medium | standard → standard
|
||||
* high | xhigh | extended → extended
|
||||
*
|
||||
* `medium` collapses to `standard` because chatgpt.com only has two levels —
|
||||
* there is no separate medium tier on the web product. Returns null for
|
||||
* absent/unknown inputs. */
|
||||
function normalizeThinkingEffort(input: unknown): "standard" | "extended" | null {
|
||||
if (typeof input !== "string") return null;
|
||||
const v = input.trim().toLowerCase();
|
||||
if (v === "extended" || v === "high" || v === "xhigh") return "extended";
|
||||
if (v === "standard" || v === "low" || v === "medium" || v === "minimal") {
|
||||
return "standard";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolve the requested effort for this turn.
|
||||
* Order: `providerSpecificData.thinkingEffort` (raw override, takes
|
||||
* `standard`/`extended` directly) > `body.reasoning_effort` (top-level OpenAI
|
||||
* Chat Completions field) > `body.reasoning.effort` (Responses-API nesting).
|
||||
* Returns null when the caller did not request one. */
|
||||
function resolveThinkingEffort(
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
): "standard" | "extended" | null {
|
||||
if (providerSpecificData && providerSpecificData.thinkingEffort !== undefined) {
|
||||
return normalizeThinkingEffort(providerSpecificData.thinkingEffort);
|
||||
}
|
||||
const b = (body as Record<string, unknown> | null) ?? null;
|
||||
if (!b) return null;
|
||||
const top = normalizeThinkingEffort(b.reasoning_effort);
|
||||
if (top) return top;
|
||||
const nested = (b.reasoning as Record<string, unknown> | undefined)?.effort;
|
||||
return normalizeThinkingEffort(nested);
|
||||
}
|
||||
|
||||
interface ResolvedChatGptModel {
|
||||
slug: string;
|
||||
effort: "standard" | "extended" | null;
|
||||
isPro: boolean;
|
||||
}
|
||||
|
||||
function resolveChatGptModel(
|
||||
model: string,
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
): ResolvedChatGptModel {
|
||||
const slug = MODEL_MAP[model] ?? model;
|
||||
const forcedEffort = MODEL_FORCED_EFFORT[model] ?? null;
|
||||
const effort = forcedEffort ?? resolveThinkingEffort(body, providerSpecificData);
|
||||
const isPro = slug === "gpt-5-5-pro";
|
||||
return { slug, effort, isPro };
|
||||
}
|
||||
|
||||
function configuredProPollTimeoutMs(): number {
|
||||
const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS);
|
||||
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_TIMEOUT_MS;
|
||||
@@ -1399,8 +1270,7 @@ async function* extractContent(
|
||||
// on a tool-role message (handled below).
|
||||
if (event.type === "server_ste_metadata") {
|
||||
const meta = (event as Record<string, unknown>).metadata as
|
||||
| Record<string, unknown>
|
||||
| undefined;
|
||||
Record<string, unknown> | undefined;
|
||||
if (meta && meta.turn_use_case === "image gen") {
|
||||
imageGenAsync = true;
|
||||
}
|
||||
@@ -2780,8 +2650,7 @@ export class ChatGptWebExecutor extends BaseExecutor {
|
||||
clientHeaders,
|
||||
}: ExecuteInput) {
|
||||
const messages = (body as Record<string, unknown> | null)?.messages as
|
||||
| Array<Record<string, unknown>>
|
||||
| undefined;
|
||||
Array<Record<string, unknown>> | undefined;
|
||||
if (!messages || !Array.isArray(messages) || messages.length === 0) {
|
||||
return {
|
||||
response: errorResponse(400, "Missing or empty messages array"),
|
||||
|
||||
133
open-sse/executors/chatgpt-web/models.ts
Normal file
133
open-sse/executors/chatgpt-web/models.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
// Pure model-mapping / thinking-effort resolution for the ChatGPT-web executor.
|
||||
// Extracted verbatim from chatgpt-web.ts (static maps + pure resolvers, no state).
|
||||
|
||||
export const MODEL_MAP: Record<string, string> = {
|
||||
// ChatGPT backend slugs are also accepted directly for power users / tests.
|
||||
"gpt-5-5-pro": "gpt-5-5-pro",
|
||||
"gpt-5-5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5-5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5-5": "gpt-5-5",
|
||||
"gpt-5-4-pro": "gpt-5-4-pro",
|
||||
"gpt-5-4-thinking": "gpt-5-4-thinking",
|
||||
"gpt-5-4-t-mini": "gpt-5-4-t-mini",
|
||||
"gpt-5-3": "gpt-5-3",
|
||||
"gpt-5-3-mini": "gpt-5-3-mini",
|
||||
|
||||
// Public OmniRoute dot-form ids exposed by the provider catalog.
|
||||
"gpt-5.5-pro": "gpt-5-5-pro",
|
||||
"gpt-5.5-pro-extended": "gpt-5-5-pro",
|
||||
"gpt-5.5-thinking": "gpt-5-5-thinking",
|
||||
"gpt-5.5": "gpt-5-5",
|
||||
"gpt-5.4-pro": "gpt-5-4-pro",
|
||||
"gpt-5.4-thinking": "gpt-5-4-thinking",
|
||||
"gpt-5.4-thinking-mini": "gpt-5-4-t-mini",
|
||||
"gpt-5.3-instant": "gpt-5-3-instant",
|
||||
"gpt-5.3": "gpt-5-3",
|
||||
"gpt-5.3-mini": "gpt-5-3-mini",
|
||||
o3: "o3",
|
||||
};
|
||||
|
||||
export const MODEL_FORCED_EFFORT: Record<string, "standard" | "extended"> = {
|
||||
"gpt-5-5-pro": "standard",
|
||||
"gpt-5-5-pro-extended": "extended",
|
||||
"gpt-5.5-pro": "standard",
|
||||
"gpt-5.5-pro-extended": "extended",
|
||||
};
|
||||
|
||||
/** Set of chatgpt.com slugs that the user_last_used_model_config endpoint
|
||||
* accepts a `thinking_effort` value for, derived from MODEL_MAP so adding a
|
||||
* new thinking entry there automatically extends this set. Includes the
|
||||
* abbreviated slug `gpt-5-4-t-mini` (no literal "thinking" substring) — the
|
||||
* reason this set exists at all rather than a substring match.
|
||||
*
|
||||
* Derived from MODEL_MAP keys (always dot-form) that contain "thinking" or
|
||||
* are the `o3` reasoning model; the values are the chatgpt.com-side slugs. */
|
||||
export const THINKING_CAPABLE_SLUGS: ReadonlySet<string> = new Set(
|
||||
Object.entries(MODEL_MAP)
|
||||
.filter(([k]) => k.includes("thinking") || k === "o3")
|
||||
.map(([, v]) => v)
|
||||
);
|
||||
|
||||
/** chatgpt.com only exposes the thinking-effort toggle on dedicated thinking
|
||||
* models and the o-series. PATCHing for a non-thinking surface is a no-op
|
||||
* (the server accepts it but the routing-time read picks the wrong knob).
|
||||
*
|
||||
* Three branches because the input can arrive in three shapes:
|
||||
* 1. OmniRoute dot-form id (`gpt-5.4-thinking-mini`) — every thinking
|
||||
* variant carries the literal "thinking" substring here.
|
||||
* 2. Resolved chatgpt.com slug containing "thinking" (`gpt-5-5-thinking`).
|
||||
* 3. Resolved chatgpt.com slug that drops the substring under abbreviation
|
||||
* (`gpt-5-4-t-mini`). Looked up via THINKING_CAPABLE_SLUGS, which is
|
||||
* derived from MODEL_MAP itself so adding a new abbreviated thinking
|
||||
* mapping automatically extends the check.
|
||||
*
|
||||
* Branch 3 also catches the case where a caller passes the chatgpt.com slug
|
||||
* directly as the `model` field (no MODEL_MAP translation needed), which
|
||||
* would otherwise silently bypass the PATCH. */
|
||||
export function isThinkingCapableModel(modelId: string, slug: string): boolean {
|
||||
return (
|
||||
modelId.includes("thinking") ||
|
||||
modelId === "o3" ||
|
||||
slug.includes("thinking") ||
|
||||
THINKING_CAPABLE_SLUGS.has(slug) ||
|
||||
THINKING_CAPABLE_SLUGS.has(modelId)
|
||||
);
|
||||
}
|
||||
|
||||
/** Map either a chatgpt.com-native value (`standard`/`extended`) or the
|
||||
* OpenAI Chat Completions `reasoning_effort` field to the value the
|
||||
* `user_last_used_model_config` endpoint expects.
|
||||
*
|
||||
* minimal | low | medium | standard → standard
|
||||
* high | xhigh | extended → extended
|
||||
*
|
||||
* `medium` collapses to `standard` because chatgpt.com only has two levels —
|
||||
* there is no separate medium tier on the web product. Returns null for
|
||||
* absent/unknown inputs. */
|
||||
export function normalizeThinkingEffort(input: unknown): "standard" | "extended" | null {
|
||||
if (typeof input !== "string") return null;
|
||||
const v = input.trim().toLowerCase();
|
||||
if (v === "extended" || v === "high" || v === "xhigh") return "extended";
|
||||
if (v === "standard" || v === "low" || v === "medium" || v === "minimal") {
|
||||
return "standard";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Resolve the requested effort for this turn.
|
||||
* Order: `providerSpecificData.thinkingEffort` (raw override, takes
|
||||
* `standard`/`extended` directly) > `body.reasoning_effort` (top-level OpenAI
|
||||
* Chat Completions field) > `body.reasoning.effort` (Responses-API nesting).
|
||||
* Returns null when the caller did not request one. */
|
||||
export function resolveThinkingEffort(
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
): "standard" | "extended" | null {
|
||||
if (providerSpecificData && providerSpecificData.thinkingEffort !== undefined) {
|
||||
return normalizeThinkingEffort(providerSpecificData.thinkingEffort);
|
||||
}
|
||||
const b = (body as Record<string, unknown> | null) ?? null;
|
||||
if (!b) return null;
|
||||
const top = normalizeThinkingEffort(b.reasoning_effort);
|
||||
if (top) return top;
|
||||
const nested = (b.reasoning as Record<string, unknown> | undefined)?.effort;
|
||||
return normalizeThinkingEffort(nested);
|
||||
}
|
||||
|
||||
export interface ResolvedChatGptModel {
|
||||
slug: string;
|
||||
effort: "standard" | "extended" | null;
|
||||
isPro: boolean;
|
||||
}
|
||||
|
||||
export function resolveChatGptModel(
|
||||
model: string,
|
||||
body: unknown,
|
||||
providerSpecificData: Record<string, unknown> | undefined
|
||||
): ResolvedChatGptModel {
|
||||
const slug = MODEL_MAP[model] ?? model;
|
||||
const forcedEffort = MODEL_FORCED_EFFORT[model] ?? null;
|
||||
const effort = forcedEffort ?? resolveThinkingEffort(body, providerSpecificData);
|
||||
const isPro = slug === "gpt-5-5-pro";
|
||||
return { slug, effort, isPro };
|
||||
}
|
||||
@@ -1,13 +1,16 @@
|
||||
// Tool-call emulation helpers for the ChatGPT Web executor (#5240).
|
||||
// Tool-call emulation helpers for web-cookie executors (#5240, #5927).
|
||||
//
|
||||
// chatgpt.com has no native function calling. When the OpenAI request carries
|
||||
// `tools`, the prompt-side shim (`prepareToolMessages` in
|
||||
// ../translator/webTools.ts) injects a `<tool>` contract; on the response side
|
||||
// we parse `<tool>{...}</tool>` blocks back into OpenAI `tool_calls` —
|
||||
// mirroring the sibling web-session executors (qwen-web, perplexity-web, ...).
|
||||
// Web-cookie providers (chatgpt-web, perplexity-web, ...) have no native
|
||||
// function calling. When the OpenAI request carries `tools`, the prompt-side
|
||||
// shim (`prepareToolMessages` in ../translator/webTools.ts) injects a `<tool>`
|
||||
// contract; on the response side we parse `<tool>{...}</tool>` blocks back
|
||||
// into OpenAI `tool_calls`.
|
||||
//
|
||||
// The whole tool-mode orchestration lives here so the (frozen) chatgpt-web.ts
|
||||
// only gains an import + a single delegating call.
|
||||
// The whole tool-mode orchestration lives here — provider-agnostic — so each
|
||||
// (frozen) executor only gains an import + a single delegating call. Despite
|
||||
// the filename (kept for git-blame continuity from #5240, the first caller),
|
||||
// this module is shared: `buildToolModeResponse()` accepts an `idSeed` so
|
||||
// every provider gets its own `tool_calls[].id` prefix.
|
||||
|
||||
import { buildToolAwareResult } from "../translator/webTools.ts";
|
||||
|
||||
@@ -28,7 +31,8 @@ function sseChunk(data: unknown): string {
|
||||
*/
|
||||
async function applyToolCallsToJsonResponse(
|
||||
response: Response,
|
||||
requestedTools: unknown
|
||||
requestedTools: unknown,
|
||||
idSeed: string
|
||||
): Promise<Response> {
|
||||
const bodyText = await response.text();
|
||||
try {
|
||||
@@ -37,7 +41,7 @@ async function applyToolCallsToJsonResponse(
|
||||
const { content, toolCalls, finishReason } = buildToolAwareResult(
|
||||
rawContent,
|
||||
requestedTools,
|
||||
"cgpt"
|
||||
idSeed
|
||||
);
|
||||
if (toolCalls) {
|
||||
json.choices[0].message = { role: "assistant", content: null, tool_calls: toolCalls };
|
||||
@@ -107,9 +111,13 @@ export async function buildToolModeResponse(
|
||||
bufferedJson: Response,
|
||||
requestedTools: unknown,
|
||||
stream: boolean,
|
||||
meta: { cid: string; created: number; model: string }
|
||||
meta: { cid: string; created: number; model: string; idSeed?: string }
|
||||
): Promise<Response> {
|
||||
const jsonResponse = await applyToolCallsToJsonResponse(bufferedJson, requestedTools);
|
||||
const jsonResponse = await applyToolCallsToJsonResponse(
|
||||
bufferedJson,
|
||||
requestedTools,
|
||||
meta.idSeed ?? "cgpt"
|
||||
);
|
||||
if (!stream) return jsonResponse;
|
||||
const completion = await jsonResponse.json();
|
||||
return new Response(toolCompletionToSseStream(completion, meta.cid, meta.created, meta.model), {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user