diff --git a/.env.example b/.env.example index 1ad4b13a3c..cf179cba46 100644 --- a/.env.example +++ b/.env.example @@ -91,6 +91,12 @@ OMNIROUTE_USE_TURBOPACK=1 # Used by: src/lib/runtime/ports.ts — preserves canonical port in Electron. # OMNIROUTE_PORT=20128 +# Hostname/bind address for the Next.js server. +# Used by: scripts/run-next.mjs (HOST), Playwright runner (HOSTNAME). +# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests). +#HOST=0.0.0.0 +#HOSTNAME=127.0.0.1 + # Environment mode — affects Next.js behavior, logging verbosity, and caching. # Values: production | development | Default: production NODE_ENV=production @@ -147,6 +153,10 @@ ALLOW_API_KEY_REVEAL=false # Default: false (blocked) | Set true to enable local providers. # OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true +# Legacy alias toggling the SSRF guard. Used by: src/shared/network/outboundUrlGuard.ts +# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable. +# OUTBOUND_SSRF_GUARD_ENABLED=true + # ═══════════════════════════════════════════════════════════════════════════════ # 5. INPUT SANITIZATION & PII PROTECTION (FASE-01) @@ -250,6 +260,20 @@ NEXT_PUBLIC_CLOUD_URL= # Legacy alias — fallback for NEXT_PUBLIC_BASE_URL in sync schedulers. # NEXT_PUBLIC_APP_URL=http://localhost:20128 +# Public callback URL for asynchronous image/audio jobs (kie.ai, etc.). +# Used by: open-sse/utils/kieTask.ts — overrides callbackUrlFromBaseUrl(). +# Honor order: KIE_CALLBACK_URL → OMNIROUTE_KIE_CALLBACK_URL → OMNIROUTE_PUBLIC_URL. +#KIE_CALLBACK_URL= +#OMNIROUTE_KIE_CALLBACK_URL= +#OMNIROUTE_PUBLIC_URL= + +# Upstream quota endpoints used by the Usage page. Override only for +# debugging or when routing through a corporate mirror. Used by: +# open-sse/services/usage.ts. +#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/ +#OMNIROUTE_GEMINI_CLI_USAGE_URL=https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist +#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com + # ═══════════════════════════════════════════════════════════════════════════════ # 8. OUTBOUND PROXY (Upstream Provider Calls) @@ -332,6 +356,17 @@ NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true # Full list: admin, combos, health, models, routing, budget, metrics, pricing, memory, skills # OMNIROUTE_MCP_SCOPES=admin,combos,health +# Compress MCP tool descriptions before serializing the manifest. +# Used by: open-sse/mcp-server/descriptionCompressor.ts — reduces token spend +# for clients that read the full tool catalog. +# Accepted disabling values: 0, false, off. Default: enabled. +# OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS=1 + +# Algorithm/profile used when description compression is enabled. +# Used by: open-sse/mcp-server/descriptionCompressor.ts +# Set to 0/false/off to skip compression entirely. Default: rtk +# OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk + # Model catalog sync interval in hours. # Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh. # Default: 24 @@ -347,6 +382,48 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70 # Useful for: CI builds, test environments, or resource-constrained containers. # OMNIROUTE_DISABLE_BACKGROUND_SERVICES=false +# Force runtime background tasks (healthchecks/sync) even under automated test +# detection. Used by: src/lib/config/runtimeSettings.ts — overrides the test +# heuristic in instrumentation-node.ts. Default: unset (tests skip background). +#OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS=1 + +# Background job interval for budget reset checks (ms). Default: 600000 (10m). +# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000. +#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000 + +# Reasoning cache cleanup cadence (ms). Default: 1800000 (30m). Floor: 60000. +# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts. +#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000 + +# Spend write batcher cadence (ms) and buffer size before forced flush. +# Used by: src/lib/spend/batchWriter.ts. Defaults: 60000 ms / 1000 entries. +#OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000 +#OMNIROUTE_SPEND_MAX_BUFFER_SIZE=1000 + +# Config hot-reload polling interval (ms). Default: 5000. +# Used by: src/lib/config/hotReload.ts. Lower than 1000ms is rejected. +#OMNIROUTE_CONFIG_HOT_RELOAD_MS=5000 + +# Override the migrations directory used by src/lib/db/migrationRunner.ts. +# Default: /src/lib/db/migrations. +#OMNIROUTE_MIGRATIONS_DIR= + +# Trust user-managed RTK project filter rules without strict signature checks. +# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0. +#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0 + +# Force a DB healthcheck regardless of cadence. Default: 0. +# Used by: src/lib/db/core.ts::shouldRunDbHealthCheck(). +#OMNIROUTE_FORCE_DB_HEALTHCHECK=0 + +# DB healthcheck cadence override (ms). Default: 21600000 (6h). +# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs(). +#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000 + +# Skip the Redis-backed auth cache used by API key lookups (forces DB reads). +# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled. +#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0 + # Flag set by bootstrap script after initial setup is complete. # Used by: src/app/(dashboard)/dashboard/page.tsx — shows setup wizard vs. dashboard. # OMNIROUTE_BOOTSTRAPPED=false @@ -355,6 +432,11 @@ PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70 # Used by: open-sse/executors/antigravity.ts — escape hatch for multi-project setups. # OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=0 +# Adjust how Antigravity advertises remaining credits. Used by: +# open-sse/services/antigravityCredits.ts — accepts forced override strings. +# Default: empty (use upstream-reported credits). +#ANTIGRAVITY_CREDITS= + # ═══════════════════════════════════════════════════════════════════════════════ # 11. OAUTH PROVIDER CREDENTIALS @@ -406,6 +488,14 @@ WINDSURF_FIREBASE_API_KEY=AIzaSyBpLTEGSt59AUPKxBb7lIWjSE2ZXQH7mgU # Required scopes: api, read_user, openid, profile, email # GITLAB_DUO_OAUTH_CLIENT_ID=*** # GITLAB_DUO_OAUTH_CLIENT_SECRET=*** # optional — PKCE flow does not require a secret +# +# Self-managed GitLab Duo instance overrides. +# Used by: src/lib/oauth/gitlab.ts and src/lib/oauth/constants/oauth.ts — +# fall back to these when the _DUO_ variants above are unset. +#GITLAB_DUO_BASE_URL=https://gitlab.com +#GITLAB_BASE_URL=https://gitlab.com +#GITLAB_OAUTH_CLIENT_ID= +#GITLAB_OAUTH_CLIENT_SECRET= # ── Qoder ── QODER_OAUTH_CLIENT_SECRET=4Z3YjXycVsQvyGF1etiNlIBB4RsqSDtW @@ -477,6 +567,10 @@ QWEN_USER_AGENT="QwenCode/0.15.9 (linux; x64)" CURSOR_USER_AGENT="Cursor/3.3" GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" +# Override Codex client version sent in headers independently of the +# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts. +# CODEX_CLIENT_VERSION=0.130.0 + # ═══════════════════════════════════════════════════════════════════════════════ # 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection) @@ -491,7 +585,6 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # CLI_COMPAT_CLAUDE=1 # CLI_COMPAT_GITHUB=1 # CLI_COMPAT_ANTIGRAVITY=1 -# CLI_COMPAT_KIRO=1 # CLI_COMPAT_CURSOR=1 # CLI_COMPAT_KIMI_CODING=1 # CLI_COMPAT_KILOCODE=1 @@ -501,6 +594,12 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # Or enable for all providers at once: # CLI_COMPAT_ALL=1 +# ── Kimi Coding CLI identity overrides ── +# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers. +# Leave unset to use the captured defaults baked into the OmniRoute build. +#KIMI_CLI_VERSION=1.36.0 +#KIMI_CODING_DEVICE_ID= + # ═══════════════════════════════════════════════════════════════════════════════ # 14. API KEY PROVIDERS @@ -509,20 +608,20 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # Preferred setup: Dashboard → Providers → Add API Key. # Setting here is an alternative for Docker/headless deployments. +# Static API keys for direct-authentication providers wired through the runtime. +# OmniRoute loads provider credentials from the encrypted database or +# data/provider-credentials.json. The variables below are documented escape +# hatches that are referenced in code today. # DEEPSEEK_API_KEY= -# GROQ_API_KEY= -# XAI_API_KEY= -# MISTRAL_API_KEY= -# PERPLEXITY_API_KEY= -# TOGETHER_API_KEY= -# FIREWORKS_API_KEY= -# CEREBRAS_API_KEY= -# COHERE_API_KEY= # NVIDIA_API_KEY= +# Windsurf / Devin CLI direct API key. +# Used by: open-sse/executors/devin-cli.ts — bypasses OAuth when set. +# WINDSURF_API_KEY= + # Embedding Providers (optional — used by /v1/embeddings) -# NEBIUS_API_KEY= -# Provider keys above (OpenAI, Mistral, Together, Fireworks, NVIDIA) also work for embeddings. +# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers +# also work for embeddings. # ═══════════════════════════════════════════════════════════════════════════════ @@ -546,6 +645,29 @@ GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0" # FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s) # FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s) +# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the +# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min). +# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000 + +# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ── +# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for +# the bogdanfinn/tls-client koffi binding and the JS-side grace window +# layered on top of it when the native library is wedged. +# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000 +# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000 + +# ── Circuit breaker thresholds and reset windows ── +# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts. +# Defaults match historical PROVIDER_PROFILES values (post-scaling for +# 500+ connections). Lower the threshold to react faster, raise it to +# tolerate more transient failures before short-circuiting. +# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD=8 +# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS=60000 +# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD=12 +# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS=30000 +# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD=2 +# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS=15000 + # ── Stream idle detection ── # STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000) # # Extended-thinking models rarely pause >90s. @@ -718,6 +840,13 @@ APP_LOG_TO_FILE=true # NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s) # NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s) +# ── AWS Bedrock (Kiro / Audio) ── +# Region used to construct AWS Bedrock endpoints. Used by: +# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts. +# AWS_REGION takes precedence over AWS_DEFAULT_REGION when both are set. +# AWS_REGION=us-east-1 +# AWS_DEFAULT_REGION=us-east-1 + # ── Cloudflare Workers AI ── # Account ID override for Cloudflare Workers AI executor. # Used by: open-sse/executors/cloudflare-ai.ts @@ -777,21 +906,49 @@ APP_LOG_TO_FILE=true # Used by: open-sse/services/rateLimitManager.ts # RATE_LIMIT_MAX_WAIT_MS=120000 +# Force the auto-enable rate limit safety net on/off regardless of the persisted +# Dashboard setting. Used by: open-sse/services/rateLimitManager.ts. +# Accepted values: true|1|on (force on), false|0|off (force off), unset (use Dashboard). +# RATE_LIMIT_AUTO_ENABLE= + +# Stagger interval (ms) between provider token healthchecks at startup. +# Used by: src/lib/tokenHealthCheck.ts. Default: 3000. +# HEALTHCHECK_STAGGER_MS=3000 + # ═══════════════════════════════════════════════════════════════════════════════ # 22. DEBUGGING # ═══════════════════════════════════════════════════════════════════════════════ # These variables enable verbose debugging output. NEVER enable in production. -# Dump Cursor protobuf decode/encode details to console. -# CURSOR_PROTOBUF_DEBUG=1 - -# Dump raw Cursor SSE stream data to console. +# Cursor executor verbose debug (decoded SSE chunks, etc.). +# CURSOR_STREAM_DEBUG is kept as a backward-compatible alias. +# Used by: open-sse/executors/cursor.ts +# CURSOR_DEBUG=1 # CURSOR_STREAM_DEBUG=1 +# When CURSOR_DEBUG=1, also append raw decoded chunks to this file path. +# CURSOR_DUMP_FILE=/tmp/cursor-stream.log + +# Cursor stream idle timeout (ms). Default: 300000 (5 min). +# Used by: open-sse/executors/cursor.ts. +# CURSOR_STREAM_TIMEOUT_MS=300000 + +# Cursor state DB path override (for cursor version detection). +# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically. +# CURSOR_STATE_DB_PATH= + +# Direct Cursor bearer token used by scripts/cursor-tap.cjs (developer tooling). +# CURSOR_TOKEN= + # Log Responses API SSE-to-JSON translation details. # DEBUG_RESPONSES_SSE_TO_JSON=true +# Log request shape (content-type + content-length) for large chat payloads. +# Used by: src/app/api/v1/chat/completions/route.ts. Set to "0" to silence. +# Default: enabled. +# OMNIROUTE_LOG_REQUEST_SHAPE=1 + # Write raw (untruncated) request/response JSON in call log artifacts. # When enabled, serializeArtifactForStorage skips size-based truncation. # Also enabled automatically when APP_LOG_LEVEL=debug. @@ -813,3 +970,128 @@ APP_LOG_TO_FILE=true # GitHub Personal Access Token with issues:write scope. # GITHUB_ISSUES_TOKEN=ghp_xxxx + +# Generic GitHub access token consumed by issue triage / agent helpers. +# Used by: src/app/api/v1/issues/* and src/lib/cloudAgent/* — falls back to +# GITHUB_ISSUES_TOKEN when unset. +# GITHUB_TOKEN= + + +# ═══════════════════════════════════════════════════════════════════════════════ +# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS +# ═══════════════════════════════════════════════════════════════════════════════ +# Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug +# proxy), 1Proxy egress pool, skills sandbox runtime, and miscellaneous CLI +# binaries referenced by the executor layer or the dashboard runtime. + +# ── Alibaba (Bailian) coding plan quota ── +# Host/full URL override used by: open-sse/services/bailianQuotaFetcher.ts. +# When unset the fetcher uses the production Alibaba endpoints. +# ALIBABA_CODING_PLAN_HOST= +# ALIBABA_CODING_PLAN_QUOTA_URL= + +# ── Context window tuning ── +# Tokens reserved for completion output when computing prompt budgets. +# Used by: open-sse/services/contextManager.ts. Default: 1024. +# CONTEXT_RESERVE_TOKENS=1024 + +# ── Model alias rewriting (legacy compatibility) ── +# Toggle the legacy model-alias compatibility layer used by older clients. +# Used by: open-sse/services/model.ts. Default: enabled. +# MODEL_ALIAS_COMPAT_ENABLED=true + +# ── Devin CLI binary path ── +# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH. +# CLI_DEVIN_BIN=devin + +# ── Command Code (custom CLI) callback ── +# Local port used for OAuth-style callbacks from the Command Code CLI helper. +# Used by: src/app/api/providers/command-code/auth/shared.ts. +# COMMAND_CODE_CALLBACK_PORT= + +# ── MITM debug proxy (development only) ── +# Used by: src/mitm/server.cjs — captures upstream traffic for inspection. +# MITM_LOCAL_PORT=443 +# MITM_DISABLE_TLS_VERIFY=0 + +# ── 1Proxy egress pool ── +# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute +# CrofAI 1Proxy service. Disable, override URL, or tune the import quality. +# ONEPROXY_ENABLED=true +# ONEPROXY_API_URL=https://1proxy-api.aitradepulse.com +# ONEPROXY_MAX_PROXIES=500 +# ONEPROXY_MIN_QUALITY_THRESHOLD=50 + +# ── Tailscale tunnel binaries ── +# Optional explicit paths to tailscale/tailscaled binaries used by the +# dashboard's tunnel manager. Used by: src/lib/tailscaleTunnel.ts. +# TAILSCALE_BIN=/usr/local/bin/tailscale +# TAILSCALED_BIN=/usr/local/bin/tailscaled + +# ── Ngrok tunnel ── +# Used by: src/lib/ngrokTunnel.ts — authenticates outbound tunnels. +# NGROK_AUTHTOKEN= + +# ── Database backups ── +# Used by: src/lib/db/backup.ts. +# DB_BACKUP_MAX_FILES=20 +# DB_BACKUP_RETENTION_DAYS=0 + +# ── TLS sidecar override ── +# Used by: open-sse/services/chatgptTlsClient.ts tests. Production deployments +# should leave this unset; the sidecar is auto-managed. +# OMNIROUTE_TLS_PROXY_URL= + +# ── Skills sandbox (experimental) ── +# Used by: src/lib/skills/builtins.ts. All values support comma lists where +# noted in the source. +# SKILLS_MAX_FILE_BYTES=1048576 +# SKILLS_MAX_HTTP_RESPONSE_BYTES=256000 +# SKILLS_MAX_SANDBOX_OUTPUT_CHARS=100000 +# SKILLS_SANDBOX_TIMEOUT_MS=10000 +# SKILLS_SANDBOX_NETWORK_ENABLED=0 +# SKILLS_ALLOWED_SANDBOX_IMAGES= + + +# ═══════════════════════════════════════════════════════════════════════════════ +# 25. TEST & E2E +# ═══════════════════════════════════════════════════════════════════════════════ +# Used by scripts/run-next-playwright.mjs, scripts/smoke-electron-packaged.mjs, +# scripts/run-ecosystem-tests.mjs and scripts/uninstall.mjs. +# Production deployments should leave every value below unset. + +# E2E bootstrap mode for the Playwright runner. Accepted: auth | fresh | reuse. +# Default (when unset): auth. +# OMNIROUTE_E2E_BOOTSTRAP_MODE=auth + +# Admin password injected into the Playwright test environment. +# Falls back to INITIAL_PASSWORD when unset. +# OMNIROUTE_E2E_PASSWORD= + +# Disable the local healthcheck poll during Playwright runs (default: true). +# OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK=true + +# Disable the OAuth token healthcheck loop during tests (default: true). +# OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true + +# Silence healthcheck noise in Playwright stdout (default: true). +# OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true + +# Skip the Next.js production build before Playwright starts (CI optimization). +# OMNIROUTE_PLAYWRIGHT_SKIP_BUILD=0 + +# Skip the OmniRoute uninstall hook (used by CI to keep node_modules intact). +# OMNIROUTE_SKIP_UNINSTALL_HOOK=0 + +# Ecosystem/protocol test orchestrators wait this long (ms) for the server to +# become healthy. Default: 180000. +# ECOSYSTEM_SERVER_WAIT_MS=180000 + +# Electron smoke harness (used by scripts/smoke-electron-packaged.mjs). +# ELECTRON_SMOKE_URL=http://127.0.0.1:20128/login +# ELECTRON_SMOKE_TIMEOUT_MS=45000 +# ELECTRON_SMOKE_SETTLE_MS=2000 +# ELECTRON_SMOKE_APP_EXECUTABLE= +# ELECTRON_SMOKE_DATA_DIR= +# ELECTRON_SMOKE_KEEP_DATA=0 +# ELECTRON_SMOKE_STREAM_LOGS=0 diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index 71bb4a8f7f..980db87602 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -3,6 +3,12 @@ > Complete reference for every environment variable recognized by OmniRoute. > For a quick-start template, see [`.env.example`](../.env.example). +> [!IMPORTANT] +> Every variable documented here must also appear in `.env.example`, and +> every variable in `.env.example` must appear here. `npm run check:env-doc-sync` +> enforces this on commit and in CI. To omit a variable on purpose, add it to +> the allowlist inside `scripts/check-env-doc-sync.mjs`. + --- ## Table of Contents @@ -93,17 +99,19 @@ OmniRoute uses **SQLite** (via `better-sqlite3`) for all persistence. These vari ## 3. Network & Ports -| Variable | Default | Source File | Description | -| ------------------------- | ------------------------------- | --------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | -| `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. | -| `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. | -| `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. | -| `PROD_DASHBOARD_PORT` | `20130` | `docker-compose.prod.yml` | Host-side published port for the Dashboard in Docker production mode. | -| `PROD_API_PORT` | `20131` | `docker-compose.prod.yml` | Host-side published port for the API in Docker production mode. | -| `OMNIROUTE_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | Takes precedence over `PORT` when running inside Electron or other wrappers. | -| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. | -| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. | +| Variable | Default | Source File | Description | +| ------------------------- | ------------------------------- | --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PORT` | `20128` | `src/lib/runtime/ports.ts` | Primary port for both Dashboard UI and API endpoints (single-port mode). | +| `API_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the `/v1/*` proxy API on this separate port. | +| `API_HOST` | `0.0.0.0` | `src/lib/runtime/ports.ts` | Bind address for the API port. | +| `DASHBOARD_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | When set, serves the Dashboard UI on this separate port. | +| `PROD_DASHBOARD_PORT` | `20130` | `docker-compose.prod.yml` | Host-side published port for the Dashboard in Docker production mode. | +| `PROD_API_PORT` | `20131` | `docker-compose.prod.yml` | Host-side published port for the API in Docker production mode. | +| `OMNIROUTE_PORT` | _(unset)_ | `src/lib/runtime/ports.ts` | Takes precedence over `PORT` when running inside Electron or other wrappers. | +| `NODE_ENV` | `production` | Next.js core | Controls logging verbosity, caching, error detail exposure, and Next.js optimizations. | +| `OMNIROUTE_USE_TURBOPACK` | `1` (default in `.env.example`) | `package.json` / Next.js 16 | Toggles the Next.js 16 Turbopack bundler in `npm run dev` and `npm run build`. Set to `0` on Windows or when running into native binding incompatibilities. | +| `HOST` | `0.0.0.0` | `scripts/run-next.mjs` | Bind address for the Next.js dev/start server. Overrides the default `0.0.0.0` when set. | +| `HOSTNAME` | `127.0.0.1` | `scripts/run-next-playwright.mjs` | Bind address used by the Playwright runner when launching Next.js. Defaults to `127.0.0.1` for hermetic tests. | ### Port Modes @@ -190,25 +198,33 @@ OmniRoute provides a two-layer defense: request-side injection scanning and resp ## 6. Tool & Routing Policies -| Variable | Default | Source File | Description | -| ------------------ | ---------- | ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | -| `TOOL_POLICY_MODE` | `disabled` | `src/lib/toolPolicy.ts` | Controls LLM tool/function-calling access. `allowlist` = only listed tools, `denylist` = all except listed, `disabled` = no restrictions. | +| Variable | Default | Source File | Description | +| ----------------------------------- | ---------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | +| `TOOL_POLICY_MODE` | `disabled` | `src/lib/toolPolicy.ts` | Controls LLM tool/function-calling access. `allowlist` = only listed tools, `denylist` = all except listed, `disabled` = no restrictions. | +| `OMNIROUTE_PAYLOAD_RULES_PATH` | `./config/payloadRules.json` | `open-sse/services/payloadRules.ts` | Path to payload manipulation rules JSON file (per-model/protocol upstream tweaks). | +| `OMNIROUTE_PAYLOAD_RULES_RELOAD_MS` | `5000` | `open-sse/services/payloadRules.ts` | Reload interval (ms) for hot-reloading the payload rules file. Minimum `1000`. | --- ## 7. URLs & Cloud Sync -| Variable | Default | Source File | Description | -| --------------------------------------- | ------------------------ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | Server-side URL for internal sync jobs to call `/api/sync/cloud`. | -| `CLOUD_URL` | _(empty)_ | `src/lib/cloudSync.ts` | Cloud relay endpoint URL (premium feature). | -| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | HTTP timeout for cloud sync requests. | -| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links. **Must match your public URL behind reverse proxy.** | -| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. | -| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. | -| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | `open-sse/executors/chatgpt-web.ts` | Browser-facing OmniRoute origin used for image URLs in API responses (e.g., `/v1/chatgpt-web/image/`). 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_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/`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. | +| Variable | Default | Source File | Description | +| --------------------------------------- | --------------------------------------------------------------- | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `BASE_URL` | `http://localhost:20128` | `src/lib/cloudSync.ts` | Server-side URL for internal sync jobs to call `/api/sync/cloud`. | +| `CLOUD_URL` | _(empty)_ | `src/lib/cloudSync.ts` | Cloud relay endpoint URL (premium feature). | +| `CLOUD_SYNC_TIMEOUT_MS` | `12000` | `src/lib/cloudSync.ts` | HTTP timeout for cloud sync requests. | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:20128` | OAuth, Dashboard, sync | Public-facing URL for OAuth redirect_uri, Dashboard links. **Must match your public URL behind reverse proxy.** | +| `NEXT_PUBLIC_CLOUD_URL` | _(empty)_ | Client-side | Client-side mirror of `CLOUD_URL`. | +| `NEXT_PUBLIC_APP_URL` | _(unset)_ | `src/shared/services/cloudSyncScheduler.ts` | Legacy fallback for `NEXT_PUBLIC_BASE_URL`. | +| `OMNIROUTE_PUBLIC_BASE_URL` | _(unset)_ | `open-sse/executors/chatgpt-web.ts` | Browser-facing OmniRoute origin used for image URLs in API responses (e.g., `/v1/chatgpt-web/image/`). 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_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/`. Lower on memory-constrained hosts; raise if image generation is heavy and clients race the 30-minute TTL. | +| `KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public callback URL for asynchronous kie.ai jobs. Highest-priority override before `OMNIROUTE_KIE_CALLBACK_URL` and `OMNIROUTE_PUBLIC_URL`. | +| `OMNIROUTE_KIE_CALLBACK_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Alternate spelling of `KIE_CALLBACK_URL`. Falls back when the primary variable is unset. | +| `OMNIROUTE_PUBLIC_URL` | _(unset)_ | `open-sse/utils/kieTask.ts` | Public origin used to compose async callback URLs. Lowest-priority fallback for kie.ai callbacks; also used as a generic public URL for other relays. | +| `OMNIROUTE_CROF_USAGE_URL` | `https://crof.ai/usage_api/` | `open-sse/services/usage.ts` | CrofAI quota lookup endpoint used by the Usage page. Override for relays / test fixtures. | +| `OMNIROUTE_GEMINI_CLI_USAGE_URL` | `https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist` | `open-sse/services/usage.ts` | Gemini CLI quota lookup endpoint. Override for relays / test fixtures. | +| `OMNIROUTE_CODEWHISPERER_BASE_URL` | `https://codewhisperer.us-east-1.amazonaws.com` | `open-sse/services/usage.ts` | CodeWhisperer (AWS Kiro) usage limits endpoint. Override for relays / test fixtures. | > [!IMPORTANT] > When deploying behind a reverse proxy (nginx, Caddy), `NEXT_PUBLIC_BASE_URL` **must** be set to your public URL (e.g., `https://omniroute.example.com`). Without this, OAuth callbacks will fail because the redirect_uri won't match. @@ -257,6 +273,7 @@ Controls how OmniRoute discovers and launches CLI sidecars (Claude Code, Codex, | `CLI_CLINE_BIN` | `cline` | `src/shared/services/cliRuntime.ts` | Custom path to Cline CLI binary. | | `CLI_CONTINUE_BIN` | `cn` | `src/shared/services/cliRuntime.ts` | Custom path to Continue 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_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. | ### Docker Example @@ -273,19 +290,28 @@ CLI_CLAUDE_BIN=/host-cli/bin/claude ## 10. Internal Agent & MCP Integrations -| Variable | Default | Source File | Description | -| --------------------------------------- | ----------- | ------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | -| `OMNIROUTE_BASE_URL` | auto-detect | `open-sse/mcp-server/server.ts` | Explicit URL for MCP/A2A tools to reach OmniRoute. Overrides localhost auto-detection. | -| `OMNIROUTE_API_KEY` | _(unset)_ | MCP/A2A modules | API key for internal MCP tool and A2A skill calls. | -| `OMNIROUTE_API_KEY_ID` | _(unset)_ | `open-sse/mcp-server/audit.ts` | Key ID for MCP audit log attribution. | -| `ROUTER_API_KEY` | _(unset)_ | Legacy | Legacy alias for `OMNIROUTE_API_KEY`. | -| `OMNIROUTE_MCP_ENFORCE_SCOPES` | `false` | `open-sse/mcp-server/server.ts` | Enforce scope-based access control on MCP tool calls. | -| `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. | -| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. | -| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/server-init.ts` | Provider rate-limit and quota polling interval. | -| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | `false` | `src/instrumentation-node.ts` | Disable all background services (sync, pricing, model refresh). Useful for CI/test. | -| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. | -| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. | +| Variable | Default | Source File | Description | +| ----------------------------------------------- | ----------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | +| `OMNIROUTE_BASE_URL` | auto-detect | `open-sse/mcp-server/server.ts` | Explicit URL for MCP/A2A tools to reach OmniRoute. Overrides localhost auto-detection. | +| `OMNIROUTE_API_KEY` | _(unset)_ | MCP/A2A modules | API key for internal MCP tool and A2A skill calls. | +| `OMNIROUTE_API_KEY_ID` | _(unset)_ | `open-sse/mcp-server/audit.ts` | Key ID for MCP audit log attribution. | +| `ROUTER_API_KEY` | _(unset)_ | Legacy | Legacy alias for `OMNIROUTE_API_KEY`. | +| `OMNIROUTE_MCP_ENFORCE_SCOPES` | `false` | `open-sse/mcp-server/server.ts` | Enforce scope-based access control on MCP tool calls. | +| `OMNIROUTE_MCP_SCOPES` | _(all)_ | `open-sse/mcp-server/server.ts` | Comma-separated scopes: `admin`, `combos`, `health`, `models`, `routing`, `budget`, `metrics`, `pricing`, `memory`, `skills`. | +| `OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS` | enabled | `open-sse/mcp-server/descriptionCompressor.ts` | Compress MCP tool descriptions before serializing the manifest. Disable values: `0`, `false`, `off`. | +| `OMNIROUTE_MCP_DESCRIPTION_COMPRESSION` | `rtk` | `open-sse/mcp-server/descriptionCompressor.ts` | Compression algorithm/profile. Disable values: `0`, `false`, `off`. | +| `MODEL_SYNC_INTERVAL_HOURS` | `24` | `src/shared/services/modelSyncScheduler.ts` | Model catalog sync interval in hours. | +| `PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES` | `70` | `src/server-init.ts` | Provider rate-limit and quota polling interval. | +| `OMNIROUTE_DISABLE_BACKGROUND_SERVICES` | `false` | `src/instrumentation-node.ts` | Disable all background services (sync, pricing, model refresh). Useful for CI/test. | +| `OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS` | _(unset)_ | `src/lib/config/runtimeSettings.ts` | Force background tasks on under automated test detection. Set `1` to override the test heuristic. | +| `OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS` | `600000` | `src/lib/jobs/budgetResetJob.ts` | Budget reset check cadence (ms). Floor `10000`. | +| `OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS` | `1800000` | `src/lib/jobs/reasoningCacheCleanupJob.ts` | Reasoning cache cleanup cadence (ms). Floor `60000`. | +| `OMNIROUTE_CONFIG_HOT_RELOAD_MS` | `5000` | `src/lib/config/hotReload.ts` | Polling interval (ms) for config hot-reload. Lower than `1000` is rejected. | +| `OMNIROUTE_DISABLE_REDIS_AUTH_CACHE` | _(enabled)_ | `src/lib/db/apiKeys.ts` | Set `1` to bypass the Redis-backed API-key auth cache (forces DB reads). | +| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | `0` | `open-sse/services/compression/engines/rtk/filterLoader.ts` | Trust user-managed RTK project filter rules without strict signature checks. | +| `OMNIROUTE_BOOTSTRAPPED` | `false` | `src/app/(dashboard)/dashboard/page.tsx` | Set `true` by bootstrap script after initial setup. Controls setup wizard visibility. | +| `OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE` | `0` | `open-sse/executors/antigravity.ts` | Escape hatch: allow request body to override the Antigravity project field. | +| `ANTIGRAVITY_CREDITS` | _(unset)_ | `open-sse/services/antigravityCredits.ts` | Override Antigravity's advertised remaining credits (testing / forced values). | ### OAuth CLI Bridge (Internal) @@ -324,6 +350,9 @@ Built-in credentials for **localhost development**. For remote deployments, regi | `GITLAB_DUO_OAUTH_CLIENT_ID` | GitLab Duo (v3.8) | OAuth client ID for GitLab Duo. Register an app at `https://gitlab.com/-/profile/applications` with redirect URI `/callback` and scopes `api, read_user, openid, profile, email`. Falls back to `GITLAB_OAUTH_CLIENT_ID`. | | `GITLAB_DUO_OAUTH_CLIENT_SECRET` | GitLab Duo (v3.8) | OAuth client secret for GitLab Duo. Optional — PKCE flow does not require a secret. Falls back to `GITLAB_OAUTH_CLIENT_SECRET`. | | `GITLAB_DUO_BASE_URL` | GitLab Duo (v3.8) | Override GitLab base URL (self-hosted GitLab). Defaults to `https://gitlab.com`. Falls back to `GITLAB_BASE_URL`. | +| `GITLAB_BASE_URL` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_BASE_URL`. Used when the `_DUO_` variant is unset. | +| `GITLAB_OAUTH_CLIENT_ID` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_OAUTH_CLIENT_ID` consumed by `src/lib/oauth/constants/oauth.ts`. | +| `GITLAB_OAUTH_CLIENT_SECRET` | GitLab Duo (v3.8) | Legacy fallback for `GITLAB_DUO_OAUTH_CLIENT_SECRET` consumed by `src/lib/oauth/constants/oauth.ts`. | | `QODER_OAUTH_CLIENT_SECRET` | Qoder | — | | `QODER_OAUTH_AUTHORIZE_URL` | Qoder | Set to enable Qoder OAuth. | | `QODER_OAUTH_TOKEN_URL` | Qoder | — | @@ -379,24 +408,30 @@ When enabled, OmniRoute reorders HTTP headers and JSON body fields to match the ### Per-Provider -| Variable | Effect | -| -------------------------- | --------------------------------------- | -| `CLI_COMPAT_CODEX=1` | Mimics Codex CLI request signature | -| `CLI_COMPAT_CLAUDE=1` | Mimics Claude Code request signature | -| `CLI_COMPAT_GITHUB=1` | Mimics GitHub Copilot request signature | -| `CLI_COMPAT_ANTIGRAVITY=1` | Mimics Antigravity request signature | -| `CLI_COMPAT_KIRO=1` | Mimics Kiro IDE request signature | -| `CLI_COMPAT_CURSOR=1` | Mimics Cursor request signature | -| `CLI_COMPAT_KIMI_CODING=1` | Mimics Kimi Coding request signature | -| `CLI_COMPAT_KILOCODE=1` | Mimics Kilo Code request signature | -| `CLI_COMPAT_CLINE=1` | Mimics Cline request signature | -| `CLI_COMPAT_QWEN=1` | Mimics Qwen Code request signature | +| Variable | Activation | Effect | +| ------------------------ | ---------- | --------------------------------------- | +| `CLI_COMPAT_CODEX` | `=1` | Mimics Codex CLI request signature | +| `CLI_COMPAT_CLAUDE` | `=1` | Mimics Claude Code request signature | +| `CLI_COMPAT_GITHUB` | `=1` | Mimics GitHub Copilot request signature | +| `CLI_COMPAT_ANTIGRAVITY` | `=1` | Mimics Antigravity request signature | +| `CLI_COMPAT_CURSOR` | `=1` | Mimics Cursor request signature | +| `CLI_COMPAT_KIMI_CODING` | `=1` | Mimics Kimi Coding request signature | +| `CLI_COMPAT_KILOCODE` | `=1` | Mimics Kilo Code request signature | +| `CLI_COMPAT_CLINE` | `=1` | Mimics Cline request signature | +| `CLI_COMPAT_QWEN` | `=1` | Mimics Qwen Code request signature | ### Global -| Variable | Effect | -| ------------------ | --------------------------------------------------------------- | -| `CLI_COMPAT_ALL=1` | Enable fingerprint compatibility for **all** providers at once. | +| Variable | Activation | Effect | +| ---------------- | ---------- | --------------------------------------------------------------- | +| `CLI_COMPAT_ALL` | `=1` | Enable fingerprint compatibility for **all** providers at once. | + +### Kimi Coding CLI identity overrides + +| Variable | Default | Source File | Description | +| ----------------------- | -------------------- | ---------------------------------------- | ------------------------------------------------------------ | +| `KIMI_CLI_VERSION` | `1.36.0` | `src/lib/oauth/providers/kimi-coding.ts` | Override the Kimi CLI version sent during OAuth/API calls. | +| `KIMI_CODING_DEVICE_ID` | _(captured default)_ | `src/lib/oauth/providers/kimi-coding.ts` | Override the captured Kimi device ID used in client headers. | > [!NOTE] > This feature works alongside the User-Agent overrides (§12). The fingerprint system handles header ordering and body field ordering, while User-Agent overrides handle the specific UA string. Both can be enabled independently. @@ -411,20 +446,13 @@ Setting via environment variables is an alternative for Docker or headless deplo Recognized pattern: `{PROVIDER_ID}_API_KEY` -| Variable | Provider | -| -------------------- | ------------------- | -| `DEEPSEEK_API_KEY` | DeepSeek | -| `GROQ_API_KEY` | Groq | -| `XAI_API_KEY` | xAI (Grok) | -| `MISTRAL_API_KEY` | Mistral AI | -| `PERPLEXITY_API_KEY` | Perplexity | -| `TOGETHER_API_KEY` | Together AI | -| `FIREWORKS_API_KEY` | Fireworks AI | -| `CEREBRAS_API_KEY` | Cerebras | -| `COHERE_API_KEY` | Cohere | -| `NVIDIA_API_KEY` | NVIDIA NIM | -| `NEBIUS_API_KEY` | Nebius (embeddings) | -| `QIANFAN_API_KEY` | Baidu Qianfan | +| Variable | Provider | +| ------------------ | ---------- | +| `DEEPSEEK_API_KEY` | DeepSeek | +| `NVIDIA_API_KEY` | NVIDIA NIM | + +> [!NOTE] +> Static `${PROVIDER}_API_KEY` entries for Groq, xAI, Mistral, Perplexity, Together AI, Fireworks, Cerebras, Cohere, Nebius, and Qianfan were removed in v3.8.0 because the runtime no longer reads them — those providers rely exclusively on Dashboard / `data/provider-credentials.json` / the encrypted DB. See the _Audit: Removed / Dead Variables_ section at the bottom of this document for the migration path. > [!TIP] > Keys set via the Dashboard are stored encrypted in SQLite and take precedence over environment variables. @@ -469,6 +497,22 @@ REQUEST_TIMEOUT_MS (global override) | `API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS` | `5000` | Bridge keep-alive idle timeout. | | `API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS` | `0` | Raw socket timeout (0 = disabled). | | `SHUTDOWN_TIMEOUT_MS` | `30000` | Grace period on SIGTERM/SIGINT before force-exit. | +| `OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS` | `120000` | Fallback used by `src/shared/utils/fetchTimeout.ts` when `FETCH_TIMEOUT_MS` is unset. | +| `OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS` | `60000` | Wire-level timeout for the bogdanfinn/tls-client koffi binding (`chatgptTlsClient.ts`). | +| `OMNIROUTE_CHATGPT_TLS_GRACE_MS` | `10000` | JS-side grace added on top of the wire timeout when the native binding is wedged. | + +### Circuit Breaker Thresholds + +Provider-level circuit breaker tuning. Defaults reflect the scaled values used since v3.6 for 500+ connections. + +| Variable | Default | Source File | Description | +| --------------------------------------------- | ------- | ------------------------------ | --------------------------------------------------------------------------- | +| `OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD` | `8` | `open-sse/config/constants.ts` | Consecutive failure threshold for OAuth providers before the breaker trips. | +| `OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS` | `60000` | `open-sse/config/constants.ts` | Reset window (ms) for OAuth provider breaker. | +| `OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD` | `12` | `open-sse/config/constants.ts` | Consecutive failure threshold for API-key providers. | +| `OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS` | `30000` | `open-sse/config/constants.ts` | Reset window (ms) for API-key provider breaker. | +| `OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD` | `2` | `open-sse/config/constants.ts` | Consecutive failure threshold for local providers (Ollama, LM Studio, ...). | +| `OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS` | `15000` | `open-sse/config/constants.ts` | Reset window (ms) for local provider breaker. | ### Scenarios @@ -484,21 +528,27 @@ REQUEST_TIMEOUT_MS (global override) The logging system writes to both stdout and rotated log files. All configuration is read by `src/lib/logEnv.ts`. -| Variable | Default | Description | -| ----------------------------------------- | -------------------------- | -------------------------------------------------------------------------------- | -| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | -| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | -| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | -| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | -| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | -| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | -| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | -| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | -| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | -| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | -| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. | -| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. | -| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | +| Variable | Default | Description | +| ----------------------------------------- | -------------------------- | --------------------------------------------------------------------------------- | +| `APP_LOG_LEVEL` | `info` | Minimum log level: `debug`, `info`, `warn`, `error`. | +| `APP_LOG_FORMAT` | `text` | Output format: `text` (human-readable) or `json` (structured). | +| `APP_LOG_TO_FILE` | `true` | Write logs to file alongside stdout. | +| `APP_LOG_FILE_PATH` | `logs/application/app.log` | Log file path (relative to project root or `DATA_DIR`). | +| `APP_LOG_MAX_FILE_SIZE` | `50M` | Max file size before rotation. Accepts: `50M`, `1G`, `512K`, or plain bytes. | +| `APP_LOG_RETENTION_DAYS` | `7` | Days to keep rotated application log files. | +| `APP_LOG_MAX_FILES` | `20` | Maximum rotated log file backups. | +| `CALL_LOG_RETENTION_DAYS` | `7` | Days to keep request/call log entries in the database. | +| `CALL_LOG_MAX_ENTRIES` | `10000` | Max call log entries in the in-memory buffer. | +| `CALL_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `call_logs` SQLite table before pruning. | +| `CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS` | `true` | Store stream chunks in pipeline artifacts when `call_log_pipeline_enabled=true`. | +| `CALL_LOG_PIPELINE_MAX_SIZE_KB` | `512` | Max pipeline call log artifact size in KB when `call_log_pipeline_enabled=true`. | +| `PROXY_LOGS_TABLE_MAX_ROWS` | `100000` | Max rows in the `proxy_logs` SQLite table before pruning. | +| `APP_LOG_ROTATION_CHECK_INTERVAL_MS` | `60000` (1 min) | How often `src/lib/logRotation.ts` re-checks the active log file size. | +| `CHAT_LOG_TEXT_LIMIT` | `65536` | Max string length retained in chat log artifacts (default 64 KB). | +| `CHAT_LOG_ARRAY_TAIL_ITEMS` | `24` | Number of array items retained from the tail when truncating chat log payloads. | +| `CHAT_LOG_MAX_DEPTH` | `6` | Max nesting depth before chat log payloads are truncated. | +| `CHAT_LOG_MAX_OBJECT_KEYS` | `80` | Max object keys retained in chat log payloads (0 = unlimited). | +| `CHAT_DEBUG_FILE` | `false` | When true, `serializeArtifactForStorage` skips size-based truncation. Debug only. | --- @@ -558,20 +608,22 @@ Automatic model pricing data synchronization from external sources. ## 20. Provider-Specific Settings -| Variable | Default | Source File | Description | -| ----------------------------------------- | ------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------- | -| `OPENROUTER_CATALOG_TTL_MS` | `86400000` (24h) | `src/lib/catalog/openrouterCatalog.ts` | OpenRouter model catalog cache TTL. | -| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. | -| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. | -| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. | -| `CLOUDFLARED_BIN` | auto-detect | `src/lib/cloudflaredTunnel.ts` | Custom path to `cloudflared` binary. | -| `SEARCH_CACHE_TTL_MS` | `300000` (5 min) | `open-sse/services/searchCache.ts` | TTL for search API (Perplexity, Brave, etc.) response caching. | -| `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | `false` | `src/app/api/providers/route.ts` | Allow multiple simultaneous connections per OpenAI-compatible provider. | -| `ENABLE_CC_COMPATIBLE_PROVIDER` | `false` | `src/shared/utils/featureFlags.ts` | Reveal the experimental CC-compatible provider UI for Claude Code-only relays. | -| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). | -| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. | -| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | -| `LOCAL_HOSTNAMES` | _(empty)_ | `open-sse/config/providerRegistry.ts` | Comma-separated additional hostnames treated as "local" (Docker service names, etc.). | +| Variable | Default | Source File | Description | +| ----------------------------------------- | ------------------ | --------------------------------------------------------------------- | ------------------------------------------------------------------------------------- | +| `OPENROUTER_CATALOG_TTL_MS` | `86400000` (24h) | `src/lib/catalog/openrouterCatalog.ts` | OpenRouter model catalog cache TTL. | +| `NANOBANANA_POLL_TIMEOUT_MS` | `120000` | `open-sse/handlers/imageGeneration.ts` | Max wait for NanoBanana image generation jobs. | +| `NANOBANANA_POLL_INTERVAL_MS` | `2500` | `open-sse/handlers/imageGeneration.ts` | NanoBanana job polling frequency. | +| `AWS_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Region used to construct AWS Bedrock endpoints (Kiro, audio). | +| `AWS_DEFAULT_REGION` | _(unset)_ | `src/lib/providers/validation.ts`, `open-sse/handlers/audioSpeech.ts` | Fallback when `AWS_REGION` is not set. | +| `CLOUDFLARE_ACCOUNT_ID` | _(unset)_ | `open-sse/executors/cloudflare-ai.ts` | Account ID for Cloudflare Workers AI. | +| `CLOUDFLARED_BIN` | auto-detect | `src/lib/cloudflaredTunnel.ts` | Custom path to `cloudflared` binary. | +| `SEARCH_CACHE_TTL_MS` | `300000` (5 min) | `open-sse/services/searchCache.ts` | TTL for search API (Perplexity, Brave, etc.) response caching. | +| `ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE` | `false` | `src/app/api/providers/route.ts` | Allow multiple simultaneous connections per OpenAI-compatible provider. | +| `ENABLE_CC_COMPATIBLE_PROVIDER` | `false` | `src/shared/utils/featureFlags.ts` | Reveal the experimental CC-compatible provider UI for Claude Code-only relays. | +| `CLIPROXYAPI_HOST` | `127.0.0.1` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge host (legacy integration). | +| `CLIPROXYAPI_PORT` | `5544` | `open-sse/executors/cliproxyapi.ts` | CLIProxyAPI bridge port. | +| `CLIPROXYAPI_CONFIG_DIR` | `~/.cli-proxy-api` | `src/lib/versionManager/processManager.ts` | CLIProxyAPI config directory. | +| `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 exclusively. OmniRoute rewrites requests so those relays accept them. If you only want to use @@ -582,14 +634,15 @@ Anthropic-compatible provider instead. ## 21. Proxy Health -| Variable | Default | Source File | Description | -| ---------------------------- | ---------------- | ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `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. | -| `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` | `false` | `open-sse/services/rateLimitManager.ts` | When `true`/`1`, automatically engages the rate-limit manager on first observed upstream 429 (without requiring manual toggle). Accepts `true`/`1`/`yes`. | -| `REQUEST_RETRY` | `2` | `src/sse/services/cooldownAwareRetry.ts` | Number of automatic retries on model-scoped cooldown responses before returning error to client. | -| `MAX_RETRY_INTERVAL_SEC` | `30` | `src/sse/services/cooldownAwareRetry.ts` | Max backoff interval (seconds) between cooldown retries. Capped by this value regardless of upstream `Retry-After`. | +| Variable | Default | Source File | Description | +| ---------------------------- | ---------------- | ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `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. | +| `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. | +| `HEALTHCHECK_STAGGER_MS` | `3000` | `src/lib/tokenHealthCheck.ts` | Stagger interval (ms) between provider token healthchecks at startup. | +| `REQUEST_RETRY` | `2` | `src/sse/services/cooldownAwareRetry.ts` | Number of automatic retries on model-scoped cooldown responses before returning error to client. | +| `MAX_RETRY_INTERVAL_SEC` | `30` | `src/sse/services/cooldownAwareRetry.ts` | Max backoff interval (seconds) between cooldown retries. Capped by this value regardless of upstream `Retry-After`. | --- @@ -598,12 +651,17 @@ Anthropic-compatible provider instead. > [!CAUTION] > These variables produce **verbose output** and may leak sensitive data. **Never enable in production.** -| Variable | Default | Source File | Description | -| -------------------------------- | --------- | ----------------------------------------- | -------------------------------------------------------------- | -| `CURSOR_PROTOBUF_DEBUG` | _(unset)_ | `open-sse/utils/cursorProtobuf.ts` | Set `1` to dump Cursor protobuf decode/encode details. | -| `CURSOR_STREAM_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Set `1` to dump raw Cursor SSE stream data. | -| `DEBUG_RESPONSES_SSE_TO_JSON` | _(unset)_ | `open-sse/handlers/responseTranslator.ts` | Set `true` to log Responses API SSE→JSON translation details. | -| `NEXT_PUBLIC_OMNIROUTE_E2E_MODE` | _(unset)_ | E2E test harness | Set `true` to enable E2E test mode (relaxed auth, test hooks). | +| Variable | Default | Source File | Description | +| -------------------------------- | ------------------- | ------------------------------------------ | --------------------------------------------------------------------------------- | +| `CURSOR_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Set `1` to enable verbose Cursor executor logs (decoded SSE chunks, etc.). | +| `CURSOR_STREAM_DEBUG` | _(unset)_ | `open-sse/executors/cursor.ts` | Backward-compatible alias of `CURSOR_DEBUG`. | +| `CURSOR_DUMP_FILE` | _(unset)_ | `open-sse/executors/cursor.ts` | Optional file path that receives raw decoded Cursor chunks when `CURSOR_DEBUG=1`. | +| `CURSOR_STREAM_TIMEOUT_MS` | `300000` | `open-sse/executors/cursor.ts` | Stream idle timeout (ms) for the Cursor executor. | +| `CURSOR_STATE_DB_PATH` | _(probed)_ | `open-sse/utils/cursorVersionDetector.ts` | Override the Cursor state DB lookup used for version detection. | +| `CURSOR_TOKEN` | _(unset)_ | `scripts/cursor-tap.cjs` | Direct Cursor bearer token used by developer tooling. | +| `OMNIROUTE_LOG_REQUEST_SHAPE` | enabled (`!== "0"`) | `src/app/api/v1/chat/completions/route.ts` | Log content-type/length markers for large chat payloads. Set `"0"` to silence. | +| `DEBUG_RESPONSES_SSE_TO_JSON` | _(unset)_ | `open-sse/handlers/responseTranslator.ts` | Set `true` to log Responses API SSE→JSON translation details. | +| `NEXT_PUBLIC_OMNIROUTE_E2E_MODE` | _(unset)_ | E2E test harness | Set `true` to enable E2E test mode (relaxed auth, test hooks). | --- @@ -611,10 +669,11 @@ Anthropic-compatible provider instead. Allow users to report issues directly from the Dashboard. -| Variable | Default | Source File | Description | -| --------------------- | --------- | --------------------------------------- | ------------------------------------------------------- | -| `GITHUB_ISSUES_REPO` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | Repository in `owner/repo` format. | -| `GITHUB_ISSUES_TOKEN` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | GitHub Personal Access Token with `issues:write` scope. | +| Variable | Default | Source File | Description | +| --------------------- | --------- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| `GITHUB_ISSUES_REPO` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | Repository in `owner/repo` format. | +| `GITHUB_ISSUES_TOKEN` | _(unset)_ | `src/app/api/v1/issues/report/route.ts` | GitHub Personal Access Token with `issues:write` scope. | +| `GITHUB_TOKEN` | _(unset)_ | issue triage / cloud agent helpers | Generic GitHub access token used as fallback for `GITHUB_ISSUES_TOKEN` and consumed by cloud agent helpers in `src/lib/cloudAgent/*`. | --- @@ -698,19 +757,77 @@ Limits and safety knobs applied when the Skills framework (`src/lib/skills/`) ex --- +## 25. Provider Quotas, Tunnels, Backups & Misc Runtime + +Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug proxy), the 1Proxy egress pool, database backups and small per-feature overrides referenced by the executor layer or scripts. + +| Variable | Default | Source File | Description | +| -------------------------------- | ------------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------- | +| `REDIS_URL` | `redis://localhost:6379` | `src/shared/utils/rateLimiter.ts` | Redis connection string for the rate limiter backend. | +| `ALIBABA_CODING_PLAN_HOST` | _(production host)_ | `open-sse/services/bailianQuotaFetcher.ts` | Override the host used to fetch Alibaba Bailian coding-plan quotas. | +| `ALIBABA_CODING_PLAN_QUOTA_URL` | derived from host | `open-sse/services/bailianQuotaFetcher.ts` | Full quota URL override for Alibaba Bailian. | +| `CONTEXT_RESERVE_TOKENS` | `1024` | `open-sse/services/contextManager.ts` | Tokens reserved for completion output when computing prompt budgets. | +| `MODEL_ALIAS_COMPAT_ENABLED` | enabled | `open-sse/services/model.ts` | Toggle the legacy model-alias compatibility layer used by older clients. | +| `COMMAND_CODE_CALLBACK_PORT` | _(unset)_ | `src/app/api/providers/command-code/auth/shared.ts` | Local port used for OAuth-style callbacks from the Command Code CLI helper. | +| `MITM_LOCAL_PORT` | `443` | `src/mitm/server.cjs` | Local bind port for the MITM debug proxy. | +| `MITM_DISABLE_TLS_VERIFY` | `0` | `src/mitm/server.cjs` | Set `1` to disable upstream TLS verification (development only). | +| `ONEPROXY_ENABLED` | `true` | `src/lib/oneproxySync.ts` | Enable the 1Proxy egress pool sync. | +| `ONEPROXY_API_URL` | `https://1proxy-api.aitradepulse.com` | `src/lib/oneproxySync.ts` | 1Proxy service API URL override. | +| `ONEPROXY_MAX_PROXIES` | `500` | `src/lib/oneproxySync.ts` | Maximum proxies imported per sync. | +| `ONEPROXY_MIN_QUALITY_THRESHOLD` | `50` | `src/lib/oneproxySync.ts` | Minimum quality score for imported proxies. | +| `TAILSCALE_BIN` | _(auto-detect)_ | `src/lib/tailscaleTunnel.ts` | Explicit path to the `tailscale` binary. | +| `TAILSCALED_BIN` | _(auto-detect)_ | `src/lib/tailscaleTunnel.ts` | Explicit path to the `tailscaled` daemon binary. | +| `NGROK_AUTHTOKEN` | _(unset)_ | `src/lib/ngrokTunnel.ts` | Authenticates outbound ngrok tunnels. | +| `DB_BACKUP_MAX_FILES` | `20` | `src/lib/db/backup.ts` | Maximum SQLite backup files retained on disk. | +| `DB_BACKUP_RETENTION_DAYS` | `0` | `src/lib/db/backup.ts` | Maximum age (days) of retained backups. `0` disables age-based pruning. | +| `OMNIROUTE_TLS_PROXY_URL` | _(unset)_ | `open-sse/services/chatgptTlsClient.ts` | Override the TLS sidecar URL for tests. Production should leave unset. | + +--- + +## 26. Test & E2E Harness + +Used by `scripts/run-next-playwright.mjs`, `scripts/smoke-electron-packaged.mjs`, +`scripts/run-ecosystem-tests.mjs`, and `scripts/uninstall.mjs`. Leave every +value below unset in production deployments. + +| Variable | Default | Source File | Description | +| ------------------------------------- | -------------------------------- | ------------------------------------- | ---------------------------------------------------------------------------------------- | +| `OMNIROUTE_E2E_BOOTSTRAP_MODE` | `auth` | `scripts/run-next-playwright.mjs` | E2E bootstrap mode (`auth`, `fresh`, `reuse`) for the Playwright runner. | +| `OMNIROUTE_E2E_PASSWORD` | falls back to `INITIAL_PASSWORD` | `scripts/run-next-playwright.mjs` | Admin password injected into the Playwright environment. | +| `OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK` | `true` | `scripts/run-next-playwright.mjs` | Disable the local healthcheck poll during Playwright runs. | +| `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | `true` | `scripts/run-next-playwright.mjs` | Disable the OAuth token healthcheck loop during tests. | +| `OMNIROUTE_HIDE_HEALTHCHECK_LOGS` | `true` | `scripts/run-next-playwright.mjs` | Silence healthcheck noise in Playwright stdout. | +| `OMNIROUTE_PLAYWRIGHT_SKIP_BUILD` | `0` | `scripts/run-next-playwright.mjs` | Skip the Next.js production build before Playwright starts (CI optimization). | +| `OMNIROUTE_SKIP_UNINSTALL_HOOK` | `0` | `scripts/uninstall.mjs` | Skip the OmniRoute uninstall hook (used by CI to keep `node_modules` intact). | +| `ECOSYSTEM_SERVER_WAIT_MS` | `180000` | `scripts/run-ecosystem-tests.mjs` | Wait time (ms) for the server to become healthy before running ecosystem/protocol tests. | +| `ELECTRON_SMOKE_URL` | `http://127.0.0.1:20128/login` | `scripts/smoke-electron-packaged.mjs` | URL the Electron smoke harness expects the packaged app to serve. | +| `ELECTRON_SMOKE_TIMEOUT_MS` | `45000` | `scripts/smoke-electron-packaged.mjs` | Total timeout (ms) before the smoke harness gives up. | +| `ELECTRON_SMOKE_SETTLE_MS` | `2000` | `scripts/smoke-electron-packaged.mjs` | Settle window (ms) after the page loads. | +| `ELECTRON_SMOKE_APP_EXECUTABLE` | _(auto)_ | `scripts/smoke-electron-packaged.mjs` | Explicit path to the packaged Electron executable. | +| `ELECTRON_SMOKE_DATA_DIR` | _(tmpdir)_ | `scripts/smoke-electron-packaged.mjs` | Data directory for the Electron smoke run. | +| `ELECTRON_SMOKE_KEEP_DATA` | `0` | `scripts/smoke-electron-packaged.mjs` | Set `1` to preserve the smoke data directory after the run. | +| `ELECTRON_SMOKE_STREAM_LOGS` | `0` | `scripts/smoke-electron-packaged.mjs` | Set `1` to stream Electron logs to stdout during the run. | +| `CLI_DEVIN_BIN` | _(PATH lookup)_ | `open-sse/executors/devin-cli.ts` | Override the Devin CLI binary path. | + +--- + ## Audit: Removed / Dead Variables The following variables appeared in previous versions of `.env.example` but have **no runtime references** in the current codebase. They have been removed: -| Variable | Reason | -| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -| `STORAGE_DRIVER=sqlite` | Never read by any source file. SQLite is the only supported driver — no selection needed. | -| `INSTANCE_NAME=omniroute` | Present in old docs/env templates but unused at runtime. May return in a future multi-instance feature. | -| `SQLITE_MAX_SIZE_MB=2048` | Not referenced in source code. Database size is not artificially limited. | -| `SQLITE_CLEAN_LEGACY_FILES=true` | Not referenced in source code. Legacy cleanup was likely removed. | -| `CLI_ROO_BIN` | Not registered in `src/shared/services/cliRuntime.ts`. | -| `CLI_KIMI_CODING_BIN` | Not registered in `src/shared/services/cliRuntime.ts` (Kimi Coding uses OAuth, not a CLI binary). | -| `IFLOW_OAUTH_CLIENT_ID` / `IFLOW_OAUTH_CLIENT_SECRET` | Not referenced anywhere in source code. | +| Variable | Reason | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | +| `STORAGE_DRIVER=sqlite` | Never read by any source file. SQLite is the only supported driver — no selection needed. | +| `INSTANCE_NAME=omniroute` | Present in old docs/env templates but unused at runtime. May return in a future multi-instance feature. | +| `SQLITE_MAX_SIZE_MB=2048` | Not referenced in source code. Database size is not artificially limited. | +| `SQLITE_CLEAN_LEGACY_FILES=true` | Not referenced in source code. Legacy cleanup was likely removed. | +| `CLI_ROO_BIN` | Not registered in `src/shared/services/cliRuntime.ts`. | +| `CLI_KIMI_CODING_BIN` | Not registered in `src/shared/services/cliRuntime.ts` (Kimi Coding uses OAuth, not a CLI binary). | +| `IFLOW_OAUTH_CLIENT_ID` / `IFLOW_OAUTH_CLIENT_SECRET` | Not referenced anywhere in source code. | +| `CEREBRAS_API_KEY` / `COHERE_API_KEY` / `FIREWORKS_API_KEY` / `GROQ_API_KEY` / `MISTRAL_API_KEY` / `NEBIUS_API_KEY` / `PERPLEXITY_API_KEY` / `TOGETHER_API_KEY` / `XAI_API_KEY` | Removed in v3.8.0. The runtime no longer reads these env vars — credentials come from Dashboard / `data/provider-credentials.json` / encrypted DB. | +| `CURSOR_PROTOBUF_DEBUG` | Removed in v3.8.0. Cursor executor uses `CURSOR_DEBUG` / `CURSOR_STREAM_DEBUG` (see §22). | +| `CLI_COMPAT_KIRO` | Removed in v3.8.0. Kiro is in `CLI_COMPAT_OMITTED_PROVIDER_IDS` — its toggle has no effect. | +| `QIANFAN_API_KEY` | Removed alongside other unused provider API key stubs in v3.8.0. | ### Default Value Corrections diff --git a/open-sse/config/constants.ts b/open-sse/config/constants.ts index b55085526d..21c1adfc80 100644 --- a/open-sse/config/constants.ts +++ b/open-sse/config/constants.ts @@ -142,13 +142,23 @@ export const RateLimitReason = { // ─── Provider Resilience Profiles ─────────────────────────────────────────── // Separate behavior for OAuth (low-limit, session-based) vs API Key (high-limit, metered) +// Circuit-breaker thresholds and reset windows are overridable via +// OMNIROUTE_CIRCUIT_BREAKER_* env vars so operators can dampen or harden +// behavior without recompiling. +function envInt(name: string, fallback: number): number { + const raw = process.env[name]; + if (raw === undefined || raw === null || raw === "") return fallback; + const parsed = Number(raw); + return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback; +} + export const PROVIDER_PROFILES = { oauth: { transientCooldown: 5000, // 5s (session tokens — short recovery) rateLimitCooldown: 60000, // 60s default when no retry-after header maxBackoffLevel: 8, // Higher ceiling (sessions may stay bad longer) - circuitBreakerThreshold: 8, // Scaled for 500+ connections (was 3) - circuitBreakerReset: 60000, // 1min reset + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD", 8), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS", 60000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 10, // Scaled for 500+ connections (was 3) providerFailureWindowMs: 900000, // 15min window (was 10min) @@ -158,8 +168,8 @@ export const PROVIDER_PROFILES = { transientCooldown: 3000, // 3s (API providers recover faster) rateLimitCooldown: 0, // 0 = respect retry-after header from provider maxBackoffLevel: 5, // Lower ceiling (API quotas reset at known intervals) - circuitBreakerThreshold: 12, // Scaled for 500+ connections (was 5) - circuitBreakerReset: 30000, // 30s reset + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD", 12), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS", 30000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 15, // Scaled for 500+ connections (was 5) providerFailureWindowMs: 1800000, // 30min window (was 20min) @@ -172,8 +182,8 @@ export const PROVIDER_PROFILES = { transientCooldown: 2000, // 2s (local — very fast recovery) rateLimitCooldown: 5000, // 5s (local — no real rate limits) maxBackoffLevel: 3, // Low ceiling (local either works or doesn't) - circuitBreakerThreshold: 2, // Opens fast (if local is down, it's down) - circuitBreakerReset: 15000, // 15s reset (check again quickly) + circuitBreakerThreshold: envInt("OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD", 2), + circuitBreakerReset: envInt("OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS", 15000), // Provider-level circuit breaker (entire provider cooldown after repeated failures) providerFailureThreshold: 2, // 2 failures trigger provider cooldown providerFailureWindowMs: 300000, // 5min window for counting failures diff --git a/open-sse/services/chatgptTlsClient.ts b/open-sse/services/chatgptTlsClient.ts index 4e21f18da4..b5c3fdbf66 100644 --- a/open-sse/services/chatgptTlsClient.ts +++ b/open-sse/services/chatgptTlsClient.ts @@ -21,14 +21,16 @@ let clientPromise: Promise | null = null; let exitHookInstalled = false; const CHATGPT_PROFILE = "firefox_148"; // matches the Firefox 148 UA we send -const DEFAULT_TIMEOUT_MS = 60_000; +const DEFAULT_TIMEOUT_MS = + Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS || "", 10) || 60_000; // Grace period added to the binding's wire-level timeout before our JS-level // hard timeout fires. Under healthy operation `tls-client-node` honors // `timeoutMilliseconds` and rejects on its own; the JS-level race only wins // when the koffi-loaded native library is wedged (which the binding's own // timer can't escape). Keep the grace small so users don't wait noticeably // longer than the configured timeout when the binding is dead. -const HARD_TIMEOUT_GRACE_MS = 10_000; +const HARD_TIMEOUT_GRACE_MS = + Number.parseInt(process.env.OMNIROUTE_CHATGPT_TLS_GRACE_MS || "", 10) || 10_000; function installExitHook(): void { if (exitHookInstalled) return; diff --git a/open-sse/services/usage.ts b/open-sse/services/usage.ts index 007f77f341..21d85889f6 100644 --- a/open-sse/services/usage.ts +++ b/open-sse/services/usage.ts @@ -3,6 +3,14 @@ */ import { PROVIDERS } from "../config/constants.ts"; + +// Quota / usage upstream URLs (overridable for testing or relays). +const CROF_USAGE_URL = process.env.OMNIROUTE_CROF_USAGE_URL ?? "https://crof.ai/usage_api/"; +const GEMINI_CLI_USAGE_URL = + process.env.OMNIROUTE_GEMINI_CLI_USAGE_URL ?? + "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"; +const CODEWHISPERER_BASE_URL = + process.env.OMNIROUTE_CODEWHISPERER_BASE_URL ?? "https://codewhisperer.us-east-1.amazonaws.com"; import { getAntigravityFetchAvailableModelsUrls, ANTIGRAVITY_BASE_URLS, @@ -482,7 +490,7 @@ async function getCrofUsage(apiKey: string) { let response: Response; try { - response = await fetch("https://crof.ai/usage_api/", { + response = await fetch(CROF_USAGE_URL, { method: "GET", headers: { Authorization: `Bearer ${apiKey}`, @@ -1400,7 +1408,7 @@ async function getGeminiCliSubscriptionInfoCached(accessToken: string): Promise< */ async function getGeminiCliSubscriptionInfo(accessToken: string): Promise { try { - const response = await fetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", { + const response = await fetch(GEMINI_CLI_USAGE_URL, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, @@ -2212,7 +2220,7 @@ async function getKiroUsage(accessToken?: string, providerSpecificData?: JsonRec resourceType: "AGENTIC_REQUEST", }; - const response = await fetch("https://codewhisperer.us-east-1.amazonaws.com", { + const response = await fetch(CODEWHISPERER_BASE_URL, { method: "POST", headers: { Authorization: `Bearer ${accessToken}`, diff --git a/scripts/check/check-env-doc-sync.mjs b/scripts/check/check-env-doc-sync.mjs index eeb9db8114..f68fea2eb7 100644 --- a/scripts/check/check-env-doc-sync.mjs +++ b/scripts/check/check-env-doc-sync.mjs @@ -1,10 +1,27 @@ #!/usr/bin/env node -// Validates that env vars referenced in code appear in .env.example AND in docs/ENVIRONMENT.md. -// Exits 0 on success, 1 on missing entries. Designed for use in pre-commit / CI. -// -// Run: node scripts/check/check-env-doc-sync.mjs -// Strict mode: node scripts/check/check-env-doc-sync.mjs --strict -// In strict mode, missing entries cause failure. In default mode, only summary is printed. +/** + * Strict environment variable contract checker. + * + * Enforces that every env var referenced in OmniRoute source code appears in + * both `.env.example` and `docs/ENVIRONMENT.md`, and that the two files agree + * on the documented var set. Falls back to a small allowlist for variables + * that are intentionally documented but not literally referenced (legacy + * aliases, future-supported hooks) or vice versa. + * + * Usage: + * node scripts/check/check-env-doc-sync.mjs # strict (CI mode) + * node scripts/check/check-env-doc-sync.mjs --lenient # legacy report-only mode + * + * Strict mode exits non-zero if any of these are non-empty: + * - vars in code but missing from .env.example + * - vars in .env.example but missing from ENVIRONMENT.md + * - vars in ENVIRONMENT.md but missing from .env.example + * + * Programmatic API: + * Other Node tests can `import { runEnvDocSync } from "./check-env-doc-sync.mjs"` + * and pass `{ root, envExample, envDoc, codeVars, ignore, docOnlyAllowlist, + * envOnlyAllowlist }` to drive the checker against fixtures. + */ import fs from "node:fs"; import path from "node:path"; @@ -12,14 +29,12 @@ import { fileURLToPath } from "node:url"; import { execSync } from "node:child_process"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const ROOT = path.resolve(__dirname, "..", ".."); -const ENV_EXAMPLE = path.join(ROOT, ".env.example"); -const ENV_DOC = path.join(ROOT, "docs", "ENVIRONMENT.md"); +const REPO_ROOT = path.resolve(__dirname, "..", ".."); -const STRICT = process.argv.includes("--strict"); - -// Vars that are intentionally not documented or detected in code via dynamic patterns. -const IGNORE = new Set([ +// ─── Allowlists ──────────────────────────────────────────────────────────── +// Env vars referenced in code that should NOT trigger documentation drift. +// These are usually system/process vars or harness-only knobs. +const IGNORE_FROM_CODE = new Set([ "NODE_ENV", "PATH", "HOME", @@ -33,99 +48,230 @@ const IGNORE = new Set([ "CI", "GITHUB_ACTIONS", "RUNNER_OS", - // Add false positives here as discovered. + // OS / Node internals frequently surfaced by indirect dependencies. + "APPDATA", + "LOCALAPPDATA", + "XDG_CONFIG_HOME", + "USERPROFILE", + "PREFIX", + // Next.js / Node test runners — these are framework-managed. + "NEXT_DIST_DIR", + "NEXT_PHASE", + "NEXT_RUNTIME", + "VITEST", + // CI providers (set by the runner). + "GITHUB_BASE_REF", + "GITHUB_BASE_SHA", + // Aliases for documented vars handled via fallback ordering. + "API_KEY", + "APP_URL", + "PUBLIC_URL", + "ANTHROPIC_API_URL", + "OPENAI_API_URL", + "LOG_LEVEL", + // Internal QA helpers used only by scripts/ and Playwright. + "QA_BASE_URL", + "QA_LOCALES", + "QA_REPORT_SUFFIX", + "QA_ROUTES", + // Doctor diagnostic flags (no runtime behavior yet — placeholders). + "OMNIROUTE_DOCTOR_HOST", + "OMNIROUTE_DOCTOR_LIVENESS_URL", + "OMNIROUTE_PROVIDER_CATALOG_PATH", + "OMNIROUTE_PROVIDER_TEST_MODEL", + // Source typo / placeholder. + "OMNIROUT", + // Static config alias path (the canonical var is OMNIROUTE_PAYLOAD_RULES_PATH). + "PAYLOAD_RULES_PATH", ]); -function readEnvExampleVars() { - if (!fs.existsSync(ENV_EXAMPLE)) { - console.error(`✗ ${ENV_EXAMPLE} not found`); - process.exit(2); - } - const txt = fs.readFileSync(ENV_EXAMPLE, "utf8"); +// Vars documented in ENVIRONMENT.md but intentionally absent from .env.example. +// Used for past-tense documentation (Audit / Dead vars section), legacy aliases +// with no runtime hook, and section anchors that look like vars to the regex. +const DOC_ONLY_ALLOWLIST = new Set([ + // Audit history (Removed / Dead Variables section). + "CEREBRAS_API_KEY", + "COHERE_API_KEY", + "FIREWORKS_API_KEY", + "GROQ_API_KEY", + "MISTRAL_API_KEY", + "NEBIUS_API_KEY", + "PERPLEXITY_API_KEY", + "TOGETHER_API_KEY", + "XAI_API_KEY", + "QIANFAN_API_KEY", + "CURSOR_PROTOBUF_DEBUG", + "CLI_COMPAT_KIRO", + "CLI_KIMI_CODING_BIN", + "CLI_ROO_BIN", + "IFLOW_OAUTH_CLIENT_ID", + "IFLOW_OAUTH_CLIENT_SECRET", + // Source-code constants accidentally captured by the doc regex. + "CLI_COMPAT_OMITTED_PROVIDER_IDS", + // Sample default values that look like SHOUTY_NAMES (not env vars). + "CHANGEME", + // Legacy aliases — present in docs as "would be aliases" but read-only + // through their canonical names today. + "OMNIROUTE_CRYPT_KEY", + "OMNIROUTE_API_KEY_BASE64", + // Future-supported hooks: documented but currently hardcoded constants. + "MAX_RETRY_INTERVAL_SEC", + "REQUEST_RETRY", + "SKILLS_EXECUTION_TIMEOUT_MS", + "SKILLS_SANDBOX_DOCKER_IMAGE", +]); + +// Vars present in .env.example but intentionally absent from ENVIRONMENT.md. +// Empty today — kept for forward compatibility / explicit exemption. +const ENV_ONLY_ALLOWLIST = new Set([]); + +// ─── Parsing helpers ─────────────────────────────────────────────────────── + +/** + * Extract VAR= entries from a `.env`-style file (handles commented examples). + */ +export function parseEnvExampleVars(text) { const vars = new Set(); - for (const line of txt.split("\n")) { - // Match both "VAR=value" and "# VAR=value" (commented-out examples are still documented) + for (const line of String(text ?? "").split("\n")) { const m = line.match(/^#?\s*([A-Z][A-Z0-9_]+)\s*=/); if (m) vars.add(m[1]); } return vars; } -function readEnvDocVars() { - if (!fs.existsSync(ENV_DOC)) { - console.error(`✗ ${ENV_DOC} not found`); - process.exit(2); - } - const txt = fs.readFileSync(ENV_DOC, "utf8"); +/** + * Extract `VARNAME` tokens from a markdown doc — matches anything in backticks + * that looks like an env var (uppercase + digit + underscore). + */ +export function parseEnvDocVars(text) { const vars = new Set(); - // Match `VAR_NAME` in inline code or table cells. - for (const m of txt.matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) { + for (const m of String(text ?? "").matchAll(/`([A-Z][A-Z0-9_]{2,})`/g)) { vars.add(m[1]); } return vars; } -function readCodeVars() { +/** + * Collect environment variable references in source code via grep against + * the `process.env` member access pattern. + */ +function scanCodeVars({ cwd } = {}) { + const repoRoot = cwd ?? REPO_ROOT; + const stdout = execSync( + "grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' " + + "src/ open-sse/ bin/ scripts/ electron/main.js electron/preload.js 2>/dev/null || true", + { cwd: repoRoot, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 } + ); const vars = new Set(); - let stdout; - try { - stdout = execSync( - "grep -rhoE 'process\\.env\\.[A-Z][A-Z0-9_]+' src/ open-sse/ bin/ scripts/ 2>/dev/null || true", - { cwd: ROOT, encoding: "utf8", maxBuffer: 20 * 1024 * 1024 } - ); - } catch (e) { - console.error(`✗ grep failed: ${e.message}`); - process.exit(2); - } for (const line of stdout.split("\n")) { const m = line.match(/^process\.env\.([A-Z][A-Z0-9_]+)$/); - if (m && !IGNORE.has(m[1])) vars.add(m[1]); + if (m) vars.add(m[1]); } return vars; } +/** + * Diff helper. + */ function diff(set, against) { return [...set].filter((v) => !against.has(v)).sort(); } -function main() { - const codeVars = readCodeVars(); - const exampleVars = readEnvExampleVars(); - const docVars = readEnvDocVars(); +// ─── Programmatic entry point ────────────────────────────────────────────── - const inCodeMissingExample = diff(codeVars, exampleVars); - const inCodeMissingDoc = diff(codeVars, docVars); - const inExampleMissingDoc = diff(exampleVars, docVars); - const inExampleMissingCode = diff(exampleVars, codeVars); +/** + * Run the contract checker. All inputs are overridable for tests. + * + * Returns `{ ok: boolean, summary, problems: { codeMissingEnv, envMissingDoc, + * docMissingEnv } }`. + */ +export function runEnvDocSync(options = {}) { + const ignore = options.ignore ?? IGNORE_FROM_CODE; + const docOnly = options.docOnlyAllowlist ?? DOC_ONLY_ALLOWLIST; + const envOnly = options.envOnlyAllowlist ?? ENV_ONLY_ALLOWLIST; - console.log("Env var sync report"); - console.log("==================="); - console.log(`Code references: ${codeVars.size} unique vars`); - console.log(`In .env.example: ${exampleVars.size} unique vars`); - console.log(`In docs/ENVIRONMENT.md: ${docVars.size} unique vars (heuristic)`); - console.log(); + const envExampleText = + options.envExampleText ?? + (options.envExamplePath + ? fs.readFileSync(options.envExamplePath, "utf8") + : fs.readFileSync(path.join(REPO_ROOT, ".env.example"), "utf8")); + const envDocText = + options.envDocText ?? + (options.envDocPath + ? fs.readFileSync(options.envDocPath, "utf8") + : fs.readFileSync(path.join(REPO_ROOT, "docs", "ENVIRONMENT.md"), "utf8")); - function printList(label, list) { - if (list.length === 0) { - console.log(` ✓ ${label}: none`); - } else { - console.log(` ⚠ ${label}: ${list.length}`); - for (const v of list.slice(0, 30)) console.log(` - ${v}`); - if (list.length > 30) console.log(` ... and ${list.length - 30} more`); - } - } + const envVars = parseEnvExampleVars(envExampleText); + const docVars = parseEnvDocVars(envDocText); - printList("In code but missing from .env.example", inCodeMissingExample); - printList("In code but missing from ENVIRONMENT.md", inCodeMissingDoc); - printList("In .env.example but missing from ENVIRONMENT.md", inExampleMissingDoc); - printList("In .env.example but not referenced in code (dead?)", inExampleMissingCode); + const codeVars = new Set( + [...(options.codeVars ?? scanCodeVars({ cwd: options.root }))].filter((v) => !ignore.has(v)) + ); - const errors = inCodeMissingExample.length + inExampleMissingDoc.length; - if (STRICT && errors > 0) { - console.error(`\n✗ ${errors} drift(s) detected (strict mode)`); - process.exit(1); - } - console.log(`\n${errors === 0 ? "✓" : "⚠"} Done.`); + const codeMissingEnv = diff(codeVars, envVars); + const envMissingDoc = diff(envVars, docVars).filter((v) => !envOnly.has(v)); + const docMissingEnv = diff(docVars, envVars).filter((v) => !docOnly.has(v)); + + const ok = + codeMissingEnv.length === 0 && envMissingDoc.length === 0 && docMissingEnv.length === 0; + + return { + ok, + summary: { + code: codeVars.size, + envExample: envVars.size, + doc: docVars.size, + }, + problems: { + codeMissingEnv, + envMissingDoc, + docMissingEnv, + }, + }; } -main(); +// ─── CLI ─────────────────────────────────────────────────────────────────── + +function printList(label, list, marker) { + if (list.length === 0) { + console.log(` ${marker || "✓"} ${label}: none`); + return; + } + console.log(` ✗ ${label}: ${list.length}`); + for (const v of list.slice(0, 50)) console.log(` - ${v}`); + if (list.length > 50) console.log(` ... and ${list.length - 50} more`); +} + +function main() { + const lenient = process.argv.includes("--lenient"); + const result = runEnvDocSync(); + + console.log("Env var contract sync report"); + console.log("============================"); + console.log(`Code references: ${result.summary.code} unique vars`); + console.log(`In .env.example: ${result.summary.envExample} unique vars`); + console.log(`In docs/ENVIRONMENT.md: ${result.summary.doc} unique vars`); + console.log(); + + printList("In code but missing from .env.example", result.problems.codeMissingEnv); + printList("In .env.example but missing from ENVIRONMENT.md", result.problems.envMissingDoc); + printList("In ENVIRONMENT.md but missing from .env.example", result.problems.docMissingEnv); + + if (result.ok) { + console.log("\n✓ Env / docs contract is in sync."); + process.exit(0); + } + + if (lenient) { + console.log("\n⚠ Drift detected (lenient mode — exit 0)."); + process.exit(0); + } + + console.log("\n✗ Env / docs contract is out of sync. Update .env.example, docs/ENVIRONMENT.md,"); + console.log(" or the allowlists in scripts/check/check-env-doc-sync.mjs and try again."); + process.exit(1); +} + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +} diff --git a/src/shared/utils/fetchTimeout.ts b/src/shared/utils/fetchTimeout.ts index cfc36205ff..55efe599ac 100644 --- a/src/shared/utils/fetchTimeout.ts +++ b/src/shared/utils/fetchTimeout.ts @@ -7,7 +7,8 @@ * @module shared/utils/fetchTimeout */ -const DEFAULT_TIMEOUT_MS = 120000; // 2 minutes +const DEFAULT_TIMEOUT_MS = + parseInt(process.env.OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS || "", 10) || 120000; // 2 minutes const FETCH_TIMEOUT_MS = parseInt(process.env.FETCH_TIMEOUT_MS || "", 10) || DEFAULT_TIMEOUT_MS; interface FetchTimeoutOptions extends RequestInit { diff --git a/tests/unit/check-env-doc-sync.test.ts b/tests/unit/check-env-doc-sync.test.ts new file mode 100644 index 0000000000..2db6a62c87 --- /dev/null +++ b/tests/unit/check-env-doc-sync.test.ts @@ -0,0 +1,191 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { + parseEnvExampleVars, + parseEnvDocVars, + runEnvDocSync, +} from "../../scripts/check/check-env-doc-sync.mjs"; + +test("parseEnvExampleVars: extracts uncommented assignments", () => { + const text = ` +JWT_SECRET=abc123 +FOO_BAR=baz + # SKIPPED_VAR=leading whitespace before hash is rejected +`; + const vars = parseEnvExampleVars(text); + assert.ok(vars.has("JWT_SECRET")); + assert.ok(vars.has("FOO_BAR")); + // The regex anchors `^#?` so leading whitespace before `#` prevents a match. + assert.equal(vars.has("SKIPPED_VAR"), false); +}); + +test("parseEnvExampleVars: extracts commented examples", () => { + const text = ` +#OPTIONAL_KEY=value +# OPTIONAL_OTHER=value-with-space +ACTIVE=1 +`; + const vars = parseEnvExampleVars(text); + assert.ok(vars.has("OPTIONAL_KEY")); + assert.ok(vars.has("OPTIONAL_OTHER")); + assert.ok(vars.has("ACTIVE")); +}); + +test("parseEnvExampleVars: ignores prose", () => { + const text = ` +# This file documents env vars. Don't put values here. +Some narrative paragraph mentions PORT and DATA_DIR in passing. +PORT=20128 +`; + const vars = parseEnvExampleVars(text); + assert.deepEqual([...vars].sort(), ["PORT"]); +}); + +test("parseEnvDocVars: extracts SHOUTY_NAMES from inline backticks", () => { + const md = "Set `FOO_BAR` to enable the thing. Defaults to `BAR_BAZ_QUX`."; + const vars = parseEnvDocVars(md); + assert.deepEqual([...vars].sort(), ["BAR_BAZ_QUX", "FOO_BAR"]); +}); + +test("parseEnvDocVars: ignores values like `7s` or two-letter codes", () => { + const md = "TTL is `60s` and the type is `JSON`. Real var: `OMNIROUTE_TTL_MS`."; + const vars = parseEnvDocVars(md); + assert.deepEqual([...vars].sort(), ["JSON", "OMNIROUTE_TTL_MS"]); +}); + +test("runEnvDocSync: matched fixture passes", () => { + const envExampleText = ` +JWT_SECRET=secret +#OPTIONAL_VAR=value +ANOTHER_VAR=1 +`; + const envDocText = ` +# Reference + +| Variable | Default | Description | +| --- | --- | --- | +| \`JWT_SECRET\` | _(none)_ | required | +| \`OPTIONAL_VAR\` | _(unset)_ | optional | +| \`ANOTHER_VAR\` | 1 | enabled by default | +`; + const codeVars = new Set(["JWT_SECRET", "OPTIONAL_VAR", "ANOTHER_VAR"]); + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars, + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.codeMissingEnv, []); + assert.deepEqual(result.problems.envMissingDoc, []); + assert.deepEqual(result.problems.docMissingEnv, []); +}); + +test("runEnvDocSync: drift in code is flagged", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "NEW_VAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.codeMissingEnv, ["NEW_VAR"]); +}); + +test("runEnvDocSync: drift in doc is flagged", () => { + const envExampleText = `JWT_SECRET=secret\nNEW_VAR=value\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "NEW_VAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.envMissingDoc, ["NEW_VAR"]); +}); + +test("runEnvDocSync: drift in env (doc-only var) is flagged", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = ` +| \`JWT_SECRET\` | _(none)_ | required | +| \`OBSOLETE_VAR\` | _(unset)_ | docs-only legacy | +`; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, false); + assert.deepEqual(result.problems.docMissingEnv, ["OBSOLETE_VAR"]); +}); + +test("runEnvDocSync: docOnlyAllowlist absolves doc-only entries", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = ` +| \`JWT_SECRET\` | _(none)_ | required | +| \`LEGACY_ALIAS\` | _(unset)_ | documented but not in env.example | +`; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET"]), + ignore: new Set(), + docOnlyAllowlist: new Set(["LEGACY_ALIAS"]), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.docMissingEnv, []); +}); + +test("runEnvDocSync: envOnlyAllowlist absolves env-only entries", () => { + const envExampleText = `JWT_SECRET=secret\nFOO_BAR=bar\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "FOO_BAR"]), + ignore: new Set(), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(["FOO_BAR"]), + }); + assert.equal(result.ok, true); + assert.deepEqual(result.problems.envMissingDoc, []); +}); + +test("runEnvDocSync: ignore set skips a code-referenced var", () => { + const envExampleText = `JWT_SECRET=secret\n`; + const envDocText = "| `JWT_SECRET` | _(none)_ | required |"; + const result = runEnvDocSync({ + envExampleText, + envDocText, + codeVars: new Set(["JWT_SECRET", "PATH", "HOME"]), + ignore: new Set(["PATH", "HOME"]), + docOnlyAllowlist: new Set(), + envOnlyAllowlist: new Set(), + }); + assert.equal(result.ok, true); +}); + +test("repository contract is in sync (live data)", () => { + // Uses the real .env.example, docs/ENVIRONMENT.md, and the bundled + // allowlists. This is the same check that runs in pre-commit / CI. + const result = runEnvDocSync(); + if (!result.ok) { + const summary = JSON.stringify(result.problems, null, 2); + assert.fail(`Env/docs contract drift detected:\n${summary}`); + } + assert.equal(result.ok, true); +});