fix(config): externalize ws for copilot-m365-web executor (#6098, closes #6062)

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:
Ankit
2026-07-04 01:06:35 +05:30
committed by GitHub
parent 5bc11f4d0e
commit e61b75f007
510 changed files with 31655 additions and 6991 deletions

View File

@@ -73,6 +73,11 @@ DISABLE_SQLITE_AUTO_BACKUP=false
# Default: 20128 # Default: 20128
PORT=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. # 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. # Used by: src/lib/runtime/ports.ts — overrides PORT for each service.
# API_PORT=20129 # 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. # Do not include /v1; if included accidentally it will be normalized away.
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128 # 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 # Max wait time for an async chatgpt-web image to land via the celsius
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during # WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
# upstream queue-deep windows ("Lots of people are creating images right now"). # 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_CONTINUE_BIN=cn
# CLI_QODER_BIN=qoder # CLI_QODER_BIN=qoder
# CLI_QWEN_BIN=qwen # CLI_QWEN_BIN=qwen
# CLI_AUGGIE_BIN=auggie
# AUGGIE_BIN=auggie
# Override the Hermes Agent home directory (where OmniRoute reads/writes the # Override the Hermes Agent home directory (where OmniRoute reads/writes the
# Hermes CLI config). Matches the env var the Hermes PowerShell installer sets # 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_PERSONAL_ACCESS_TOKEN=
# QODER_CLI_WORKSPACE= # QODER_CLI_WORKSPACE=
# OMNIROUTE_QODER_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) ── # ── Blackbox Web validated-token override (issue #2252) ──
# Used by: open-sse/executors/blackbox-web.ts. Blackbox `/api/chat` rejects # 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). # fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000 # 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) ── # ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ──
# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for # Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for
# the bogdanfinn/tls-client koffi binding and the JS-side grace window # 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_PORT=5544
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api # 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) ── # ── Local hostnames (Docker networking) ──
# Comma-separated additional hostnames treated as "local" for provider routing. # Comma-separated additional hostnames treated as "local" for provider routing.
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names. # 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. # healthy-result cache window under high concurrency.
# PROXY_HEALTH_UNHEALTHY_CACHE_TTL_MS=2000 # 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 # Allow OAuth and provider validation flows to bypass a pinned proxy and connect
# directly when proxy reachability pre-checks fail. Default: false. # directly when proxy reachability pre-checks fail. Default: false.
# Also configurable from Dashboard > Settings > Feature Flags. # Also configurable from Dashboard > Settings > Feature Flags.
@@ -1675,6 +1720,15 @@ APP_LOG_TO_FILE=true
# FREE_PROXY_IPLOCATE_ENABLED=false # FREE_PROXY_IPLOCATE_ENABLED=false
# FREE_PROXY_IPLOCATE_BASE_URL=https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols # 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 ── # ── Vercel Relay ──
# Used by: src/app/api/settings/proxy/vercel-deploy/route.ts # Used by: src/app/api/settings/proxy/vercel-deploy/route.ts
# Hides the "Deploy Relay" button when set to false. # 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 # duplicated). Falls back to TS path via X-Bifrost-Fallback header on
# timeout/failure. See bin/omniroute for the local-redis companion. # timeout/failure. See bin/omniroute for the local-redis companion.
# BIFROST_BASE_URL= # 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 # API key for the Bifrost gateway (sent as Authorization: Bearer ...). If
# unset, the route expects the request to carry a valid OmniRoute API key; # unset, the route expects the request to carry a valid OmniRoute API key;
# this key is for gateway-side auth only. # this key is for gateway-side auth only.

View File

@@ -624,7 +624,7 @@ jobs:
cache: npm cache: npm
- uses: ./.github/actions/npm-ci-retry - uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime - 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: test-vitest:
name: Vitest (MCP / autoCombo / UI components) name: Vitest (MCP / autoCombo / UI components)
@@ -676,7 +676,7 @@ jobs:
cache: npm cache: npm
- uses: ./.github/actions/npm-ci-retry - uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime - 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: node-26-compat-build:
name: Node 26 Compatibility Build name: Node 26 Compatibility Build
@@ -729,7 +729,7 @@ jobs:
cache: npm cache: npm
- uses: ./.github/actions/npm-ci-retry - uses: ./.github/actions/npm-ci-retry
- run: npm run check:node-runtime - 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: test-coverage-shard:
name: Coverage Shard (${{ matrix.shard }}/8) name: Coverage Shard (${{ matrix.shard }}/8)
@@ -770,7 +770,7 @@ jobs:
--exclude=tests/** \ --exclude=tests/** \
--exclude=**/*.test.* \ --exclude=**/*.test.* \
node --max-old-space-size=4096 --import tsx --import ./tests/_setup/isolateDataDir.ts --test --test-force-exit --test-concurrency=4 \ 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 - name: Upload raw shard coverage
if: always() if: always()
uses: actions/upload-artifact@v7 uses: actions/upload-artifact@v7

View File

@@ -145,4 +145,4 @@ jobs:
--import ./tests/_setup/isolateDataDir.ts --import ./tests/_setup/isolateDataDir.ts
--test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2 --test --test-force-exit --test-concurrency=4 --test-shard=${{ matrix.shard }}/2
tests/unit/*.test.ts 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"

View File

@@ -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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ 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) - **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 ### 🔧 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) - **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)

View File

@@ -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". 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`. 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. 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.)
--- ---

View File

@@ -56,7 +56,7 @@ export function buildProfileSettings(modelId, baseUrl, cfg) {
* behaviorally identical. Writes `<claudeHome>/profiles/<name>/settings.json` * behaviorally identical. Writes `<claudeHome>/profiles/<name>/settings.json`
* (directory-per-profile); never touches the active/default Claude config. * (directory-per-profile); never touches the active/default Claude config.
* @param {Array} models * @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}>}>} * @returns {Promise<{written:number, skipped:number, profiles:Array<{name:string, model:string, filePath:string}>}>}
*/ */
export async function syncClaudeProfilesFromModels(models, opts = {}) { export async function syncClaudeProfilesFromModels(models, opts = {}) {
@@ -64,6 +64,12 @@ export async function syncClaudeProfilesFromModels(models, opts = {}) {
const profilesRoot = join(claudeHome, "profiles"); const profilesRoot = join(claudeHome, "profiles");
const baseUrl = opts.baseUrl; const baseUrl = opts.baseUrl;
const dryRun = Boolean(opts.dryRun); 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; const onlyFilter = opts.only ? opts.only.split(",").map((s) => s.trim()) : null;
if (!dryRun && !existsSync(profilesRoot)) { if (!dryRun && !existsSync(profilesRoot)) {
@@ -96,8 +102,8 @@ export async function syncClaudeProfilesFromModels(models, opts = {}) {
const content = buildProfileSettings(id, baseUrl, cfg); const content = buildProfileSettings(id, baseUrl, cfg);
if (dryRun) { if (dryRun) {
console.log(`\n── [dry-run] ${filePath} ──`); log(`\n── [dry-run] ${filePath} ──`);
console.log(content); log(content);
} else { } else {
mkdirSync(dir, { recursive: true }); mkdirSync(dir, { recursive: true });
writeFileSync(filePath, content, "utf8"); writeFileSync(filePath, content, "utf8");

View File

@@ -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.", "_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_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_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.", "_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.",

View File

@@ -1,5 +1,6 @@
{ {
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.", "_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_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_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.", "_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.", "_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, "cap": 800,
"frozen": { "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).", "_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/config/providerRegistry.ts": 4731,
"open-sse/executors/antigravity.ts": 1813, "open-sse/executors/antigravity.ts": 1813,
@@ -182,7 +185,7 @@
"open-sse/utils/stream.ts": 2727, "open-sse/utils/stream.ts": 2727,
"src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385, "src/app/(dashboard)/dashboard/HomePageClient.tsx": 1385,
"src/app/(dashboard)/dashboard/analytics/ComboHealthTab.tsx": 1028, "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/media/MediaPageClient.tsx": 1105,
"src/app/(dashboard)/dashboard/cache/page.tsx": 845, "src/app/(dashboard)/dashboard/cache/page.tsx": 845,
"src/app/(dashboard)/dashboard/cli-code/components/CodexToolCard.tsx": 900, "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/CompressionSettingsTab.tsx": 974,
"src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898, "src/app/(dashboard)/dashboard/settings/components/MemorySkillsTab.tsx": 898,
"src/app/(dashboard)/dashboard/settings/components/PricingTab.tsx": 1012, "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/ResilienceTab.tsx": 1183,
"src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629, "src/app/(dashboard)/dashboard/settings/components/RoutingTab.tsx": 1629,
"src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924, "src/app/(dashboard)/dashboard/settings/components/SystemStorageTab.tsx": 1924,
@@ -240,14 +243,14 @@
"src/lib/usage/providerLimits.ts": 955, "src/lib/usage/providerLimits.ts": 955,
"src/lib/usage/usageHistory.ts": 988, "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.", "_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/RequestLoggerV2.tsx": 1553,
"src/shared/components/analytics/charts.tsx": 1558, "src/shared/components/analytics/charts.tsx": 1558,
"src/shared/constants/cliTools.ts": 875, "src/shared/constants/cliTools.ts": 875,
"src/shared/constants/pricing.ts": 1662, "src/shared/constants/pricing.ts": 1662,
"src/shared/constants/providers.ts": 3276, "src/shared/constants/providers.ts": 3276,
"src/shared/constants/sidebarVisibility.ts": 1198, "src/shared/constants/sidebarVisibility.ts": 1198,
"src/shared/services/cliRuntime.ts": 1090, "src/shared/services/cliRuntime.ts": 1100,
"src/shared/validation/schemas.ts": 2523, "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.", "_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, "src/sse/handlers/chat.ts": 1647,
@@ -261,7 +264,7 @@
"tests/unit/provider-validation-specialty.test.ts": 2843, "tests/unit/provider-validation-specialty.test.ts": 2843,
"open-sse/executors/huggingchat.ts": 813, "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.", "_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 "src/lib/tokenHealthCheck.ts": 830
}, },
"testCap": 800, "testCap": 800,
@@ -281,7 +284,7 @@
"tests/unit/db-core-init.test.ts": 877, "tests/unit/db-core-init.test.ts": 877,
"tests/unit/db-migration-runner.test.ts": 1491, "tests/unit/db-migration-runner.test.ts": 1491,
"tests/unit/db-settings-crud.test.ts": 941, "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-antigravity.test.ts": 942,
"tests/unit/executor-codex.test.ts": 1347, "tests/unit/executor-codex.test.ts": 1347,
"tests/unit/executor-default-base.test.ts": 1523, "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).", "_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/oauth-providers-config.test.ts": 873,
"tests/unit/perplexity-web.test.ts": 959, "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, "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.", "_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/reasoning-cache.test.ts": 980,
"tests/unit/route-edge-coverage.test.ts": 1234, "tests/unit/route-edge-coverage.test.ts": 1234,
"tests/unit/search-handler-extended.test.ts": 1124, "tests/unit/search-handler-extended.test.ts": 1124,
@@ -306,14 +309,15 @@
"tests/unit/token-refresh-service.test.ts": 1353, "tests/unit/token-refresh-service.test.ts": 1353,
"tests/unit/translator-friendly-test-bench.test.tsx": 848, "tests/unit/translator-friendly-test-bench.test.tsx": 848,
"tests/unit/translator-helper-branches.test.ts": 870, "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-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/translator-resp-gemini-to-openai.test.ts": 1234,
"tests/unit/usage-service-hardening.test.ts": 1633, "tests/unit/usage-service-hardening.test.ts": 1633,
"tests/unit/vscode-token-routes.test.ts": 1212, "tests/unit/vscode-token-routes.test.ts": 1212,
"tests/unit/combo-config.test.ts": 881, "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 "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.", "_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_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_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_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_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_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.", "_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_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_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_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_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_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.", "_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.",

View File

@@ -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.", "_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": { "metrics": {
"eslintWarnings": { "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.", "_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", "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.", "_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." "_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": { "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.", "_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", "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.", "_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.",

View File

@@ -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. - [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_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. - [openapi.yaml](openapi.yaml) — OpenAPI spec for the public API.
- [ENVIRONMENT.md](reference/ENVIRONMENT.md) — environment variables reference. - [ENVIRONMENT.md](reference/ENVIRONMENT.md) — environment variables reference.
- [FEATURE_FLAGS.md](reference/FEATURE_FLAGS.md) — feature flags and their defaults. - [FEATURE_FLAGS.md](reference/FEATURE_FLAGS.md) — feature flags and their defaults.

View File

@@ -1,7 +1,7 @@
--- ---
title: "Extending the Compression Pipeline" title: "Extending the Compression Pipeline"
version: 3.8.40 version: 3.8.44
lastUpdated: 2026-06-28 lastUpdated: 2026-07-02
--- ---
# Extending the Compression Pipeline # 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 ## Best Practices
### Engine Development ### Engine Development

View File

@@ -1,13 +1,13 @@
--- ---
title: "Embedded Services" title: "Embedded Services"
description: "Reference for 9Router and CLIProxyAPI" description: "Reference for 9Router, CLIProxyAPI, Mux, and Bifrost"
--- ---
# Embedded Services # Embedded Services
> **Version:** v3.8.4 > **Version:** v3.8.44
> **Last updated:** 2026-06-28 > **Last updated:** 2026-07-03
> **Audience:** Engineers adding, maintaining, or debugging embedded services (9Router, CLIProxyAPI). > **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 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 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? ### 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 | | 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}` | | **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 | | **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 installs them under `DATA_DIR/services/{name}/` (isolated from OmniRoute's own `package.json`)
- OmniRoute spawns and monitors them as child processes - 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) - All management routes (`/api/services/*`) are **LOCAL_ONLY** — accessible only from loopback (hard rule #17)
### Key decisions (from design plan) ### 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) | | Installation mechanism | `npm install {package}` via `execFile` (no shell interpolation) |
| Consumption mode | Provider registered as `9router/{sub}/{model}` in routing engine | | 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 | | 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 | | Auto-start | Toggle per service, default OFF |
--- ---
@@ -64,12 +66,13 @@ Both follow the same supervisory model:
``` ```
┌────────────────────────────────────────────────────────────────────┐ ┌────────────────────────────────────────────────────────────────────┐
│ Layer 1 — UI │ │ 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 │ │ Logs live (SSE), Start/Stop/Restart/Update, Settings, Install │
│ │ │ │
│ src/app/(dashboard)/dashboard/providers/services/ │ │ src/app/(dashboard)/dashboard/providers/services/ │
│ ├── page.tsx Shell + tab routing by ?tab= │ │ ├── page.tsx Shell + tab routing by ?tab= │
│ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab│ │ ├── tabs/ CliproxyServiceTab, NinerouterServiceTab,
│ │ MuxServiceTab │
│ └── components/ ServiceStatusCard, ServiceLifecycleButtons,│ │ └── components/ ServiceStatusCard, ServiceLifecycleButtons,│
│ ServiceLogsPanel, ApiKeyCard, ... │ │ ServiceLogsPanel, ApiKeyCard, ... │
└──────────────────────┬─────────────────────────────────────────────┘ └──────────────────────┬─────────────────────────────────────────────┘
@@ -81,6 +84,8 @@ Both follow the same supervisory model:
│ rotate-key|status|auto-start|logs} │ │ rotate-key|status|auto-start|logs} │
│ /api/services/cliproxy/{install|start|stop|restart|update| │ │ /api/services/cliproxy/{install|start|stop|restart|update| │
│ status|auto-start|logs} │ │ status|auto-start|logs} │
│ /api/services/mux/{install|start|stop|restart|update| │
│ status|auto-start|logs} │
│ /dashboard/providers/services/9router/embed/[...path] │ │ /dashboard/providers/services/9router/embed/[...path] │
│ (reverse HTTP + WebSocket proxy → 9Router upstream) │ │ (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 │ │ modelSync.ts Periodic GET /v1/models → service_models table │
│ ringBuffer.ts Circular log buffer (5 MB per service) │ │ ringBuffer.ts Circular log buffer (5 MB per service) │
│ healthCheck.ts Polling HTTP health probe │ │ 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) │ OpenAI-compatible HTTP (loopback)
┌──────────────────────▼─────────────────────────────────────────────┐ ┌──────────────────────▼─────────────────────────────────────────────┐
@@ -123,6 +129,10 @@ Both follow the same supervisory model:
│ open-sse/config/providerRegistry.ts │ │ open-sse/config/providerRegistry.ts │
│ Models stored as "9router/{sub}/{model}" (prefixed). │ │ Models stored as "9router/{sub}/{model}" (prefixed). │
│ Synced every 5 min by modelSync.ts. │ │ 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/healthCheck.ts` | HTTP health probe (configurable interval) |
| `src/lib/services/installers/ninerouter.ts` | npm install/update/uninstall for 9Router | | `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/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/9router/_lib.ts` | `getOrInitSupervisor()` helper |
| `src/app/api/services/[name]/logs/route.ts` | Shared SSE logs endpoint | | `src/app/api/services/[name]/logs/route.ts` | Shared SSE logs endpoint |
| `open-sse/executors/ninerouter.ts` | Provider executor (Layer 4) | | `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`) | | `GET` | `/api/services/cliproxy/status` | Live + DB status (no `apiKeyMasked`) |
| `POST` | `/api/services/cliproxy/auto-start` | Toggle auto-start | | `POST` | `/api/services/cliproxy/auto-start` | Toggle auto-start |
The shared `GET /api/services/{name}/logs` endpoint (see §4.1) works for both The shared `GET /api/services/{name}/logs` endpoint (see §4.1) works for all
services using the `[name]` dynamic segment. 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 The dashboard embeds the 9Router web UI inside an iframe via an internal reverse
proxy at: proxy at:
@@ -486,14 +541,20 @@ matrix.
### API key injection ### 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 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). 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. 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 ### SSRF defense
The reverse HTTP proxy (`/dashboard/.../embed/[...path]`) is hardcoded to forward The reverse HTTP proxy (`/dashboard/.../embed/[...path]`) is hardcoded to forward

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -6,6 +6,24 @@
## [3.8.31] — 2026-06-20 ## [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 ## [3.8.43] — 2026-07-02
### ✨ New Features ### ✨ New Features

View File

@@ -1,7 +1,7 @@
openapi: 3.1.0 openapi: 3.1.0
info: info:
title: OmniRoute API title: OmniRoute API
version: 3.8.43 version: 3.8.44
description: | description: |
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible 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, endpoint that routes requests to multiple AI providers with load balancing,
@@ -3444,6 +3444,276 @@ paths:
"400": "400":
description: Invalid request body 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: /api/services/{name}/logs:
get: get:
tags: [Embedded Services] tags: [Embedded Services]
@@ -5264,6 +5534,214 @@ paths:
$ref: "#/components/responses/InternalError" $ref: "#/components/responses/InternalError"
"503": "503":
description: Generator module not available description: Generator module not available
/api/v1/ocr:
post:
tags:
- Images
summary: Document OCR
description: >-
Mistral OCRcompatible 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 Whispercompatible 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: components:
securitySchemes: securitySchemes:

View File

@@ -18,6 +18,7 @@ Complete reference for all OmniRoute API endpoints.
- [Embeddings](#embeddings) - [Embeddings](#embeddings)
- [Image Generation](#image-generation) - [Image Generation](#image-generation)
- [List Models](#list-models) - [List Models](#list-models)
- [Provider Plugin Manifest](#provider-plugin-manifest)
- [Compatibility Endpoints](#compatibility-endpoints) - [Compatibility Endpoints](#compatibility-endpoints)
- [Files API](#files-api) - [Files API](#files-api)
- [Batches API](#batches-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 ## Compatibility Endpoints
| Method | Path | Format | | Method | Path | Format |

View File

@@ -12,7 +12,7 @@ OmniRoute integrates with three categories of CLI tools spread across three dedi
| Page | Route | Concept | Count | | 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 | | **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 | | **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`: 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 | | forge | ForgeCode | Antinomy HQ | full | custom | true |
| jcode | jcode | 1jehuang (OSS) | full | custom | false | | jcode | jcode | 1jehuang (OSS) | full | custom | false |
| deepseek-tui | DeepSeek TUI | Hunter Bown (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 | | opencode | OpenCode | Anomaly (ex-SST) | full | guide | true |
| droid | Factory Droid | Factory AI | partial | guide | false | | droid | Factory Droid | Factory AI | partial | guide | false |
| copilot | GitHub Copilot CLI | GitHub/MS | full | custom | 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/forge-settings` | ForgeCode (.forge.toml) |
| `POST /api/cli-tools/jcode-settings` | jcode (--base-url flag) | | `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/smelt-settings` | Smelt |
| `POST /api/cli-tools/pi-settings` | Pi coding agent | | `POST /api/cli-tools/pi-settings` | Pi coding agent |

View File

@@ -117,6 +117,7 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari
| Variable | Default | Source File | Description | | Variable | Default | Source File | Description |
| ------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- | | ------------------------------------------- | ------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | | `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_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. | | `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. | | `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_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`. | | `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_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_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_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. | | `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_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_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. | | `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`). | | `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 ### 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_PERSONAL_ACCESS_TOKEN` | Qoder | Direct API key fallback (bypasses OAuth). |
| `QODER_CLI_WORKSPACE` | Qoder | Workspace ID for Qoder CLI. | | `QODER_CLI_WORKSPACE` | Qoder | Workspace ID for Qoder CLI. |
| `OMNIROUTE_QODER_WORKSPACE` | Qoder | Alias for `QODER_CLI_WORKSPACE`. | | `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. | | `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_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. | | `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. | | `FETCH_KEEPALIVE_TIMEOUT_MS` | `4000` | Keep-alive socket idle timeout. |
| `TLS_CLIENT_TIMEOUT_MS` | = `FETCH_TIMEOUT_MS` | TLS fingerprint proxy (wreq-js) 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. | | `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_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_HEADERS_TIMEOUT_MS` | `60000` | Time to send response headers via the bridge. |
| `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Bridge keep-alive idle timeout. | | `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_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_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. |
| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | | `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.). | | `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 `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_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_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_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. | | `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_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. | | `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_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_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_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. | | `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). | | `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. | | `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). | | `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_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_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_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_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. | | `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. |

View 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.

View File

@@ -75,3 +75,10 @@ For sustained high RPM/RPS and strict success SLO:
- `BIFROST_ENABLED=1` - `BIFROST_ENABLED=1`
- Keep API keys, allowlist, sanitizer, and rate-limit checks enabled in route handlers (they always run before downstream forwarding). - 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. - 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
View 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.

View File

@@ -1,12 +1,12 @@
{ {
"name": "omniroute-desktop", "name": "omniroute-desktop",
"version": "3.8.43", "version": "3.8.44",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "omniroute-desktop", "name": "omniroute-desktop",
"version": "3.8.43", "version": "3.8.44",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"electron-updater": "^6.8.9" "electron-updater": "^6.8.9"

View File

@@ -1,6 +1,6 @@
{ {
"name": "omniroute-desktop", "name": "omniroute-desktop",
"version": "3.8.43", "version": "3.8.44",
"description": "OmniRoute Desktop Application", "description": "OmniRoute Desktop Application",
"main": "main.js", "main": "main.js",
"author": { "author": {

View File

@@ -94,6 +94,13 @@ function readTimeoutMs(...values) {
/** @type {import('next').NextConfig} */ /** @type {import('next').NextConfig} */
const 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, distDir,
// Turbopack config: redirect native modules to stubs at build time // Turbopack config: redirect native modules to stubs at build time
turbopack: { turbopack: {
@@ -207,6 +214,13 @@ const nextConfig = {
"tough-cookie", "tough-cookie",
"@ngrok/ngrok", "@ngrok/ngrok",
"@huggingface/transformers", "@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", "child_process",
"fs", "fs",
"path", "path",

View File

@@ -3,6 +3,7 @@
* *
* Defines providers that support audio endpoints: * Defines providers that support audio endpoints:
* - /v1/audio/transcriptions (Whisper API) * - /v1/audio/transcriptions (Whisper API)
* - /v1/audio/translations (Whisper translate-to-English API)
* - /v1/audio/speech (TTS 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> = { export const AUDIO_SPEECH_PROVIDERS: Record<string, AudioProvider> = {
vertex: { vertex: {
id: "vertex", id: "vertex",
@@ -403,6 +428,13 @@ export function getTranscriptionProvider(providerId: string): AudioProvider | nu
return AUDIO_TRANSCRIPTION_PROVIDERS[providerId] || null; 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 * 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); 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 * Get all audio models as a flat list
*/ */

View File

@@ -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: { upstage: {
id: "upstage", id: "upstage",
baseUrl: "https://api.upstage.ai/v1/embeddings", baseUrl: "https://api.upstage.ai/v1/embeddings",

View File

@@ -175,6 +175,20 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "2048x2048"], 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: { together: {
id: "together", id: "together",
baseUrl: "https://api.together.xyz/v1/images/generations", baseUrl: "https://api.together.xyz/v1/images/generations",
@@ -549,6 +563,35 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
supportedSizes: ["1024x1024", "1024x1280", "1280x1024"], 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 // SenseNova (商汤日日新) Text-to-Image on the free Token Plan. OpenAI-compatible
// `/v1/images/generations`, so the generic OpenAI image handler routes it — same // `/v1/images/generations`, so the generic OpenAI image handler routes it — same
// SenseNova api-key/connection as the chat provider. (9router#2233) // 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" }], models: [{ id: "sensenova-u1-fast", name: "SenseNova U1 Fast" }],
supportedSizes: ["1024x1024"], 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"],
},
}; };
/** /**

View File

@@ -17,14 +17,12 @@
* still declared explicitly via `serviceKinds` on the provider entry; callers * still declared explicitly via `serviceKinds` on the provider entry; callers
* union the two sources. * union the two sources.
*/ */
import { import { AUDIO_TRANSCRIPTION_PROVIDERS, AUDIO_SPEECH_PROVIDERS } from "./audioRegistry.ts";
AUDIO_TRANSCRIPTION_PROVIDERS,
AUDIO_SPEECH_PROVIDERS,
} from "./audioRegistry.ts";
import { VIDEO_PROVIDERS } from "./videoRegistry.ts"; import { VIDEO_PROVIDERS } from "./videoRegistry.ts";
import { MUSIC_PROVIDERS } from "./musicRegistry.ts"; import { MUSIC_PROVIDERS } from "./musicRegistry.ts";
import { IMAGE_PROVIDERS } from "./imageRegistry.ts"; import { IMAGE_PROVIDERS } from "./imageRegistry.ts";
import { EMBEDDING_PROVIDERS } from "./embeddingRegistry.ts"; import { EMBEDDING_PROVIDERS } from "./embeddingRegistry.ts";
import { OCR_PROVIDERS } from "./ocrRegistry.ts";
/** Media kinds whose provider membership is defined by a backend registry. */ /** Media kinds whose provider membership is defined by a backend registry. */
export const MEDIA_KIND_REGISTRIES = { export const MEDIA_KIND_REGISTRIES = {
@@ -34,6 +32,7 @@ export const MEDIA_KIND_REGISTRIES = {
music: MUSIC_PROVIDERS, music: MUSIC_PROVIDERS,
image: IMAGE_PROVIDERS, image: IMAGE_PROVIDERS,
embedding: EMBEDDING_PROVIDERS, embedding: EMBEDDING_PROVIDERS,
ocr: OCR_PROVIDERS,
} as const satisfies Record<string, Record<string, unknown>>; } as const satisfies Record<string, Record<string, unknown>>;
export type RegistryMediaKind = keyof typeof MEDIA_KIND_REGISTRIES; export type RegistryMediaKind = keyof typeof MEDIA_KIND_REGISTRIES;

View File

@@ -5,7 +5,25 @@
* Follows OpenAI's moderation API format. * 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: { openai: {
id: "openai", id: "openai",
baseUrl: "https://api.openai.com/v1/moderations", baseUrl: "https://api.openai.com/v1/moderations",
@@ -16,22 +34,29 @@ export const MODERATION_PROVIDERS = {
{ id: "text-moderation-latest", name: "Text Moderation Latest" }, { 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; 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 }; 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 + "/")) { if (modelStr.startsWith(providerId + "/")) {
return { provider: providerId, model: modelStr.slice(providerId.length + 1) }; 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() { export function getAllModerationModels(): Array<{ id: string; name: string; provider: string }> {
const models = []; const models: Array<{ id: string; name: string; provider: string }> = [];
for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) { for (const [providerId, config] of Object.entries(MODERATION_PROVIDERS)) {
for (const model of config.models) { for (const model of config.models) {
models.push({ models.push({

View 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;
}

View 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;
}

View 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;
}

View 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),
};
}

View File

@@ -41,6 +41,8 @@ import { yiProvider } from "./registry/yi/index.ts";
import { deepseekProvider } from "./registry/deepseek/index.ts"; import { deepseekProvider } from "./registry/deepseek/index.ts";
import { deepseek_webProvider } from "./registry/deepseek/web/index.ts"; import { deepseek_webProvider } from "./registry/deepseek/web/index.ts";
import { dgridProvider } from "./registry/dgrid/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_coding_apikeyProvider } from "./registry/kimi/coding-apikey/index.ts";
import { kimi_codingProvider } from "./registry/kimi/coding/index.ts"; import { kimi_codingProvider } from "./registry/kimi/coding/index.ts";
import { kimiProvider } from "./registry/kimi/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 { inference_netProvider } from "./registry/inference-net/index.ts";
import { llm7Provider } from "./registry/llm7/index.ts"; import { llm7Provider } from "./registry/llm7/index.ts";
import { cerebrasProvider } from "./registry/cerebras/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 { sparkdeskProvider } from "./registry/sparkdesk/index.ts";
import { nlpcloudProvider } from "./registry/nlpcloud/index.ts"; import { nlpcloudProvider } from "./registry/nlpcloud/index.ts";
import { nvidiaProvider } from "./registry/nvidia/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 { grok_webProvider } from "./registry/grok-web/index.ts";
import { kieProvider } from "./registry/kie/index.ts"; import { kieProvider } from "./registry/kie/index.ts";
import { monsterapiProvider } from "./registry/monsterapi/index.ts"; import { monsterapiProvider } from "./registry/monsterapi/index.ts";
import { modelscopeProvider } from "./registry/modelscope/index.ts";
import { sensenovaProvider } from "./registry/sensenova/index.ts"; import { sensenovaProvider } from "./registry/sensenova/index.ts";
import { hyperbolicProvider } from "./registry/hyperbolic/index.ts"; import { hyperbolicProvider } from "./registry/hyperbolic/index.ts";
import { lambda_aiProvider } from "./registry/lambda-ai/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 { bailian_coding_planProvider } from "./registry/bailian-coding-plan/index.ts";
import { gigachatProvider } from "./registry/gigachat/index.ts"; import { gigachatProvider } from "./registry/gigachat/index.ts";
import { devin_cliProvider } from "./registry/devin-cli/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 { chutesProvider } from "./registry/chutes/index.ts";
import { factoryProvider } from "./registry/factory/index.ts"; import { factoryProvider } from "./registry/factory/index.ts";
import { databricksProvider } from "./registry/databricks/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 { codebuddy_cnProvider } from "./registry/codebuddy-cn/index.ts";
import { pioneerProvider } from "./registry/pioneer/index.ts"; import { pioneerProvider } from "./registry/pioneer/index.ts";
import { zenmux_freeProvider } from "./registry/zenmux-free/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> = { export const REGISTRY: Record<string, RegistryEntry> = {
aimlapi: aimlapiProvider, aimlapi: aimlapiProvider,
@@ -212,6 +221,8 @@ export const REGISTRY: Record<string, RegistryEntry> = {
deepseek: deepseekProvider, deepseek: deepseekProvider,
"deepseek-web": deepseek_webProvider, "deepseek-web": deepseek_webProvider,
dgrid: dgridProvider, dgrid: dgridProvider,
bai: baiProvider,
qiniu: qiniuProvider,
"kimi-coding-apikey": kimi_coding_apikeyProvider, "kimi-coding-apikey": kimi_coding_apikeyProvider,
"kimi-coding": kimi_codingProvider, "kimi-coding": kimi_codingProvider,
kimi: kimiProvider, kimi: kimiProvider,
@@ -220,6 +231,9 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"inference-net": inference_netProvider, "inference-net": inference_netProvider,
llm7: llm7Provider, llm7: llm7Provider,
cerebras: cerebrasProvider, cerebras: cerebrasProvider,
"charm-hyper": charmHyperProvider,
nube: nubeProvider,
clinepass: clinepassProvider,
sparkdesk: sparkdeskProvider, sparkdesk: sparkdeskProvider,
nlpcloud: nlpcloudProvider, nlpcloud: nlpcloudProvider,
nvidia: nvidiaProvider, nvidia: nvidiaProvider,
@@ -249,6 +263,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"grok-web": grok_webProvider, "grok-web": grok_webProvider,
kie: kieProvider, kie: kieProvider,
monsterapi: monsterapiProvider, monsterapi: monsterapiProvider,
modelscope: modelscopeProvider,
sensenova: sensenovaProvider, sensenova: sensenovaProvider,
hyperbolic: hyperbolicProvider, hyperbolic: hyperbolicProvider,
"lambda-ai": lambda_aiProvider, "lambda-ai": lambda_aiProvider,
@@ -311,6 +326,7 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"bailian-coding-plan": bailian_coding_planProvider, "bailian-coding-plan": bailian_coding_planProvider,
gigachat: gigachatProvider, gigachat: gigachatProvider,
"devin-cli": devin_cliProvider, "devin-cli": devin_cliProvider,
auggie: auggieProvider,
chutes: chutesProvider, chutes: chutesProvider,
factory: factoryProvider, factory: factoryProvider,
databricks: databricksProvider, databricks: databricksProvider,
@@ -344,4 +360,6 @@ export const REGISTRY: Record<string, RegistryEntry> = {
"codebuddy-cn": codebuddy_cnProvider, "codebuddy-cn": codebuddy_cnProvider,
pioneer: pioneerProvider, pioneer: pioneerProvider,
"zenmux-free": zenmux_freeProvider, "zenmux-free": zenmux_freeProvider,
sumopod: sumopodProvider,
x5lab: x5labProvider,
}; };

View File

@@ -38,6 +38,13 @@ export const anthropicProvider: RegistryEntry = {
}, },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6" }, { id: "claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "claude-opus-4.5", name: "Claude Opus 4.5" }, { 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.6", name: "Claude Sonnet 4.6" },
{ id: "claude-sonnet-4.5", 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" }, { id: "claude-haiku-4.5", name: "Claude Haiku 4.5" },

View 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 },
],
};

View 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,
};

View File

@@ -12,6 +12,7 @@ export const blackboxProvider: RegistryEntry = {
models: [ models: [
{ id: "claude-fable-5", name: "Claude Fable 5" }, { id: "claude-fable-5", name: "Claude Fable 5" },
{ id: "claude-opus-4.8", name: "Claude Opus 4.8" }, { 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: "claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
{ id: "gpt-5.5", name: "GPT-5.5" }, { id: "gpt-5.5", name: "GPT-5.5" },
{ id: "gpt-5.4-pro", name: "GPT-5.4 Pro" }, { id: "gpt-5.4-pro", name: "GPT-5.4 Pro" },

View 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,
};

View File

@@ -65,6 +65,18 @@ export const claudeProvider: RegistryEntry = {
contextLength: 200000, contextLength: 200000,
maxOutputTokens: 64000, 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", id: "claude-sonnet-4-6",
name: "Claude 4.6 Sonnet", name: "Claude 4.6 Sonnet",

View 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,
};

View File

@@ -14,13 +14,13 @@ export const grok_cliProvider: RegistryEntry = {
{ {
id: "grok-build", id: "grok-build",
name: "Grok Build", name: "Grok Build",
contextLength: 128000, contextLength: 256000,
unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"],
}, },
{ {
id: "grok-composer-2.5-fast", id: "grok-composer-2.5-fast",
name: "Grok Composer 2.5 Fast", name: "Grok Composer 2.5 Fast",
contextLength: 128000, contextLength: 200000,
unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"], unsupportedParams: ["presencePenalty", "frequencyPenalty", "logprobs", "topLogprobs"],
}, },
], ],

View File

@@ -14,8 +14,14 @@ export const kimi_webProvider: RegistryEntry = {
authType: "apikey", authType: "apikey",
authHeader: "cookie", authHeader: "cookie",
models: [ models: [
{ id: "kimi-default", name: "Kimi Default" }, // Model ids are the `key` field from www.kimi.com's
{ id: "kimi-k2.6", name: "Kimi K2.6 (Thinking)" }, // `/apiv2/kimi.gateway.config.v1.ConfigService/GetAvailableModels` response.
{ id: "kimi-128k", name: "Kimi 128K (Long Context)" }, // 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 },
], ],
}; };

View 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: [],
};

View 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: [],
};

View 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,
};

View File

@@ -18,6 +18,8 @@ export const qoderProvider: RegistryEntry = {
}, },
models: [ models: [
{ id: "qoder-rome-30ba3b", name: "Qoder ROME" }, { 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-coder-plus", name: "Qwen3 Coder Plus" },
{ id: "qwen3-max", name: "Qwen3 Max" }, { id: "qwen3-max", name: "Qwen3 Max" },
{ id: "qwen3-vl-plus", name: "Qwen3 Vision Plus", supportsVision: true }, { id: "qwen3-vl-plus", name: "Qwen3 Vision Plus", supportsVision: true },

View 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,
};

View File

@@ -11,15 +11,41 @@ export const theoldllmProvider: RegistryEntry = {
authType: "none", authType: "none",
authHeader: "none", authHeader: "none",
defaultContextLength: 200000, 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: [ models: [
// ── Current free tier (refreshed for #5181) ──
{ id: "GPT_5_4", name: "GPT-5.4 (The Old LLM 🆓)", contextLength: 400000 }, { 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: "GPT_4o", name: "GPT-4o (The Old LLM 🆓)" },
{ id: "claude_opus_4", name: "Claude Opus 4 (The Old LLM 🆓)", contextLength: 200000 }, { 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_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: "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: "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_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, passthroughModels: true,
}; };

View 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,
};

View File

@@ -4,7 +4,7 @@ export const xaiProvider: RegistryEntry = {
id: "xai", id: "xai",
alias: "xai", alias: "xai",
format: "openai", format: "openai",
executor: "default", executor: "xai",
baseUrl: "https://api.x.ai/v1/chat/completions", baseUrl: "https://api.x.ai/v1/chat/completions",
authType: "apikey", authType: "apikey",
authHeader: "bearer", authHeader: "bearer",

View File

@@ -46,7 +46,13 @@ import {
} from "../services/cloudCodeThinking.ts"; } from "../services/cloudCodeThinking.ts";
import { buildGeminiTools } from "../translator/helpers/geminiToolsSanitizer.ts"; import { buildGeminiTools } from "../translator/helpers/geminiToolsSanitizer.ts";
import { DEFAULT_SAFETY_SETTINGS } from "../translator/helpers/geminiHelper.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 { import {
applyAntigravityClientProfileHeaders, applyAntigravityClientProfileHeaders,
removeHeaderCaseInsensitive, removeHeaderCaseInsensitive,
@@ -155,70 +161,6 @@ function serializeAntigravityRequest(
return applyFingerprint(provider, { ...headers }, serializedBody); 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> & { type AntigravityRequestEnvelope = Record<string, unknown> & {
project: string; project: string;
model?: string; model?: string;
@@ -372,84 +314,6 @@ export function markConnectionQuotaExhausted(connectionId: string, retryAfterMs:
* Accumulate one Antigravity SSE `data:` payload into `collected`. Exported for unit * Accumulate one Antigravity SSE `data:` payload into `collected`. Exported for unit
* tests (the markdown / candidate-parts extraction branches). @internal * 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"). * Strip provider prefixes (e.g. "antigravity/model" → "model").

View 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);
}

View 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",
},
});
}

View File

@@ -66,6 +66,26 @@ import {
stripProxyToolPrefix, stripProxyToolPrefix,
} from "./claudeIdentity.ts"; } from "./claudeIdentity.ts";
import { withForcedResponsesUpstream } from "./forceResponsesUpstream.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. * Sanitizes a custom API path to prevent path traversal attacks.
@@ -155,95 +175,6 @@ export type CountTokensInput = {
signal?: AbortSignal | null; 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 { export function mergeAbortSignals(primary: AbortSignal, secondary: AbortSignal): AbortSignal {
const controller = new AbortController(); const controller = new AbortController();
@@ -272,161 +203,6 @@ function hasActiveClaudeThinking(body: Record<string, unknown>): boolean {
return thinking?.type === "enabled" || thinking?.type === "adaptive"; 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 * 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 * fields (e.g. `cc/claude-opus-4-8` → `claude-opus-4-8`). Versioned built-in

View 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;
}

View 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;
}

View File

@@ -32,6 +32,7 @@ import {
__resetChatGptImageCacheForTesting, __resetChatGptImageCacheForTesting,
type ChatGptImageConversationContext, type ChatGptImageConversationContext,
} from "../services/chatgptImageCache.ts"; } from "../services/chatgptImageCache.ts";
import { isThinkingCapableModel, resolveChatGptModel } from "./chatgpt-web/models.ts";
// ─── Constants ────────────────────────────────────────────────────────────── // ─── 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 // 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; // 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". // "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 ────────────────────────────────────────── // ─── 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_TTL_MS = 5 * 60 * 1000;
const THINKING_EFFORT_CACHE_MAX = 400; 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 { function configuredProPollTimeoutMs(): number {
const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS); const raw = Number(process.env.OMNIROUTE_CGPT_WEB_PRO_TIMEOUT_MS);
if (!Number.isFinite(raw) || raw <= 0) return DEFAULT_PRO_POLL_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). // on a tool-role message (handled below).
if (event.type === "server_ste_metadata") { if (event.type === "server_ste_metadata") {
const meta = (event as Record<string, unknown>).metadata as const meta = (event as Record<string, unknown>).metadata as
| Record<string, unknown> Record<string, unknown> | undefined;
| undefined;
if (meta && meta.turn_use_case === "image gen") { if (meta && meta.turn_use_case === "image gen") {
imageGenAsync = true; imageGenAsync = true;
} }
@@ -2780,8 +2650,7 @@ export class ChatGptWebExecutor extends BaseExecutor {
clientHeaders, clientHeaders,
}: ExecuteInput) { }: ExecuteInput) {
const messages = (body as Record<string, unknown> | null)?.messages as const messages = (body as Record<string, unknown> | null)?.messages as
| Array<Record<string, unknown>> Array<Record<string, unknown>> | undefined;
| undefined;
if (!messages || !Array.isArray(messages) || messages.length === 0) { if (!messages || !Array.isArray(messages) || messages.length === 0) {
return { return {
response: errorResponse(400, "Missing or empty messages array"), response: errorResponse(400, "Missing or empty messages array"),

View 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 };
}

View File

@@ -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 // Web-cookie providers (chatgpt-web, perplexity-web, ...) have no native
// `tools`, the prompt-side shim (`prepareToolMessages` in // function calling. When the OpenAI request carries `tools`, the prompt-side
// ../translator/webTools.ts) injects a `<tool>` contract; on the response side // shim (`prepareToolMessages` in ../translator/webTools.ts) injects a `<tool>`
// we parse `<tool>{...}</tool>` blocks back into OpenAI `tool_calls` — // contract; on the response side we parse `<tool>{...}</tool>` blocks back
// mirroring the sibling web-session executors (qwen-web, perplexity-web, ...). // into OpenAI `tool_calls`.
// //
// The whole tool-mode orchestration lives here so the (frozen) chatgpt-web.ts // The whole tool-mode orchestration lives here — provider-agnostic — so each
// only gains an import + a single delegating call. // (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"; import { buildToolAwareResult } from "../translator/webTools.ts";
@@ -28,7 +31,8 @@ function sseChunk(data: unknown): string {
*/ */
async function applyToolCallsToJsonResponse( async function applyToolCallsToJsonResponse(
response: Response, response: Response,
requestedTools: unknown requestedTools: unknown,
idSeed: string
): Promise<Response> { ): Promise<Response> {
const bodyText = await response.text(); const bodyText = await response.text();
try { try {
@@ -37,7 +41,7 @@ async function applyToolCallsToJsonResponse(
const { content, toolCalls, finishReason } = buildToolAwareResult( const { content, toolCalls, finishReason } = buildToolAwareResult(
rawContent, rawContent,
requestedTools, requestedTools,
"cgpt" idSeed
); );
if (toolCalls) { if (toolCalls) {
json.choices[0].message = { role: "assistant", content: null, tool_calls: toolCalls }; json.choices[0].message = { role: "assistant", content: null, tool_calls: toolCalls };
@@ -107,9 +111,13 @@ export async function buildToolModeResponse(
bufferedJson: Response, bufferedJson: Response,
requestedTools: unknown, requestedTools: unknown,
stream: boolean, stream: boolean,
meta: { cid: string; created: number; model: string } meta: { cid: string; created: number; model: string; idSeed?: string }
): Promise<Response> { ): Promise<Response> {
const jsonResponse = await applyToolCallsToJsonResponse(bufferedJson, requestedTools); const jsonResponse = await applyToolCallsToJsonResponse(
bufferedJson,
requestedTools,
meta.idSeed ?? "cgpt"
);
if (!stream) return jsonResponse; if (!stream) return jsonResponse;
const completion = await jsonResponse.json(); const completion = await jsonResponse.json();
return new Response(toolCompletionToSseStream(completion, meta.cid, meta.created, meta.model), { 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