Upload 22 files
Browse files- .dockerignore +0 -93
- .env.example +23 -1443
- .gitignore +57 -170
- .npmignore +12 -84
- CHANGELOG.md +0 -0
- CLAUDE.md +76 -438
- DOCKER.md +132 -0
- Dockerfile +35 -150
- LICENSE +1 -1
- README.md +1526 -10
- README.zh-CN.md +1315 -0
- README_HUGGINGFACE.md +25 -0
- captain-definition +4 -0
- custom-server.js +139 -0
- docker-compose.yml +23 -171
- eslint.config.mjs +11 -97
- hf-server.js +126 -0
- jsconfig.json +12 -0
- next.config.mjs +62 -472
- package.json +46 -255
- postcss.config.mjs +9 -4
- start.sh +4 -0
.dockerignore
CHANGED
|
@@ -9,7 +9,6 @@
|
|
| 9 |
# Dependencies and build output
|
| 10 |
node_modules
|
| 11 |
.next
|
| 12 |
-
.build
|
| 13 |
out
|
| 14 |
build
|
| 15 |
dist
|
|
@@ -31,95 +30,3 @@ npm-debug.log*
|
|
| 31 |
yarn-debug.log*
|
| 32 |
yarn-error.log*
|
| 33 |
.pnpm-debug.log*
|
| 34 |
-
|
| 35 |
-
# Test suites
|
| 36 |
-
tests
|
| 37 |
-
test-results
|
| 38 |
-
playwright-report
|
| 39 |
-
blob-report
|
| 40 |
-
|
| 41 |
-
# Documentation
|
| 42 |
-
# Issue #2348: The Dashboard Docs viewer reads markdown from `/app/docs` at
|
| 43 |
-
# runtime. The previous `docs/*` block hid every file except openapi.yaml,
|
| 44 |
-
# so the in-product help screen failed with ENOENT for every page.
|
| 45 |
-
# We now keep the English markdown tree plus the docs assets imported by MDX
|
| 46 |
-
# during `next build`, while still dropping the bulky translated docs and
|
| 47 |
-
# extra raster diagram sources that account for most of the docs footprint
|
| 48 |
-
# of the ~50 MB docs directory. The Docs viewer reads the default-locale
|
| 49 |
-
# (English) sources at runtime, so translations are not required in the
|
| 50 |
-
# container image.
|
| 51 |
-
docs/i18n/**
|
| 52 |
-
docs/diagrams/**/*.png
|
| 53 |
-
docs/diagrams/**/*.jpg
|
| 54 |
-
docs/diagrams/**/*.jpeg
|
| 55 |
-
docs/diagrams/**/*.gif
|
| 56 |
-
docs/diagrams/**/*.webp
|
| 57 |
-
# Note: `*.md` matches the root only (Go filepath.Match does not cross /),
|
| 58 |
-
# so nested docs/**/*.md is implicitly kept without a re-include rule.
|
| 59 |
-
*.md
|
| 60 |
-
!README.md
|
| 61 |
-
|
| 62 |
-
# Electron (separate build)
|
| 63 |
-
electron
|
| 64 |
-
|
| 65 |
-
# VS Code extension (separate project)
|
| 66 |
-
vscode-extension
|
| 67 |
-
|
| 68 |
-
# Build artifacts
|
| 69 |
-
*.tgz
|
| 70 |
-
*.AppImage
|
| 71 |
-
*.deb
|
| 72 |
-
*.rpm
|
| 73 |
-
|
| 74 |
-
# Package manager lock (bun)
|
| 75 |
-
bun.lock
|
| 76 |
-
|
| 77 |
-
# Agent config
|
| 78 |
-
.agents
|
| 79 |
-
.gemini
|
| 80 |
-
|
| 81 |
-
# Misc
|
| 82 |
-
llm.txt
|
| 83 |
-
images
|
| 84 |
-
clipr
|
| 85 |
-
omnirouteCloud
|
| 86 |
-
omnirouteSite
|
| 87 |
-
|
| 88 |
-
# Temporary/Scratch Folders
|
| 89 |
-
_*
|
| 90 |
-
|
| 91 |
-
# CI/CD and Version Control (that are not actual code)
|
| 92 |
-
.github
|
| 93 |
-
.husky
|
| 94 |
-
.omc
|
| 95 |
-
|
| 96 |
-
# Test Configs and Reports
|
| 97 |
-
playwright.config.ts
|
| 98 |
-
vitest*.ts
|
| 99 |
-
audit-report.json
|
| 100 |
-
sonar-project.properties
|
| 101 |
-
|
| 102 |
-
# Deployment Configs
|
| 103 |
-
docker-compose*.yml
|
| 104 |
-
fly.toml
|
| 105 |
-
|
| 106 |
-
# Consistent with .gitignore
|
| 107 |
-
.DS_Store
|
| 108 |
-
.idea/
|
| 109 |
-
.config/
|
| 110 |
-
.data/
|
| 111 |
-
.omnivscodeagent/
|
| 112 |
-
*.sqlite-*
|
| 113 |
-
*.tsbuildinfo
|
| 114 |
-
next-env.d.ts
|
| 115 |
-
security-analysis/
|
| 116 |
-
.analysis/
|
| 117 |
-
antigravity-manager-analysis/
|
| 118 |
-
.sisyphus/
|
| 119 |
-
.plans/
|
| 120 |
-
app.__qa_backup/
|
| 121 |
-
.app-build-backup-*/
|
| 122 |
-
.gitnexus
|
| 123 |
-
.worktrees
|
| 124 |
-
.next-playwright/
|
| 125 |
-
cloud/
|
|
|
|
| 9 |
# Dependencies and build output
|
| 10 |
node_modules
|
| 11 |
.next
|
|
|
|
| 12 |
out
|
| 13 |
build
|
| 14 |
dist
|
|
|
|
| 30 |
yarn-debug.log*
|
| 31 |
yarn-error.log*
|
| 32 |
.pnpm-debug.log*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.env.example
CHANGED
|
@@ -1,1461 +1,41 @@
|
|
| 1 |
-
#
|
| 2 |
-
#
|
| 3 |
-
# │ This file documents EVERY environment variable read by the runtime. │
|
| 4 |
-
# │ Copy to .env and adjust values. Lines starting with # are commented out │
|
| 5 |
-
# │ (optional / off-by-default). Uncomment only what you need. │
|
| 6 |
-
# │ Reference: docs/ENVIRONMENT.md for full details and usage scenarios. │
|
| 7 |
-
# └─────────────────────────────────────────────────────────────────────────────┘
|
| 8 |
|
| 9 |
-
#
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
|
| 14 |
-
#
|
| 15 |
-
# Used by: src/lib/auth — signs/verifies all authenticated session cookies.
|
| 16 |
-
# Generate: openssl rand -base64 48
|
| 17 |
-
JWT_SECRET=
|
| 18 |
-
|
| 19 |
-
# Encryption key for API keys stored in the database.
|
| 20 |
-
# Used by: src/lib/db/apiKeys.ts — encrypts API key values at rest in SQLite.
|
| 21 |
-
# Generate: openssl rand -hex 32
|
| 22 |
-
API_KEY_SECRET=
|
| 23 |
-
|
| 24 |
-
# Initial admin login password — CHANGE THIS before first use!
|
| 25 |
-
# Used by: bootstrap only — sets the initial dashboard password on first boot.
|
| 26 |
-
# After first login you can change it from Dashboard → Settings → Security.
|
| 27 |
-
# Default: CHANGEME (insecure, for local dev only)
|
| 28 |
-
INITIAL_PASSWORD=CHANGEME
|
| 29 |
-
|
| 30 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 31 |
-
# 2. STORAGE & DATABASE
|
| 32 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 33 |
-
# OmniRoute uses SQLite for all persistence. These variables control where
|
| 34 |
-
# data lives, encryption, and cleanup policies.
|
| 35 |
-
|
| 36 |
-
# Base directory for all persistent data (SQLite DB, logs, backups).
|
| 37 |
-
# Used by: src/lib/db/core.ts — resolves the SQLite database file path.
|
| 38 |
-
# Default: ~/.omniroute/ | Override for Docker or custom installations.
|
| 39 |
-
# Hint: When running in Docker, consider mounting a host directory here for data persistence across container restarts
|
| 40 |
-
# also if you want to share the same database as "npm run dev" use "./data"
|
| 41 |
-
# DATA_DIR=/var/lib/omniroute
|
| 42 |
-
|
| 43 |
-
# Encryption key for SQLite database encryption at rest.
|
| 44 |
-
# Used by: src/lib/db/encryption.ts — encrypts the entire SQLite database.
|
| 45 |
-
# Generate: openssl rand -hex 32 | Leave empty to disable DB encryption.
|
| 46 |
-
STORAGE_ENCRYPTION_KEY=
|
| 47 |
-
|
| 48 |
-
# Version tag for the encryption key — allows future key rotation.
|
| 49 |
-
# Used by: scripts/bootstrap-env.mjs, electron/main.js — persists key version.
|
| 50 |
-
# Default: v1 | Increment when rotating STORAGE_ENCRYPTION_KEY.
|
| 51 |
-
STORAGE_ENCRYPTION_KEY_VERSION=v1
|
| 52 |
-
|
| 53 |
-
# Automatic SQLite backup on startup.
|
| 54 |
-
# Used by: src/lib/db/backup.ts — creates a timestamped backup before migrations.
|
| 55 |
-
# Default: false (backups enabled) | Set true to skip backup on every restart.
|
| 56 |
-
DISABLE_SQLITE_AUTO_BACKUP=false
|
| 57 |
-
|
| 58 |
-
# ── Redis (Rate Limiting) ──
|
| 59 |
-
# Redis connection URL for the rate limiter backend.
|
| 60 |
-
# Used by: src/shared/utils/rateLimiter.ts
|
| 61 |
-
# Default: redis://localhost:6379 (or redis://redis:6379 in Docker)
|
| 62 |
-
REDIS_URL=redis://localhost:6379
|
| 63 |
-
|
| 64 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 65 |
-
# 3. NETWORK & PORTS
|
| 66 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 67 |
-
# OmniRoute can run on a single port (default) or split Dashboard/API ports.
|
| 68 |
-
|
| 69 |
-
# Canonical port for both Dashboard UI and API (single-port mode).
|
| 70 |
-
# Used by: src/lib/runtime/ports.ts — base port for the Next.js server.
|
| 71 |
-
# Default: 20128
|
| 72 |
PORT=20128
|
| 73 |
-
|
| 74 |
-
# Split-port mode: serve Dashboard and API on separate ports for network isolation.
|
| 75 |
-
# Used by: src/lib/runtime/ports.ts — overrides PORT for each service.
|
| 76 |
-
# API_PORT=20129
|
| 77 |
-
# API_HOST=0.0.0.0
|
| 78 |
-
# DASHBOARD_PORT=20128
|
| 79 |
-
|
| 80 |
-
# Port for the real-time WebSocket live monitoring server.
|
| 81 |
-
# Used by: src/server/ws/liveServer.ts, src/app/api/v1/ws/route.ts
|
| 82 |
-
# Default: 20129
|
| 83 |
-
# LIVE_WS_PORT=20129
|
| 84 |
-
|
| 85 |
-
# Bind address for the live WebSocket server.
|
| 86 |
-
# Default: 127.0.0.1 (loopback only). Set to 0.0.0.0 to expose on LAN —
|
| 87 |
-
# remember to also configure LIVE_WS_ALLOWED_ORIGINS when doing so.
|
| 88 |
-
# LIVE_WS_HOST=127.0.0.1
|
| 89 |
-
|
| 90 |
-
# Comma-separated extra origins allowed to open a live WebSocket. The
|
| 91 |
-
# loopback dashboard origins are already permitted by default; use this
|
| 92 |
-
# var when fronting the server with a domain (e.g. https://omni.local).
|
| 93 |
-
# LIVE_WS_ALLOWED_ORIGINS=https://omni.local,https://dashboard.example.com
|
| 94 |
-
|
| 95 |
-
# Disable the standalone live WebSocket helper used by scripts/start-ws-server.mjs.
|
| 96 |
-
# Used by: scripts/start-ws-server.mjs (CI/embedded harness toggle).
|
| 97 |
-
# OMNIROUTE_DISABLE_LIVE_WS=0
|
| 98 |
-
|
| 99 |
-
# Enable the real-time dashboard WebSocket server.
|
| 100 |
-
# Used by: src/server/ws/liveServer.ts, scripts/start-ws-server.mjs
|
| 101 |
-
# Default: ON. Set to 0 or false to disable startup of the live WS server.
|
| 102 |
-
# Combine with LIVE_WS_HOST / LIVE_WS_ALLOWED_ORIGINS above when exposing
|
| 103 |
-
# beyond loopback.
|
| 104 |
-
# OMNIROUTE_ENABLE_LIVE_WS=1
|
| 105 |
-
|
| 106 |
-
# Per-(token,IP) relay rate limit, requests/minute. In-memory, per instance.
|
| 107 |
-
# 0 or negative disables the IP-dimension gate (per-token DB limit still applies).
|
| 108 |
-
# Default: 30
|
| 109 |
-
# Used by: src/app/api/v1/relay/chat/completions/route.ts
|
| 110 |
-
# RELAY_IP_PER_MINUTE=30
|
| 111 |
-
|
| 112 |
-
# Use Turbopack in local dev. Next 16.2.4 can fail to compile next/font/google
|
| 113 |
-
# through the custom dev runner without this on Windows.
|
| 114 |
-
OMNIROUTE_USE_TURBOPACK=1
|
| 115 |
-
|
| 116 |
-
# Skip the SQLite integrity health check on startup (faster boot on large DBs).
|
| 117 |
-
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to skip.
|
| 118 |
-
# OMNIROUTE_SKIP_DB_HEALTHCHECK=1
|
| 119 |
-
|
| 120 |
-
# Interval (ms) for the background credential health check scheduler.
|
| 121 |
-
# Default: 300000 (5 minutes). Minimum: 10000 (10 seconds).
|
| 122 |
-
# Used by: open-sse/config/constants.ts, src/lib/credentialHealth/scheduler.ts
|
| 123 |
-
# CREDENTIAL_HEALTH_CHECK_INTERVAL=300000
|
| 124 |
-
|
| 125 |
-
# TTL (ms) for cached credential health status.
|
| 126 |
-
# Default: 300000 (5 minutes).
|
| 127 |
-
# Used by: open-sse/config/constants.ts, src/lib/credentialHealth/cache.ts
|
| 128 |
-
# CREDENTIAL_HEALTH_CACHE_TTL=300000
|
| 129 |
-
|
| 130 |
-
# Set to 1 or true to disable background periodic testing of provider connections.
|
| 131 |
-
# Default: false
|
| 132 |
-
# Used by: src/lib/credentialHealth/scheduler.ts
|
| 133 |
-
# OMNIROUTE_DISABLE_CREDENTIAL_HEALTH_CHECK=false
|
| 134 |
-
|
| 135 |
-
# Set to "true" to emit `[ProxyFetch]` debug logs from the Vercel relay path
|
| 136 |
-
# in open-sse/utils/proxyFetch.ts. Off by default to avoid leaking routing
|
| 137 |
-
# hints in production logs.
|
| 138 |
-
# OMNIROUTE_PROXY_FETCH_DEBUG=true
|
| 139 |
-
|
| 140 |
-
# Docker production port mappings (docker-compose.prod.yml only).
|
| 141 |
-
# These set the HOST-side published ports. Container ports use PORT/API_PORT.
|
| 142 |
-
# PROD_DASHBOARD_PORT=20130
|
| 143 |
-
# PROD_API_PORT=20131
|
| 144 |
-
|
| 145 |
-
# Runtime override used by Electron and wrapped environments.
|
| 146 |
-
# OMNIROUTE_PORT takes precedence over PORT when running inside wrappers.
|
| 147 |
-
# Used by: src/lib/runtime/ports.ts — preserves canonical port in Electron.
|
| 148 |
-
# OMNIROUTE_PORT=20128
|
| 149 |
-
|
| 150 |
-
# Hostname/bind address for the Next.js server.
|
| 151 |
-
# Used by: scripts/dev/run-next.mjs (HOST), Playwright runner (HOSTNAME).
|
| 152 |
-
# Default: 0.0.0.0 (HOST) / 127.0.0.1 (HOSTNAME inside tests).
|
| 153 |
-
#HOST=0.0.0.0
|
| 154 |
-
#HOSTNAME=127.0.0.1
|
| 155 |
-
|
| 156 |
-
# Environment mode — affects Next.js behavior, logging verbosity, and caching.
|
| 157 |
-
# Values: production | development | Default: production
|
| 158 |
NODE_ENV=production
|
| 159 |
|
| 160 |
-
#
|
| 161 |
-
|
| 162 |
-
# Set to "podman" when running under rootless Podman so the entrypoint
|
| 163 |
-
# gives the correct fix instructions (podman unshare chown vs sudo chown).
|
| 164 |
-
CONTAINER_HOST=docker
|
| 165 |
-
|
| 166 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 167 |
-
# 4. SECURITY & AUTHENTICATION
|
| 168 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 169 |
-
|
| 170 |
-
# Salt for generating unique machine IDs (fingerprint diversification).
|
| 171 |
-
# Used by: src/lib/auth — combined with hardware identifiers for machine-id hash.
|
| 172 |
-
# Default: endpoint-proxy-salt | Change per-deployment for isolation.
|
| 173 |
MACHINE_ID_SALT=endpoint-proxy-salt
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
# Used by: src/lib/machineToken.ts — rotates the local CLI auth token without
|
| 177 |
-
# touching code. Set to a new value to invalidate existing CLI tokens.
|
| 178 |
-
# Default: omniroute-cli-auth-v1
|
| 179 |
-
# OMNIROUTE_CLI_SALT=omniroute-cli-auth-v1
|
| 180 |
-
|
| 181 |
-
# Set true when running behind HTTPS (reverse proxy with TLS termination).
|
| 182 |
-
# Used by: src/lib/auth — sets the Secure flag on session cookies.
|
| 183 |
-
# Default: false | MUST be true in any non-localhost deployment.
|
| 184 |
AUTH_COOKIE_SECURE=false
|
| 185 |
-
|
| 186 |
-
# Require an API key for all /v1/* proxy endpoints.
|
| 187 |
-
# Used by: API middleware — rejects unauthenticated requests to the proxy API.
|
| 188 |
-
# Default: false | Set true for multi-user/public deployments.
|
| 189 |
REQUIRE_API_KEY=false
|
| 190 |
|
| 191 |
-
#
|
| 192 |
-
#
|
| 193 |
-
#
|
| 194 |
-
ALLOW_API_KEY_REVEAL=false
|
| 195 |
-
|
| 196 |
-
# Shared secret for the internal Codex Responses WebSocket bridge.
|
| 197 |
-
# Used by: src/app/api/internal/codex-responses-ws/route.ts — authenticates
|
| 198 |
-
# bridge requests between the Electron/browser WS relay and OmniRoute.
|
| 199 |
-
# ⚠️ REQUIRED for production — if unset, all WS bridge requests are rejected.
|
| 200 |
-
# Generate: openssl rand -base64 32
|
| 201 |
-
# OMNIROUTE_WS_BRIDGE_SECRET=
|
| 202 |
-
|
| 203 |
-
# Per-process secret that proves the trusted peer-IP stamp came from OmniRoute's
|
| 204 |
-
# own HTTP server (scripts/dev/peer-stamp.mjs). The custom server stamps the real
|
| 205 |
-
# TCP peer IP as `<token>|<ip>`; the authz middleware trusts the locality only
|
| 206 |
-
# when the token matches. Used by: src/server/authz/policies/management.ts.
|
| 207 |
-
# Auto-generated per boot — leave UNSET in normal use. Only set it to pin a fixed
|
| 208 |
-
# value across processes (e.g. a multi-process setup that must share the stamp).
|
| 209 |
-
# OMNIROUTE_PEER_STAMP_TOKEN=
|
| 210 |
-
|
| 211 |
-
# Comma-separated API key IDs that skip request logging (GDPR/compliance).
|
| 212 |
-
# Used by: src/lib/compliance/index.ts — suppresses logs for specific keys.
|
| 213 |
-
# NO_LOG_API_KEY_IDS=key_abc123,key_def456
|
| 214 |
-
|
| 215 |
-
# Fallback per-day request budget applied to API keys whose `rate_limits`
|
| 216 |
-
# column is null. Default (unset/empty/malformed) preserves the legacy
|
| 217 |
-
# 1000/day, 5000/week, 20000/month windows so existing deployments do not
|
| 218 |
-
# silently lose rate limiting on upgrade.
|
| 219 |
-
# Set explicitly to "0" to opt out entirely (unlimited fallback). Any
|
| 220 |
-
# positive integer N enables N/day, 5N/week, 20N/month.
|
| 221 |
-
# Used by: src/shared/utils/apiKeyPolicy.ts — checkRateLimit() fallback.
|
| 222 |
-
# DEFAULT_RATE_LIMIT_PER_DAY=1000
|
| 223 |
-
|
| 224 |
-
# Maximum request body size in bytes (rejects larger payloads).
|
| 225 |
-
# Used by: src/shared/middleware/bodySizeGuard.ts — prevents oversized uploads.
|
| 226 |
-
# Default: 10485760 (10 MB)
|
| 227 |
-
# MAX_BODY_SIZE_BYTES=10485760
|
| 228 |
-
|
| 229 |
-
# CORS configuration — controls which origins can call the API.
|
| 230 |
-
# Used by: Next.js middleware — sets Access-Control-Allow-Origin header.
|
| 231 |
-
# Default: * (all origins) | Restrict for production security.
|
| 232 |
-
# CORS_ORIGIN=https://your-domain.com
|
| 233 |
-
|
| 234 |
-
# Allow provider URLs pointing to private/local networks (localhost, 192.168.x.x, etc.).
|
| 235 |
-
# REQUIRED for self-hosted providers: LM Studio, Ollama, vLLM, Llamafile, Triton, etc.
|
| 236 |
-
# Used by: src/shared/network/outboundUrlGuard.ts — disables SSRF guard for provider calls.
|
| 237 |
-
# Default: false (blocked) | Set true to enable local providers.
|
| 238 |
-
# OMNIROUTE_ALLOW_PRIVATE_PROVIDER_URLS=true
|
| 239 |
-
|
| 240 |
-
# Legacy alias toggling the SSRF guard. Used by: src/shared/network/outboundUrlGuard.ts
|
| 241 |
-
# When unset, OmniRoute uses the per-feature defaults. Set to "false"/"0" to disable.
|
| 242 |
-
# OUTBOUND_SSRF_GUARD_ENABLED=true
|
| 243 |
-
|
| 244 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 245 |
-
# 5. INPUT SANITIZATION & PII PROTECTION (FASE-01)
|
| 246 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 247 |
-
# Multi-layer defense: request-side injection guard + response-side PII sanitizer.
|
| 248 |
-
|
| 249 |
-
# ── Request-Side: Prompt Injection Guard ──
|
| 250 |
-
# Scans incoming messages for prompt injection patterns before routing.
|
| 251 |
-
# Used by: src/middleware/promptInjectionGuard.ts
|
| 252 |
-
# INPUT_SANITIZER_ENABLED=true
|
| 253 |
-
# INPUT_SANITIZER_MODE=warn # warn = log only | block = reject request | redact = strip patterns
|
| 254 |
-
|
| 255 |
-
# Legacy alias for INPUT_SANITIZER_MODE (same effect).
|
| 256 |
-
# INJECTION_GUARD_MODE=warn
|
| 257 |
-
|
| 258 |
-
# PII detection in incoming requests (emails, phone numbers, SSNs, etc.).
|
| 259 |
-
# Used by: src/middleware/promptInjectionGuard.ts — extends injection guard.
|
| 260 |
-
# PII_REDACTION_ENABLED=false
|
| 261 |
-
|
| 262 |
-
# Minimum streaming window size for PII detection (bytes). Default: 200.
|
| 263 |
-
# Used by: src/lib/streamingPiiTransform.ts.
|
| 264 |
-
# PII_WINDOW_SIZE=200
|
| 265 |
-
|
| 266 |
-
# Test bypass: allow setting PII_WINDOW_SIZE below minimum. Default: false.
|
| 267 |
-
# Used by: src/lib/streamingPiiTransform.ts.
|
| 268 |
-
# PII_TEST_BYPASS_MIN_WINDOW=false
|
| 269 |
-
|
| 270 |
-
# ── Response-Side: PII Sanitizer ──
|
| 271 |
-
# Scans LLM responses for leaked PII before returning to the client.
|
| 272 |
-
# Used by: src/lib/piiSanitizer.ts
|
| 273 |
-
# PII_RESPONSE_SANITIZATION=false
|
| 274 |
-
# PII_RESPONSE_SANITIZATION_MODE=redact # redact = mask PII | warn = log only | block = drop response
|
| 275 |
-
|
| 276 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 277 |
-
# 6. TOOL & ROUTING POLICIES
|
| 278 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 279 |
-
|
| 280 |
-
# Tool policy mode — controls which tools LLMs can invoke via function calling.
|
| 281 |
-
# Used by: src/lib/toolPolicy.ts — enforces allowlist/denylist on tool_choice.
|
| 282 |
-
# Values: allowlist | denylist | disabled | Default: disabled
|
| 283 |
-
# TOOL_POLICY_MODE=disabled
|
| 284 |
-
|
| 285 |
-
# Payload manipulation rules JSON file.
|
| 286 |
-
# Used by: open-sse/services/payloadRules.ts — injects/removes upstream payload fields per model/protocol.
|
| 287 |
-
# Default: ./config/payloadRules.json
|
| 288 |
-
# OMNIROUTE_PAYLOAD_RULES_PATH=./config/payloadRules.json
|
| 289 |
-
|
| 290 |
-
# Reload interval for payloadRules.json mtime checks in milliseconds.
|
| 291 |
-
# Used by: open-sse/services/payloadRules.ts — keeps file-based rules hot-reloadable without restart.
|
| 292 |
-
# Default: 5000 | Minimum: 1000
|
| 293 |
-
# OMNIROUTE_PAYLOAD_RULES_RELOAD_MS=5000
|
| 294 |
-
|
| 295 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 296 |
-
# 7. URLS & CLOUD SYNC
|
| 297 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 298 |
-
# URLs used for internal sync jobs, OAuth callbacks, and cloud relay.
|
| 299 |
-
|
| 300 |
-
# Internal base URL — used by server-side sync jobs to call /api/sync/cloud.
|
| 301 |
-
# Used by: src/lib/cloudSync.ts, src/lib/initCloudSync.ts
|
| 302 |
-
# Default: http://localhost:20128
|
| 303 |
BASE_URL=http://localhost:20128
|
| 304 |
-
|
| 305 |
-
#
|
| 306 |
-
# Used by: src/lib/cloudSync.ts — pushes/pulls settings from OmniRoute Cloud.
|
| 307 |
-
CLOUD_URL=
|
| 308 |
-
|
| 309 |
-
# Timeout for cloud sync HTTP requests in milliseconds.
|
| 310 |
-
# Used by: src/lib/cloudSync.ts — fetchWithTimeout wrapper.
|
| 311 |
-
# Default: 12000 (12 seconds)
|
| 312 |
-
# CLOUD_SYNC_TIMEOUT_MS=12000
|
| 313 |
-
|
| 314 |
-
# Public-facing base URL — CRITICAL for reverse proxy / OAuth callback setups.
|
| 315 |
-
# Used by: OAuth redirect_uri computation, Dashboard UI links, cloud/model sync.
|
| 316 |
-
# Set to your public URL when behind nginx/Caddy (e.g., https://omniroute.example.com).
|
| 317 |
-
#
|
| 318 |
-
# Dashboard display behavior: when this variable is unset, the dashboard
|
| 319 |
-
# auto-detects the base URL shown in curl examples and CLI tool snippets
|
| 320 |
-
# from window.location.origin (the host the user is browsing). Setting it
|
| 321 |
-
# explicitly is only required when running behind a reverse proxy with a
|
| 322 |
-
# different public hostname, or when OAuth callbacks must point to a
|
| 323 |
-
# canonical URL.
|
| 324 |
-
#
|
| 325 |
-
# Default: http://localhost:20128
|
| 326 |
NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
|
|
|
| 327 |
|
| 328 |
-
#
|
| 329 |
-
#
|
| 330 |
-
# Set this when OpenWebUI or another relay reaches OmniRoute by an internal URL
|
| 331 |
-
# but the user's browser must fetch images from a LAN, tunnel, or public origin.
|
| 332 |
-
# Do not include /v1; if included accidentally it will be normalized away.
|
| 333 |
-
# OMNIROUTE_PUBLIC_BASE_URL=http://192.168.0.15:20128
|
| 334 |
-
|
| 335 |
-
# Max wait time for an async chatgpt-web image to land via the celsius
|
| 336 |
-
# WebSocket, in milliseconds. Default 180000 (3 minutes). Increase during
|
| 337 |
-
# upstream queue-deep windows ("Lots of people are creating images right now").
|
| 338 |
-
# OMNIROUTE_CGPT_WEB_IMAGE_TIMEOUT_MS=180000
|
| 339 |
-
|
| 340 |
-
# Total in-memory byte budget for the chatgpt-web image cache (used to serve
|
| 341 |
-
# /v1/chatgpt-web/image/<id>), in megabytes. Default 256. Lower this if you
|
| 342 |
-
# run OmniRoute on a memory-constrained host; raise it if image generation
|
| 343 |
-
# is heavy and clients are racing the 30-minute TTL.
|
| 344 |
-
# OMNIROUTE_CGPT_WEB_IMAGE_CACHE_MAX_MB=256
|
| 345 |
-
|
| 346 |
-
# Public cloud URL — client-side mirror of CLOUD_URL.
|
| 347 |
-
NEXT_PUBLIC_CLOUD_URL=
|
| 348 |
-
|
| 349 |
-
# Legacy alias — fallback for NEXT_PUBLIC_BASE_URL in sync schedulers.
|
| 350 |
-
# NEXT_PUBLIC_APP_URL=http://localhost:20128
|
| 351 |
-
|
| 352 |
-
# Public callback URL for asynchronous image/audio jobs (kie.ai, etc.).
|
| 353 |
-
# Used by: open-sse/utils/kieTask.ts — overrides callbackUrlFromBaseUrl().
|
| 354 |
-
# Honor order: KIE_CALLBACK_URL → OMNIROUTE_KIE_CALLBACK_URL → OMNIROUTE_PUBLIC_URL.
|
| 355 |
-
#KIE_CALLBACK_URL=
|
| 356 |
-
#OMNIROUTE_KIE_CALLBACK_URL=
|
| 357 |
-
#OMNIROUTE_PUBLIC_URL=
|
| 358 |
-
|
| 359 |
-
# Upstream quota endpoints used by the Usage page. Override only for
|
| 360 |
-
# debugging or when routing through a corporate mirror. Used by:
|
| 361 |
-
# open-sse/services/usage.ts.
|
| 362 |
-
#OMNIROUTE_CROF_USAGE_URL=https://crof.ai/usage_api/
|
| 363 |
-
#OMNIROUTE_GEMINI_CLI_USAGE_URL=https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist
|
| 364 |
-
#OMNIROUTE_CODEWHISPERER_BASE_URL=https://codewhisperer.us-east-1.amazonaws.com
|
| 365 |
-
#OMNIROUTE_OPENCODE_QUOTA_URL=https://opencode.ai/zen/go/v1/quota
|
| 366 |
-
#OMNIROUTE_OPENCODE_GO_QUOTA_URL=https://api.z.ai/api/monitor/usage/quota/limit
|
| 367 |
-
|
| 368 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 369 |
-
# 8. OUTBOUND PROXY (Upstream Provider Calls)
|
| 370 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 371 |
-
# Route upstream LLM API calls through an HTTP/SOCKS5 proxy.
|
| 372 |
-
# Useful for corporate egress, geo-routing, or IP masking.
|
| 373 |
-
|
| 374 |
-
# Enable SOCKS5 proxy support in both server and client components.
|
| 375 |
-
# Used by: open-sse/executors — wraps fetch() calls through the proxy agent.
|
| 376 |
-
ENABLE_SOCKS5_PROXY=true
|
| 377 |
-
NEXT_PUBLIC_ENABLE_SOCKS5_PROXY=true
|
| 378 |
-
|
| 379 |
-
# Standard proxy variables (lowercase variants also supported).
|
| 380 |
# HTTP_PROXY=http://127.0.0.1:7890
|
| 381 |
# HTTPS_PROXY=http://127.0.0.1:7890
|
| 382 |
# ALL_PROXY=socks5://127.0.0.1:7890
|
| 383 |
# NO_PROXY=localhost,127.0.0.1
|
| 384 |
|
| 385 |
-
#
|
| 386 |
-
#
|
| 387 |
-
# Used by: open-sse/executors — replaces Node.js default TLS fingerprint.
|
| 388 |
-
# ENABLE_TLS_FINGERPRINT=true
|
| 389 |
-
|
| 390 |
-
# Allow the Claude Turnstile Playwright browser context to ignore HTTPS certificate errors.
|
| 391 |
-
# Only enable for local debugging or trusted MITM/corporate proxy environments.
|
| 392 |
-
# Used by: open-sse/services/claudeTurnstileSolver.ts
|
| 393 |
-
# OMNIROUTE_TURNSTILE_IGNORE_TLS_ERRORS=false
|
| 394 |
-
|
| 395 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 396 |
-
# 9. CLI TOOL INTEGRATION
|
| 397 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 398 |
-
# Control how OmniRoute discovers and launches CLI sidecars (Claude, Codex, etc.).
|
| 399 |
-
# Used by: src/shared/services/cliRuntime.ts
|
| 400 |
-
|
| 401 |
-
# CLI discovery mode: auto = search PATH | manual = use explicit paths below.
|
| 402 |
-
# CLI_MODE=auto
|
| 403 |
-
|
| 404 |
-
# Additional PATH entries for finding CLI binaries (colon-separated).
|
| 405 |
-
# CLI_EXTRA_PATHS=/host-cli/bin:/usr/local/bin
|
| 406 |
-
|
| 407 |
-
# Home directory override for reading CLI config files (~/.claude, etc.).
|
| 408 |
-
# CLI_CONFIG_HOME=/root
|
| 409 |
-
|
| 410 |
-
# Allow OmniRoute to write CLI config files (token refresh, etc.).
|
| 411 |
-
# CLI_ALLOW_CONFIG_WRITES=true
|
| 412 |
-
|
| 413 |
-
# Override binary paths for individual CLI tools.
|
| 414 |
-
# CLI_CLAUDE_BIN=claude
|
| 415 |
-
# CLI_CODEX_BIN=codex
|
| 416 |
-
# CLI_DROID_BIN=droid
|
| 417 |
-
# CLI_OPENCLAW_BIN=openclaw
|
| 418 |
-
# CLI_CURSOR_BIN=agent
|
| 419 |
-
# CLI_CLINE_BIN=cline
|
| 420 |
-
# CLI_CONTINUE_BIN=cn
|
| 421 |
-
# CLI_QODER_BIN=qoder
|
| 422 |
-
# CLI_QWEN_BIN=qwen
|
| 423 |
-
|
| 424 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 425 |
-
# 10. INTERNAL AGENT & MCP INTEGRATIONS
|
| 426 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 427 |
-
# Used by MCP server, A2A skills, and CLI sidecars to call the running instance.
|
| 428 |
-
|
| 429 |
-
# Explicit base URL for MCP/A2A tools to reach OmniRoute (overrides localhost auto-detect).
|
| 430 |
-
# For browser-visible generated image URLs, prefer OMNIROUTE_PUBLIC_BASE_URL above.
|
| 431 |
-
# Used by: open-sse/mcp-server/server.ts, src/lib/a2a/
|
| 432 |
-
# OMNIROUTE_BASE_URL=http://localhost:20128
|
| 433 |
-
|
| 434 |
-
# API key for internal tool calls (MCP tools, A2A skills).
|
| 435 |
-
# OMNIROUTE_API_KEY=
|
| 436 |
-
|
| 437 |
-
# API key ID for MCP audit logging.
|
| 438 |
-
# Used by: open-sse/mcp-server/audit.ts — tags audit events with a key identity.
|
| 439 |
-
# OMNIROUTE_API_KEY_ID=
|
| 440 |
-
|
| 441 |
-
# Legacy alias for OMNIROUTE_API_KEY.
|
| 442 |
-
# ROUTER_API_KEY=
|
| 443 |
-
|
| 444 |
-
# Enforce scope-based access control on MCP tool calls.
|
| 445 |
-
# Used by: open-sse/mcp-server/server.ts — rejects calls outside allowed scopes.
|
| 446 |
-
# OMNIROUTE_MCP_ENFORCE_SCOPES=false
|
| 447 |
-
|
| 448 |
-
# Comma-separated scopes granted to this MCP connection.
|
| 449 |
-
# Full list: admin, combos, health, models, routing, budget, metrics, pricing, memory, skills
|
| 450 |
-
# OMNIROUTE_MCP_SCOPES=admin,combos,health
|
| 451 |
-
|
| 452 |
-
# Compress MCP tool descriptions before serializing the manifest.
|
| 453 |
-
# Used by: open-sse/mcp-server/descriptionCompressor.ts — reduces token spend
|
| 454 |
-
# for clients that read the full tool catalog.
|
| 455 |
-
# Accepted disabling values: 0, false, off. Default: enabled.
|
| 456 |
-
# OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS=1
|
| 457 |
-
|
| 458 |
-
# Algorithm/profile used when description compression is enabled.
|
| 459 |
-
# Used by: open-sse/mcp-server/descriptionCompressor.ts
|
| 460 |
-
# Set to 0/false/off to skip compression entirely. Default: rtk
|
| 461 |
-
# OMNIROUTE_MCP_DESCRIPTION_COMPRESSION=rtk
|
| 462 |
-
|
| 463 |
-
# Model catalog sync interval in hours.
|
| 464 |
-
# Used by: src/shared/services/modelSyncScheduler.ts — periodic model refresh.
|
| 465 |
-
# Default: 24
|
| 466 |
-
# MODEL_SYNC_INTERVAL_HOURS=24
|
| 467 |
-
|
| 468 |
-
# Provider limits sync interval in minutes (rate limit windows, quotas).
|
| 469 |
-
# Used by: src/server-init.ts — polls provider health endpoints.
|
| 470 |
-
# Default: 70
|
| 471 |
-
PROVIDER_LIMITS_SYNC_INTERVAL_MINUTES=70
|
| 472 |
-
|
| 473 |
-
# Gap (ms) between consecutive OAuth quota fetches in a bulk provider-limits sync.
|
| 474 |
-
# OAuth providers are fetched one at a time with this spacing so a single host
|
| 475 |
-
# never bursts simultaneous usage/refresh requests to the same upstream. Set to 0
|
| 476 |
-
# to opt out (restores fully concurrent fetches). Default: 1500
|
| 477 |
-
PROVIDER_LIMITS_SYNC_SPACING_MS=1500
|
| 478 |
-
|
| 479 |
-
# Disable all background services (sync, pricing, model refresh).
|
| 480 |
-
# Used by: src/instrumentation-node.ts, src/lib/initCloudSync.ts
|
| 481 |
-
# Useful for: CI builds, test environments, or resource-constrained containers.
|
| 482 |
-
# OMNIROUTE_DISABLE_BACKGROUND_SERVICES=false
|
| 483 |
-
|
| 484 |
-
# Force runtime background tasks (healthchecks/sync) even under automated test
|
| 485 |
-
# detection. Used by: src/lib/config/runtimeSettings.ts — overrides the test
|
| 486 |
-
# heuristic in instrumentation-node.ts. Default: unset (tests skip background).
|
| 487 |
-
#OMNIROUTE_ENABLE_RUNTIME_BACKGROUND_TASKS=1
|
| 488 |
-
|
| 489 |
-
# Background job interval for budget reset checks (ms). Default: 600000 (10m).
|
| 490 |
-
# Used by: src/lib/jobs/budgetResetJob.ts. Floor: 10000.
|
| 491 |
-
#OMNIROUTE_BUDGET_RESET_JOB_INTERVAL_MS=600000
|
| 492 |
-
|
| 493 |
-
# Reasoning cache cleanup cadence (ms). Default: 1800000 (30m). Floor: 60000.
|
| 494 |
-
# Used by: src/lib/jobs/reasoningCacheCleanupJob.ts.
|
| 495 |
-
#OMNIROUTE_REASONING_CACHE_CLEANUP_INTERVAL_MS=1800000
|
| 496 |
-
|
| 497 |
-
# Spend write batcher cadence (ms) and buffer size before forced flush.
|
| 498 |
-
# Used by: src/lib/spend/batchWriter.ts. Defaults: 60000 ms / 1000 entries.
|
| 499 |
-
#OMNIROUTE_SPEND_FLUSH_INTERVAL_MS=60000
|
| 500 |
-
#OMNIROUTE_SPEND_MAX_BUFFER_SIZE=1000
|
| 501 |
-
|
| 502 |
-
# Batch request processor retry, backoff, and concurrency settings.
|
| 503 |
-
# Used by: open-sse/services/batchProcessor.ts. Defaults shown.
|
| 504 |
-
#BATCH_RETRY_DURATION_MS=86400000
|
| 505 |
-
#BATCH_BACKOFF_BASE_MS=5000
|
| 506 |
-
#BATCH_BACKOFF_MAX_MS=3600000
|
| 507 |
-
#BATCH_MAX_CONCURRENT=1
|
| 508 |
-
|
| 509 |
-
# Config hot-reload polling interval (ms). Default: 5000.
|
| 510 |
-
# Used by: src/lib/config/hotReload.ts. Lower than 1000ms is rejected.
|
| 511 |
-
#OMNIROUTE_CONFIG_HOT_RELOAD_MS=5000
|
| 512 |
-
|
| 513 |
-
# Override the migrations directory used by src/lib/db/migrationRunner.ts.
|
| 514 |
-
# Default: <repo>/src/lib/db/migrations.
|
| 515 |
-
#OMNIROUTE_MIGRATIONS_DIR=
|
| 516 |
-
|
| 517 |
-
# Trust user-managed RTK project filter rules without strict signature checks.
|
| 518 |
-
# Used by: open-sse/services/compression/engines/rtk/filterLoader.ts. Default: 0.
|
| 519 |
-
#OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=0
|
| 520 |
-
|
| 521 |
-
# Skip the postinstall native-runtime warm-up (useful in CI / headless installs). Default: 0.
|
| 522 |
-
# Used by: scripts/postinstall.mjs.
|
| 523 |
-
#OMNIROUTE_SKIP_POSTINSTALL=0
|
| 524 |
-
|
| 525 |
-
# Skip the DB healthcheck entirely on startup (useful for short-lived tasks / tests).
|
| 526 |
-
# Used by: src/lib/db/core.ts, src/lib/db/healthCheck.ts. Set to 1 to disable. Default: 0.
|
| 527 |
-
#OMNIROUTE_SKIP_DB_HEALTHCHECK=0
|
| 528 |
-
|
| 529 |
-
# Force a DB healthcheck regardless of cadence. Default: 0.
|
| 530 |
-
# Used by: src/lib/db/core.ts::shouldRunDbHealthCheck().
|
| 531 |
-
#OMNIROUTE_FORCE_DB_HEALTHCHECK=0
|
| 532 |
-
|
| 533 |
-
# DB healthcheck cadence override (ms). Default: 21600000 (6h).
|
| 534 |
-
# Used by: src/lib/db/core.ts::getDbHealthCheckIntervalMs().
|
| 535 |
-
#OMNIROUTE_DB_HEALTHCHECK_INTERVAL_MS=21600000
|
| 536 |
-
|
| 537 |
-
# Skip the Redis-backed auth cache used by API key lookups (forces DB reads).
|
| 538 |
-
# Used by: src/lib/db/apiKeys.ts. Set to 1 to disable. Default: enabled.
|
| 539 |
-
#OMNIROUTE_DISABLE_REDIS_AUTH_CACHE=0
|
| 540 |
-
|
| 541 |
-
# Flag set by bootstrap script after initial setup is complete.
|
| 542 |
-
# Used by: src/app/(dashboard)/dashboard/page.tsx — shows setup wizard vs. dashboard.
|
| 543 |
-
# OMNIROUTE_BOOTSTRAPPED=false
|
| 544 |
-
|
| 545 |
-
# Allow request body to override the Antigravity project field.
|
| 546 |
-
# Used by: open-sse/executors/antigravity.ts — escape hatch for multi-project setups.
|
| 547 |
-
# OMNIROUTE_ALLOW_BODY_PROJECT_OVERRIDE=0
|
| 548 |
-
|
| 549 |
-
# Adjust how Antigravity advertises remaining credits. Used by:
|
| 550 |
-
# open-sse/services/antigravityCredits.ts — accepts forced override strings.
|
| 551 |
-
# Default: empty (use upstream-reported credits).
|
| 552 |
-
#ANTIGRAVITY_CREDITS=
|
| 553 |
-
|
| 554 |
-
# Override the path to the Antigravity CLI (agy) token file read by the
|
| 555 |
-
# "auto-detect local login" import. Used by:
|
| 556 |
-
# src/app/api/providers/agy-auth/apply-local/route.ts — for non-standard installs.
|
| 557 |
-
# Default: ~/.gemini/antigravity-cli/antigravity-oauth-token
|
| 558 |
-
#AGY_TOKEN_FILE=
|
| 559 |
-
|
| 560 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 561 |
-
# 11. OAUTH PROVIDER CREDENTIALS
|
| 562 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 563 |
-
# Built-in default credentials for localhost development.
|
| 564 |
-
# For remote/VPS deployments, register your own at each provider's developer console.
|
| 565 |
-
# The bootstrap-env script auto-populates these in .env if missing.
|
| 566 |
-
# Can also be overridden via data/provider-credentials.json where supported.
|
| 567 |
-
|
| 568 |
-
# ── Claude Code (Anthropic) ──
|
| 569 |
-
CLAUDE_OAUTH_CLIENT_ID=9d1c250a-e61b-44d9-88ed-5944d1962f5e
|
| 570 |
-
# Custom redirect URI override for Claude OAuth callback.
|
| 571 |
-
# CLAUDE_CODE_REDIRECT_URI=https://platform.claude.com/oauth/code/callback
|
| 572 |
-
|
| 573 |
-
# ── Codex / OpenAI ──
|
| 574 |
-
CODEX_OAUTH_CLIENT_ID=app_EMoamEEZ73f0CkXaXp7hrann
|
| 575 |
-
|
| 576 |
-
# Milliseconds to wait between consecutive Codex token refreshes.
|
| 577 |
-
# Used by: open-sse/services/refreshSerializer.ts. Default: 0 (no spacing).
|
| 578 |
-
# CODEX_REFRESH_SPACING_MS=0
|
| 579 |
-
|
| 580 |
-
# ── Trae (ByteDance) ──
|
| 581 |
-
# Trae stream idle timeout (ms). Default: 300000 (5 min).
|
| 582 |
-
# Used by: open-sse/executors/trae.ts.
|
| 583 |
-
# TRAE_STREAM_TIMEOUT_MS=300000
|
| 584 |
-
|
| 585 |
-
# Trae OAuth token override. Used by: open-sse/executors/trae.ts.
|
| 586 |
-
# TRAE_TOKEN=
|
| 587 |
-
|
| 588 |
-
# ── The Old LLM (theoldllm) ──
|
| 589 |
-
# Playwright navigation timeout (ms) for the browser-backed token capture.
|
| 590 |
-
# Used by: open-sse/executors/theoldllm.ts. Default: 30000 (30s).
|
| 591 |
-
# THEOLDLLM_NAV_TIMEOUT_MS=30000
|
| 592 |
-
|
| 593 |
-
# ── Gemini / Gemini CLI / Antigravity / Windsurf (all Google-based) ──
|
| 594 |
-
# These providers ship public OAuth client_id/secret values (or Firebase Web
|
| 595 |
-
# keys) embedded in their public CLIs/binaries. Defaults are baked into the
|
| 596 |
-
# code via open-sse/utils/publicCreds.ts — leave the env vars unset to use
|
| 597 |
-
# them. Only set these if you registered your own OAuth app and want to use
|
| 598 |
-
# your own credentials instead. See docs/security/PUBLIC_CREDS.md for context.
|
| 599 |
-
#
|
| 600 |
-
# GEMINI_OAUTH_CLIENT_ID=
|
| 601 |
-
# GEMINI_OAUTH_CLIENT_SECRET=
|
| 602 |
-
# GEMINI_CLI_OAUTH_CLIENT_ID=
|
| 603 |
-
# GEMINI_CLI_OAUTH_CLIENT_SECRET=
|
| 604 |
-
# ANTIGRAVITY_OAUTH_CLIENT_ID=
|
| 605 |
-
# ANTIGRAVITY_OAUTH_CLIENT_SECRET=
|
| 606 |
-
# WINDSURF_FIREBASE_API_KEY=
|
| 607 |
-
|
| 608 |
-
# ── Qwen (Alibaba) ──
|
| 609 |
-
QWEN_OAUTH_CLIENT_ID=f0304373b74a44d2b584a3fb70ca9e56
|
| 610 |
-
|
| 611 |
-
# ── Kimi Coding (Moonshot) ──
|
| 612 |
-
KIMI_CODING_OAUTH_CLIENT_ID=17e5f671-d194-4dfb-9706-5516cb48c098
|
| 613 |
-
|
| 614 |
-
# ── GitHub Copilot ──
|
| 615 |
-
GITHUB_OAUTH_CLIENT_ID=Iv1.b507a08c87ecfe98
|
| 616 |
-
|
| 617 |
-
# ── GitLab Duo ──
|
| 618 |
-
# Register an OAuth app at: https://gitlab.com/-/profile/applications
|
| 619 |
-
# Set redirect URI to: http://localhost:20128/callback (or your NEXT_PUBLIC_BASE_URL + /callback)
|
| 620 |
-
# Required scopes: api, read_user, openid, profile, email
|
| 621 |
-
# GITLAB_DUO_OAUTH_CLIENT_ID=***
|
| 622 |
-
# GITLAB_DUO_OAUTH_CLIENT_SECRET=*** # optional — PKCE flow does not require a secret
|
| 623 |
-
#
|
| 624 |
-
# Self-managed GitLab Duo instance overrides.
|
| 625 |
-
# Used by: src/lib/oauth/gitlab.ts and src/lib/oauth/constants/oauth.ts —
|
| 626 |
-
# fall back to these when the _DUO_ variants above are unset.
|
| 627 |
-
#GITLAB_DUO_BASE_URL=https://gitlab.com
|
| 628 |
-
#GITLAB_BASE_URL=https://gitlab.com
|
| 629 |
-
#GITLAB_OAUTH_CLIENT_ID=
|
| 630 |
-
#GITLAB_OAUTH_CLIENT_SECRET=
|
| 631 |
-
|
| 632 |
-
# ── Qoder ──
|
| 633 |
-
# Public OAuth client secret embedded in the Qoder CLI binary. Required only
|
| 634 |
-
# when QODER_OAUTH_AUTHORIZE_URL / TOKEN_URL / USERINFO_URL / CLIENT_ID are
|
| 635 |
-
# also set (see QODER_CONFIG.enabled in src/lib/oauth/constants/oauth.ts).
|
| 636 |
-
# Extract the value from the public Qoder CLI binary if you intend to use it.
|
| 637 |
-
# QODER_OAUTH_CLIENT_SECRET=
|
| 638 |
-
|
| 639 |
-
# ── Qoder Browser OAuth (experimental) ──
|
| 640 |
-
# OmniRoute only enables the browser OAuth flow when ALL 5 variables below are set:
|
| 641 |
-
# - QODER_OAUTH_AUTHORIZE_URL
|
| 642 |
-
# - QODER_OAUTH_TOKEN_URL
|
| 643 |
-
# - QODER_OAUTH_USERINFO_URL
|
| 644 |
-
# - QODER_OAUTH_CLIENT_ID
|
| 645 |
-
# - QODER_OAUTH_CLIENT_SECRET
|
| 646 |
-
#
|
| 647 |
-
# Redirect URI to register in the Qoder OAuth app:
|
| 648 |
-
# - Localhost dev with PORT=20128: http://localhost:20128/callback
|
| 649 |
-
# - LAN access (example): http://192.168.0.15:20128/callback
|
| 650 |
-
# - Public domain (recommended): https://omniroute.example.com/callback
|
| 651 |
-
#
|
| 652 |
-
# Behind reverse proxy / public domain, also set NEXT_PUBLIC_BASE_URL to the same public origin.
|
| 653 |
-
# If these values are not available, prefer QODER_PERSONAL_ACCESS_TOKEN below.
|
| 654 |
-
# QODER_OAUTH_AUTHORIZE_URL=
|
| 655 |
-
# QODER_OAUTH_TOKEN_URL=
|
| 656 |
-
# QODER_OAUTH_USERINFO_URL=
|
| 657 |
-
# QODER_OAUTH_CLIENT_ID=
|
| 658 |
-
# QODER_OAUTH_CLIENT_SECRET=
|
| 659 |
-
|
| 660 |
-
# ── Qoder Personal Access Token (direct API key fallback) ──
|
| 661 |
-
# Used by: open-sse/executors/qoder.ts — bypasses OAuth when set.
|
| 662 |
-
# QODER_PERSONAL_ACCESS_TOKEN=
|
| 663 |
-
# QODER_CLI_WORKSPACE=
|
| 664 |
-
# OMNIROUTE_QODER_WORKSPACE=
|
| 665 |
-
|
| 666 |
-
# ── Blackbox Web validated-token override (issue #2252) ──
|
| 667 |
-
# Used by: open-sse/executors/blackbox-web.ts. Blackbox `/api/chat` rejects
|
| 668 |
-
# requests whose `validated` field doesn't match the frontend `tk` token,
|
| 669 |
-
# returning HTTP 403 even with a valid session cookie + active subscription.
|
| 670 |
-
# Set this to the `tk` value exported from app.blackbox.ai's Next.js bundle
|
| 671 |
-
# to bypass the random-UUID fallback. Leave empty to keep the legacy behavior.
|
| 672 |
-
# BLACKBOX_WEB_VALIDATED_TOKEN=
|
| 673 |
-
|
| 674 |
-
# ── Vision Bridge OpenAI-compatible endpoint override (issue #2232) ──
|
| 675 |
-
# Used by: src/lib/guardrails/visionBridgeHelpers.ts. By default the
|
| 676 |
-
# vision-bridge guardrail sends non-Anthropic image-description calls to
|
| 677 |
-
# `https://api.openai.com/v1`, which fails with 401 if your operator doesn't
|
| 678 |
-
# have an OpenAI key or wants to use a different vision model
|
| 679 |
-
# (e.g., `google/gemini-2.0-flash` via the Gemini OpenAI-compat endpoint, or
|
| 680 |
-
# any model registered in OmniRoute via the self-loop endpoint).
|
| 681 |
-
#
|
| 682 |
-
# Set these two env vars to point the bridge at any OpenAI-compatible URL:
|
| 683 |
-
# - VISION_BRIDGE_BASE_URL=http://localhost:20128/v1 (OmniRoute self-loop)
|
| 684 |
-
# - VISION_BRIDGE_BASE_URL=https://generativelanguage.googleapis.com/v1beta/openai
|
| 685 |
-
# - VISION_BRIDGE_BASE_URL=https://openrouter.ai/api/v1
|
| 686 |
-
# Anthropic models (anthropic/*) keep their dedicated path and are unaffected.
|
| 687 |
-
# VISION_BRIDGE_BASE_URL=
|
| 688 |
-
# VISION_BRIDGE_API_KEY=
|
| 689 |
-
|
| 690 |
-
# ─────────────────────────────────────────────────────────────────────────────
|
| 691 |
-
# ⚠️ GOOGLE OAUTH (Antigravity, Gemini CLI) & OTHER PROVIDERS — REMOTE SERVERS
|
| 692 |
-
# ─────────────────────────────────────────────────────────────────────────────
|
| 693 |
-
# The default Client IDs above ONLY work when OmniRoute runs on localhost.
|
| 694 |
-
# For remote/VPS hosting (including Docker containers on remote servers):
|
| 695 |
-
# 1. By default, the browser will attempt OAuth redirects back to localhost, which will fail.
|
| 696 |
-
# 2. Set NEXT_PUBLIC_BASE_URL=https://your-domain.com to fix the redirect URI.
|
| 697 |
-
# 3. You MUST create your own OAuth App in each provider's developer console (Google Cloud, etc.)
|
| 698 |
-
# and set the Authorized redirect URI to your domain (e.g., https://your-domain.com/callback).
|
| 699 |
-
# 4. Replace the _OAUTH_CLIENT_ID and _SECRET values above with your own credentials.
|
| 700 |
-
# ─────────────────────────────────────────────────────────────────────────────
|
| 701 |
-
|
| 702 |
-
# ── OAuth sidecar/CLI bridge (internal) ──
|
| 703 |
-
# Used by: src/lib/oauth/config/index.ts — internal CLI↔OmniRoute auth bridge.
|
| 704 |
-
# OMNIROUTE_SERVER=http://localhost:20128
|
| 705 |
-
# OMNIROUTE_TOKEN=
|
| 706 |
-
# OMNIROUTE_USER_ID=cli
|
| 707 |
-
# CLI_TOKEN= # legacy alias for OMNIROUTE_TOKEN
|
| 708 |
-
# CLI_USER_ID= # legacy alias for OMNIROUTE_USER_ID
|
| 709 |
-
# SERVER_URL= # legacy alias for OMNIROUTE_SERVER
|
| 710 |
-
|
| 711 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 712 |
-
# 12. PROVIDER USER-AGENT OVERRIDES
|
| 713 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 714 |
-
# Customize the User-Agent header sent to each upstream provider.
|
| 715 |
-
# Format: {PROVIDER_ID}_USER_AGENT=custom-value
|
| 716 |
-
# Used by: open-sse/executors/base.ts — buildHeaders() dynamic lookup.
|
| 717 |
-
# Update these when providers release new CLI versions to avoid blocks.
|
| 718 |
-
|
| 719 |
-
CLAUDE_USER_AGENT="claude-cli/2.1.158 (external, cli)"
|
| 720 |
-
|
| 721 |
-
# Disable the deterministic tool-name cloak applied on both Anthropic-bound paths
|
| 722 |
-
# (executors/base.ts native OAuth + executors/cliproxyapi.ts CLIProxyAPI) ���
|
| 723 |
-
# third-party-harness tool names are aliased to
|
| 724 |
-
# Claude Code canonical or PascalCase forms so Anthropic does not refuse the
|
| 725 |
-
# stream with a misleading 400 out-of-extra-usage placeholder. Set to true to
|
| 726 |
-
# forward the original names verbatim (debugging only).
|
| 727 |
-
# CLAUDE_DISABLE_TOOL_NAME_CLOAK=false
|
| 728 |
-
CODEX_USER_AGENT="codex-cli/0.132.0 (Windows 10.0.26200; x64)"
|
| 729 |
-
GITHUB_USER_AGENT="GitHubCopilotChat/0.45.1"
|
| 730 |
-
ANTIGRAVITY_USER_AGENT="antigravity/2.0.1 linux/arm64 google-api-nodejs-client/10.3.0"
|
| 731 |
-
KIRO_USER_AGENT="AWS-SDK-JS/3.0.0 kiro-ide/1.0.0"
|
| 732 |
-
# Optional override for the Kiro social device-code OAuth clientId. Kiro's
|
| 733 |
-
# device endpoint accepts any non-empty string and behaves like a User-Agent
|
| 734 |
-
# rather than a secret. Only override if AWS ever starts enforcing this field.
|
| 735 |
-
# Used by: src/lib/oauth/constants/oauth.ts (KIRO_CONFIG.socialClientId).
|
| 736 |
-
# KIRO_OAUTH_CLIENT_ID=kiro-cli
|
| 737 |
-
QODER_USER_AGENT="Qoder-Cli"
|
| 738 |
-
QWEN_USER_AGENT="QwenCode/0.15.11 (linux; x64)"
|
| 739 |
-
CURSOR_USER_AGENT="Cursor/3.4"
|
| 740 |
-
GEMINI_CLI_USER_AGENT="google-api-nodejs-client/10.3.0"
|
| 741 |
-
|
| 742 |
-
# Override Codex client version sent in headers independently of the
|
| 743 |
-
# CODEX_USER_AGENT string. Used by: open-sse/config/codexClient.ts.
|
| 744 |
-
# CODEX_CLIENT_VERSION=0.132.0
|
| 745 |
-
|
| 746 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 747 |
-
# 13. CLI FINGERPRINT COMPATIBILITY (Anti-Detection)
|
| 748 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 749 |
-
# When enabled, OmniRoute reorders HTTP headers and JSON body fields to match
|
| 750 |
-
# the exact signature of official CLI tools, reducing account flagging risk.
|
| 751 |
-
# Your proxy IP is preserved — you get both stealth AND IP masking.
|
| 752 |
-
# Used by: open-sse/config/cliFingerprints.ts, open-sse/executors/base.ts
|
| 753 |
-
|
| 754 |
-
# Enable per-provider:
|
| 755 |
-
# CLI_COMPAT_CODEX=1
|
| 756 |
-
# CLI_COMPAT_CLAUDE=1
|
| 757 |
-
# CLI_COMPAT_GITHUB=1
|
| 758 |
-
# CLI_COMPAT_ANTIGRAVITY=1
|
| 759 |
-
# CLI_COMPAT_CURSOR=1
|
| 760 |
-
# CLI_COMPAT_KIMI_CODING=1
|
| 761 |
-
# CLI_COMPAT_KILOCODE=1
|
| 762 |
-
# CLI_COMPAT_CLINE=1
|
| 763 |
-
# CLI_COMPAT_QWEN=1
|
| 764 |
-
|
| 765 |
-
# Or enable for all providers at once:
|
| 766 |
-
# CLI_COMPAT_ALL=1
|
| 767 |
-
|
| 768 |
-
# ── Kimi Coding CLI identity overrides ──
|
| 769 |
-
# Used by: src/lib/oauth/providers/kimi-coding.ts — sent in OAuth + API headers.
|
| 770 |
-
# Leave unset to use the captured defaults baked into the OmniRoute build.
|
| 771 |
-
#KIMI_CLI_VERSION=1.36.0
|
| 772 |
-
#KIMI_CODING_DEVICE_ID=
|
| 773 |
-
|
| 774 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 775 |
-
# 14. API KEY PROVIDERS
|
| 776 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 777 |
-
# API keys for direct-authentication providers.
|
| 778 |
-
# Preferred setup: Dashboard → Providers → Add API Key.
|
| 779 |
-
# Setting here is an alternative for Docker/headless deployments.
|
| 780 |
-
|
| 781 |
-
# Static API keys for direct-authentication providers wired through the runtime.
|
| 782 |
-
# OmniRoute loads provider credentials from the encrypted database or
|
| 783 |
-
# data/provider-credentials.json. The variables below are documented escape
|
| 784 |
-
# hatches that are referenced in code today.
|
| 785 |
-
# DEEPSEEK_API_KEY=
|
| 786 |
-
# NVIDIA_API_KEY=
|
| 787 |
-
|
| 788 |
-
# Windsurf / Devin CLI direct API key.
|
| 789 |
-
# Used by: open-sse/executors/devin-cli.ts — bypasses OAuth when set.
|
| 790 |
-
# WINDSURF_API_KEY=
|
| 791 |
-
|
| 792 |
-
# Embedding Providers (optional — used by /v1/embeddings)
|
| 793 |
-
# OpenAI/Mistral/Together/Fireworks/NVIDIA configured via Dashboard → Providers
|
| 794 |
-
# also work for embeddings.
|
| 795 |
-
|
| 796 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 797 |
-
# 15. TIMEOUT SETTINGS
|
| 798 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 799 |
-
# All timeout values are in milliseconds.
|
| 800 |
-
# Used by: src/shared/utils/runtimeTimeouts.ts — centralized timeout resolution.
|
| 801 |
-
#
|
| 802 |
-
# Hierarchy: REQUEST_TIMEOUT_MS acts as a global override.
|
| 803 |
-
# If set, it becomes the default for FETCH_TIMEOUT_MS and STREAM_IDLE_TIMEOUT_MS.
|
| 804 |
-
# The fine-grained variables below override their respective defaults only when set.
|
| 805 |
-
|
| 806 |
-
# ── Global shortcut ──
|
| 807 |
-
# REQUEST_TIMEOUT_MS=600000 # Overrides both fetch and stream idle defaults
|
| 808 |
-
|
| 809 |
-
# ── Upstream fetch (provider calls) ──
|
| 810 |
-
# FETCH_TIMEOUT_MS=600000 # Total request timeout (default: 600000 = 10 min)
|
| 811 |
-
# # Also drives anthropic-compatible-cc-* X-Stainless-Timeout.
|
| 812 |
-
# FETCH_HEADERS_TIMEOUT_MS=600000 # Time to receive response headers
|
| 813 |
-
# FETCH_BODY_TIMEOUT_MS=600000 # Time to receive full response body
|
| 814 |
-
# FETCH_CONNECT_TIMEOUT_MS=30000 # TCP connection establishment (default: 30s)
|
| 815 |
-
# FETCH_KEEPALIVE_TIMEOUT_MS=4000 # Keep-alive socket idle timeout (default: 4s)
|
| 816 |
-
|
| 817 |
-
# Default timeout (ms) for src/shared/utils/fetchTimeout.ts. Acts as the
|
| 818 |
-
# fallback when FETCH_TIMEOUT_MS is unset. Default: 120000 (2 min).
|
| 819 |
-
# OMNIROUTE_DEFAULT_FETCH_TIMEOUT_MS=120000
|
| 820 |
-
|
| 821 |
-
# ── ChatGPT TLS sidecar (Firefox-fingerprinted client) ──
|
| 822 |
-
# Used by: open-sse/services/chatgptTlsClient.ts — wire-level timeout for
|
| 823 |
-
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
| 824 |
-
# layered on top of it when the native library is wedged.
|
| 825 |
-
# OMNIROUTE_CHATGPT_TLS_TIMEOUT_MS=60000
|
| 826 |
-
# OMNIROUTE_CHATGPT_TLS_GRACE_MS=10000
|
| 827 |
-
|
| 828 |
-
# ── Claude TLS sidecar (Chromium-fingerprinted client) ──
|
| 829 |
-
# Used by: open-sse/services/claudeTlsClient.ts — wire-level timeout for
|
| 830 |
-
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
| 831 |
-
# layered on top of it when the native library is wedged.
|
| 832 |
-
# OMNIROUTE_CLAUDE_TLS_TIMEOUT_MS=60000
|
| 833 |
-
# OMNIROUTE_CLAUDE_TLS_GRACE_MS=10000
|
| 834 |
-
|
| 835 |
-
# ── Perplexity TLS sidecar (Firefox-fingerprinted client) ──
|
| 836 |
-
# Used by: open-sse/services/perplexityTlsClient.ts — wire-level timeout for
|
| 837 |
-
# the bogdanfinn/tls-client koffi binding and the JS-side grace window
|
| 838 |
-
# layered on top of it when the native library is wedged.
|
| 839 |
-
# OMNIROUTE_PPLX_TLS_TIMEOUT_MS=30000
|
| 840 |
-
# OMNIROUTE_PPLX_TLS_GRACE_MS=10000
|
| 841 |
-
|
| 842 |
-
# ── Grok web TLS sidecar (Chrome-fingerprinted client) ──
|
| 843 |
-
# Used by: open-sse/services/grokTlsClient.ts — wire-level timeout for the
|
| 844 |
-
# bogdanfinn/tls-client koffi binding and the JS-side grace window layered on
|
| 845 |
-
# top of it when the native library is wedged.
|
| 846 |
-
# OMNIROUTE_GROK_TLS_TIMEOUT_MS=60000
|
| 847 |
-
# OMNIROUTE_GROK_TLS_GRACE_MS=10000
|
| 848 |
-
|
| 849 |
-
# ── Circuit breaker thresholds and reset windows ──
|
| 850 |
-
# Used by: open-sse/config/constants.ts → src/lib/resilience/settings.ts.
|
| 851 |
-
# Defaults match historical PROVIDER_PROFILES values (post-scaling for
|
| 852 |
-
# 500+ connections). Lower the threshold to react faster, raise it to
|
| 853 |
-
# tolerate more transient failures before short-circuiting.
|
| 854 |
-
# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_THRESHOLD=8
|
| 855 |
-
# OMNIROUTE_CIRCUIT_BREAKER_OAUTH_RESET_MS=60000
|
| 856 |
-
# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_THRESHOLD=12
|
| 857 |
-
# OMNIROUTE_CIRCUIT_BREAKER_API_KEY_RESET_MS=30000
|
| 858 |
-
# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_THRESHOLD=2
|
| 859 |
-
# OMNIROUTE_CIRCUIT_BREAKER_LOCAL_RESET_MS=15000
|
| 860 |
-
|
| 861 |
-
# ── Stream idle detection ──
|
| 862 |
-
# STREAM_IDLE_TIMEOUT_MS=600000 # Max silence between SSE chunks (default: 600000)
|
| 863 |
-
# # Extended-thinking models rarely pause >90s.
|
| 864 |
-
|
| 865 |
-
# ── TLS client (wreq-js fingerprint proxy) ──
|
| 866 |
-
# TLS_CLIENT_TIMEOUT_MS=600000 # Inherits from FETCH_TIMEOUT_MS by default
|
| 867 |
-
|
| 868 |
-
# ── API Bridge (/v1 proxy server) ──
|
| 869 |
-
# API_BRIDGE_PROXY_TIMEOUT_MS=600000 # Proxy hop timeout (default: 10min)
|
| 870 |
-
# API_BRIDGE_SERVER_REQUEST_TIMEOUT_MS=600000 # Overall server request timeout (default: 10min)
|
| 871 |
-
# API_BRIDGE_SERVER_HEADERS_TIMEOUT_MS=60000 # Time to send response headers
|
| 872 |
-
# API_BRIDGE_SERVER_KEEPALIVE_TIMEOUT_MS=5000 # Keep-alive idle timeout
|
| 873 |
-
# API_BRIDGE_SERVER_SOCKET_TIMEOUT_MS=0 # Raw socket timeout (0 = disabled)
|
| 874 |
-
|
| 875 |
-
# ── Graceful shutdown ──
|
| 876 |
-
# Time to wait for in-flight requests before force-exiting on SIGTERM/SIGINT.
|
| 877 |
-
# Used by: src/lib/gracefulShutdown.ts
|
| 878 |
-
# Default: 30000 (30 seconds)
|
| 879 |
-
# SHUTDOWN_TIMEOUT_MS=30000
|
| 880 |
-
|
| 881 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 882 |
-
# 16. LOGGING
|
| 883 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 884 |
-
# Used by: src/lib/logEnv.ts, src/lib/logRotation.ts, src/shared/utils/logger.ts
|
| 885 |
-
|
| 886 |
-
# Application log level — controls console and file log verbosity.
|
| 887 |
-
# Values: debug | info | warn | error | Default: info
|
| 888 |
-
# APP_LOG_LEVEL=info
|
| 889 |
-
|
| 890 |
-
# Log output format.
|
| 891 |
-
# Values: text | json | Default: text
|
| 892 |
-
# APP_LOG_FORMAT=text
|
| 893 |
-
|
| 894 |
-
# Write logs to file in addition to stdout.
|
| 895 |
-
# Default: true | Set false to disable file logging.
|
| 896 |
-
APP_LOG_TO_FILE=true
|
| 897 |
-
|
| 898 |
-
# Path to the application log file.
|
| 899 |
-
# Default: logs/application/app.log (relative to project root / DATA_DIR)
|
| 900 |
-
# APP_LOG_FILE_PATH=logs/application/app.log
|
| 901 |
-
|
| 902 |
-
# Maximum single log file size before rotation.
|
| 903 |
-
# Accepts: plain bytes or suffixed (50M, 1G, 512K). Default: 50M
|
| 904 |
-
# APP_LOG_MAX_FILE_SIZE=50M
|
| 905 |
-
|
| 906 |
-
# Days to keep rotated application log files before auto-deletion.
|
| 907 |
-
# Default: 7
|
| 908 |
-
# APP_LOG_RETENTION_DAYS=7
|
| 909 |
-
|
| 910 |
-
# Maximum number of rotated log file backups to keep.
|
| 911 |
-
# Default: 20
|
| 912 |
-
# APP_LOG_MAX_FILES=20
|
| 913 |
-
|
| 914 |
-
# How often OmniRoute checks whether the active log file has exceeded
|
| 915 |
-
# APP_LOG_MAX_FILE_SIZE and triggers a rotation. Set lower for very verbose
|
| 916 |
-
# services to prevent log files from growing large between checks.
|
| 917 |
-
# Accepts milliseconds. Default: 60000 (1 minute)
|
| 918 |
-
# APP_LOG_ROTATION_CHECK_INTERVAL_MS=60000
|
| 919 |
-
|
| 920 |
-
# Days to keep request/call log entries in the database before auto-cleanup.
|
| 921 |
-
# Default: 7
|
| 922 |
-
# CALL_LOG_RETENTION_DAYS=7
|
| 923 |
-
|
| 924 |
-
# Maximum call log entries stored in-memory buffer.
|
| 925 |
-
# Default: 10000
|
| 926 |
-
# CALL_LOG_MAX_ENTRIES=10000
|
| 927 |
-
|
| 928 |
-
# Maximum rows in the call_logs SQLite table before oldest entries are pruned.
|
| 929 |
-
# Default: 100000
|
| 930 |
-
# CALL_LOGS_TABLE_MAX_ROWS=100000
|
| 931 |
-
|
| 932 |
-
# Whether call log pipeline capture stores stream chunks when enabled in settings.
|
| 933 |
-
# Only applies when call_log_pipeline_enabled=true.
|
| 934 |
-
# Default: true
|
| 935 |
-
# CALL_LOG_PIPELINE_CAPTURE_STREAM_CHUNKS=true
|
| 936 |
-
|
| 937 |
-
# Maximum call log artifact size for pipeline captures, in KB.
|
| 938 |
-
# Only applies when call_log_pipeline_enabled=true.
|
| 939 |
-
# Default: 512
|
| 940 |
-
# CALL_LOG_PIPELINE_MAX_SIZE_KB=512
|
| 941 |
-
|
| 942 |
-
# Call log payload truncation limits — controls how much of request/response
|
| 943 |
-
# bodies is retained in the database.
|
| 944 |
-
# Used by: open-sse/handlers/chatCore.ts — cloneBoundedChatLogPayload()
|
| 945 |
-
# CHAT_LOG_TEXT_LIMIT=65536 # Max string length before truncation (default: 64 KB)
|
| 946 |
-
# CHAT_LOG_ARRAY_TAIL_ITEMS=24 # Number of array items retained from tail (default: 24)
|
| 947 |
-
# CHAT_LOG_MAX_DEPTH=6 # Max nesting depth before truncation (default: 6)
|
| 948 |
-
# CHAT_LOG_MAX_OBJECT_KEYS=80 # Max object keys retained (default: 80, 0 = no limit)
|
| 949 |
-
|
| 950 |
-
# Maximum rows in the proxy_logs SQLite table.
|
| 951 |
-
# Default: 100000
|
| 952 |
-
# PROXY_LOGS_TABLE_MAX_ROWS=100000
|
| 953 |
-
|
| 954 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 955 |
-
# 17. MEMORY OPTIMIZATION (Low-RAM / Docker)
|
| 956 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 957 |
-
|
| 958 |
-
# Node.js V8 heap limit in MB, passed to the server via --max-old-space-size.
|
| 959 |
-
# Used by the standalone launcher (Docker CMD) and `omniroute serve`.
|
| 960 |
-
# Clamped to [64, 16384]. Default: 512 (safe for a 1 GB / 1 core VPS). Size it to
|
| 961 |
-
# roughly half the box's RAM, leaving the rest for native memory (better-sqlite3,
|
| 962 |
-
# buffers — ~300 MB) and the OS:
|
| 963 |
-
# 1 GB RAM → 512 (default)
|
| 964 |
-
# 2 GB RAM → 1024
|
| 965 |
-
# 4 GB RAM → 2048
|
| 966 |
-
# In a memory-capped container, set this EXPLICITLY: Node reads the HOST's RAM,
|
| 967 |
-
# not the cgroup limit, so leaving it to a RAM heuristic can oversize the heap and
|
| 968 |
-
# get the container OOM-killed. (#2939)
|
| 969 |
-
# OMNIROUTE_MEMORY_MB=512
|
| 970 |
-
|
| 971 |
-
# Heap-pressure shed threshold (MB) — chatCore returns 503 when V8 heapUsed exceeds
|
| 972 |
-
# it, to avoid hard OOM under concurrent large-context load.
|
| 973 |
-
# LEAVE UNSET: it now AUTO-CALIBRATES to 85% of the actual V8 heap ceiling, so it
|
| 974 |
-
# tracks OMNIROUTE_MEMORY_MB above and never sits below the ~260 MB runtime baseline
|
| 975 |
-
# (a fixed 200 here used to reject every request). Used by: open-sse/utils/heapPressure.ts.
|
| 976 |
-
# Override only to hand-tune for a known workload.
|
| 977 |
-
# HEAP_PRESSURE_THRESHOLD_MB=
|
| 978 |
-
|
| 979 |
-
# ── CLI helpers (bin/cli/) ──
|
| 980 |
-
# Override UI language for CLI output. Accepts BCP-47 locale (e.g. en, pt-BR).
|
| 981 |
-
# Falls back to LC_ALL / LC_MESSAGES / LANG / en if unset.
|
| 982 |
-
# OMNIROUTE_LANG=en
|
| 983 |
-
|
| 984 |
-
# Show server logs inline when running in supervised mode (omniroute serve).
|
| 985 |
-
# Set to "1" to forward server stdout/stderr to the terminal.
|
| 986 |
-
# Equivalent to the --log flag on `omniroute serve`.
|
| 987 |
-
# OMNIROUTE_SHOW_LOG=1
|
| 988 |
-
|
| 989 |
-
# Bearer token injected as x-omniroute-cli-token header for machine-auth (task 8.12).
|
| 990 |
-
# Auto-generated on first run if machine-id is available; set manually to override.
|
| 991 |
-
# OMNIROUTE_CLI_TOKEN=
|
| 992 |
-
|
| 993 |
-
# Per-attempt HTTP timeout for CLI → server calls (milliseconds). Default: 30000.
|
| 994 |
-
# OMNIROUTE_HTTP_TIMEOUT_MS=30000
|
| 995 |
-
|
| 996 |
-
# Set to 1 to print retry/backoff details to stderr during CLI commands.
|
| 997 |
-
# OMNIROUTE_VERBOSE=0
|
| 998 |
-
|
| 999 |
-
# Custom directory for CLI plugin discovery (omniroute-cmd-* packages).
|
| 1000 |
-
# Default: ~/.omniroute/plugins/ Override in dev/CI to point at a local plugin tree.
|
| 1001 |
-
# OMNIROUTE_PLUGIN_PATH=
|
| 1002 |
-
|
| 1003 |
-
# Allow plugins to request the 'exec' permission (spawn child processes from the
|
| 1004 |
-
# plugin worker sandbox). Disabled by default; set to 1 to enable (local operator only).
|
| 1005 |
-
# OMNIROUTE_PLUGINS_ALLOW_EXEC=0
|
| 1006 |
-
|
| 1007 |
-
# ── Prompt cache (system prompt deduplication) ──
|
| 1008 |
-
# Used by: open-sse/services — caches identical system prompts across requests.
|
| 1009 |
-
# PROMPT_CACHE_MAX_SIZE=50 # Max cached entries (default: 50)
|
| 1010 |
-
# PROMPT_CACHE_MAX_BYTES=2097152 # Max total cache size in bytes (default: 2 MB)
|
| 1011 |
-
# PROMPT_CACHE_TTL_MS=300000 # Cache entry TTL (default: 5 minutes)
|
| 1012 |
-
|
| 1013 |
-
# ── Semantic cache (deterministic response dedup, temperature=0) ──
|
| 1014 |
-
# Used by: open-sse/services — caches identical temperature=0 responses.
|
| 1015 |
-
# SEMANTIC_CACHE_MAX_SIZE=100 # Max cached entries (default: 100)
|
| 1016 |
-
# SEMANTIC_CACHE_MAX_BYTES=4194304 # Max total cache size in bytes (default: 4 MB)
|
| 1017 |
-
# SEMANTIC_CACHE_TTL_MS=1800000 # Cache entry TTL (default: 30 minutes)
|
| 1018 |
-
|
| 1019 |
-
# ── In-memory log buffers ──
|
| 1020 |
-
# Maximum recent stream events kept in memory for the Dashboard live view.
|
| 1021 |
-
# STREAM_HISTORY_MAX=50
|
| 1022 |
-
|
| 1023 |
-
# ── Context length default ──
|
| 1024 |
-
# Global fallback max context length for models without explicit config.
|
| 1025 |
-
# Used by: open-sse/services/contextManager.ts
|
| 1026 |
-
# CONTEXT_LENGTH_DEFAULT=128000
|
| 1027 |
-
|
| 1028 |
-
# ── Usage token buffer ──
|
| 1029 |
-
# Extra token headroom reserved when tracking usage quotas (prevents over-limit).
|
| 1030 |
-
# Used by: open-sse/utils/usageTracking.ts
|
| 1031 |
-
# USAGE_TOKEN_BUFFER=100
|
| 1032 |
-
|
| 1033 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1034 |
-
# 18. PRICING SYNC
|
| 1035 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1036 |
-
# Automatic model pricing synchronization from external sources.
|
| 1037 |
-
# Used by: src/lib/pricingSync.ts
|
| 1038 |
-
|
| 1039 |
-
# Enable periodic pricing data sync. Default: false (opt-in only).
|
| 1040 |
-
# PRICING_SYNC_ENABLED=false
|
| 1041 |
-
|
| 1042 |
-
# Sync interval in seconds. Default: 86400 (24 hours).
|
| 1043 |
-
# PRICING_SYNC_INTERVAL=86400
|
| 1044 |
-
|
| 1045 |
-
# Comma-separated data sources. Default: litellm
|
| 1046 |
-
# PRICING_SYNC_SOURCES=litellm
|
| 1047 |
-
|
| 1048 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1049 |
-
# 19. MODEL SYNC (Dev)
|
| 1050 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1051 |
-
# Development-time model catalog sync interval in seconds.
|
| 1052 |
-
# Used by: src/lib/modelsDevSync.ts
|
| 1053 |
-
# Default: 86400 (24 hours)
|
| 1054 |
-
# MODELS_DEV_SYNC_INTERVAL=86400
|
| 1055 |
-
|
| 1056 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1057 |
-
# 20. PROVIDER-SPECIFIC SETTINGS
|
| 1058 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1059 |
-
|
| 1060 |
-
# ── OpenRouter ──
|
| 1061 |
-
# OpenRouter model catalog cache TTL in ms.
|
| 1062 |
-
# Used by: src/lib/catalog/openrouterCatalog.ts
|
| 1063 |
-
# Default: 86400000 (24 hours)
|
| 1064 |
-
# OPENROUTER_CATALOG_TTL_MS=86400000
|
| 1065 |
-
|
| 1066 |
-
# ── NanoBanana (Image Generation) ──
|
| 1067 |
-
# Polling config for async image generation jobs.
|
| 1068 |
-
# Used by: open-sse/handlers/imageGeneration.ts
|
| 1069 |
-
# NANOBANANA_POLL_TIMEOUT_MS=120000 # Max wait for job completion (default: 120s)
|
| 1070 |
-
# NANOBANANA_POLL_INTERVAL_MS=2500 # Poll frequency (default: 2.5s)
|
| 1071 |
-
|
| 1072 |
-
# ── AWS Bedrock (Kiro / Audio) ──
|
| 1073 |
-
# Region used to construct AWS Bedrock endpoints. Used by:
|
| 1074 |
-
# src/lib/providers/validation.ts and open-sse/handlers/audioSpeech.ts.
|
| 1075 |
-
# AWS_REGION takes precedence over AWS_DEFAULT_REGION when both are set.
|
| 1076 |
-
# AWS_REGION=us-east-1
|
| 1077 |
-
# AWS_DEFAULT_REGION=us-east-1
|
| 1078 |
-
|
| 1079 |
-
# ── Cloudflare Workers AI ──
|
| 1080 |
-
# Account ID override for Cloudflare Workers AI executor.
|
| 1081 |
-
# Used by: open-sse/executors/cloudflare-ai.ts
|
| 1082 |
-
# CLOUDFLARE_ACCOUNT_ID=
|
| 1083 |
-
|
| 1084 |
-
# ── Cloudflare Tunnel (cloudflared) ──
|
| 1085 |
-
# Custom path to cloudflared binary for tunnel management.
|
| 1086 |
-
# Used by: src/lib/cloudflaredTunnel.ts
|
| 1087 |
-
# CLOUDFLARED_BIN=/usr/local/bin/cloudflared
|
| 1088 |
-
|
| 1089 |
-
# ── Search cache ──
|
| 1090 |
-
# TTL for search API response caching (Perplexity, Brave, etc.).
|
| 1091 |
-
# Used by: open-sse/services/searchCache.ts
|
| 1092 |
-
# Default: 300000 (5 minutes)
|
| 1093 |
-
# SEARCH_CACHE_TTL_MS=300000
|
| 1094 |
-
|
| 1095 |
-
# ── OpenAI-compatible multi-connection ──
|
| 1096 |
-
# Allow multiple simultaneous connections per OpenAI-compatible provider node.
|
| 1097 |
-
# Used by: src/app/api/providers/route.ts
|
| 1098 |
-
# ALLOW_MULTI_CONNECTIONS_PER_COMPAT_NODE=false
|
| 1099 |
-
|
| 1100 |
-
# ── CC-compatible provider (experimental) ──
|
| 1101 |
-
# Enable the Claude Code compatible provider endpoint.
|
| 1102 |
-
# This is only for third-party relays that accept Claude Code clients exclusively.
|
| 1103 |
-
# OmniRoute rewrites requests to pass those relays' Claude Code client validation.
|
| 1104 |
-
# If you only want to use Claude Code CLI, or you are not sure what these relays are,
|
| 1105 |
-
# keep this disabled and add a regular Anthropic-compatible provider instead.
|
| 1106 |
-
# Used by: src/shared/utils/featureFlags.ts
|
| 1107 |
-
# ENABLE_CC_COMPATIBLE_PROVIDER=false
|
| 1108 |
-
|
| 1109 |
-
# ── 9router embedded service ──
|
| 1110 |
-
# Override the host/port where the embedded 9router instance listens.
|
| 1111 |
-
# Rarely needed — defaults match the bootstrap config (127.0.0.1:20130).
|
| 1112 |
-
# Used by: open-sse/executors/ninerouter.ts
|
| 1113 |
-
# NINEROUTER_HOST=127.0.0.1
|
| 1114 |
-
# NINEROUTER_PORT=20130
|
| 1115 |
-
|
| 1116 |
-
# ── Embedded service WebSocket proxy ──
|
| 1117 |
-
# Standalone WebSocket proxy that tunnels WS connections to embedded services.
|
| 1118 |
-
# Binds to loopback by default. Only change EMBED_WS_PROXY_HOST if you know
|
| 1119 |
-
# what you are doing — exposing this to non-loopback bypasses local-only policy.
|
| 1120 |
-
# Used by: src/lib/services/embedWsProxy.ts
|
| 1121 |
-
# EMBED_WS_PROXY_HOST=127.0.0.1
|
| 1122 |
-
# EMBED_WS_PROXY_PORT=20131
|
| 1123 |
-
|
| 1124 |
-
# ── CLIProxyAPI bridge (legacy) ──
|
| 1125 |
-
# Connection settings for external CLIProxyAPI instances.
|
| 1126 |
-
# Used by: open-sse/executors/cliproxyapi.ts
|
| 1127 |
-
# CLIPROXYAPI_HOST=127.0.0.1
|
| 1128 |
-
# CLIPROXYAPI_PORT=5544
|
| 1129 |
-
# CLIPROXYAPI_CONFIG_DIR=~/.cli-proxy-api
|
| 1130 |
-
|
| 1131 |
-
# ── Local hostnames (Docker networking) ──
|
| 1132 |
-
# Comma-separated additional hostnames treated as "local" for provider routing.
|
| 1133 |
-
# Used by: open-sse/config/providerRegistry.ts — allows Docker service names.
|
| 1134 |
-
# LOCAL_HOSTNAMES=omlx,mlx-audio
|
| 1135 |
-
|
| 1136 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1137 |
-
# 21. PROXY HEALTH
|
| 1138 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1139 |
-
# Fine-tune proxy health checking behavior.
|
| 1140 |
-
# Used by: src/lib/proxyHealth.ts
|
| 1141 |
-
|
| 1142 |
-
# Timeout for fast-fail health checks (ms). Default: 2000
|
| 1143 |
-
# PROXY_FAST_FAIL_TIMEOUT_MS=2000
|
| 1144 |
-
|
| 1145 |
-
# Health check result cache TTL (ms). Default: 30000 (30s)
|
| 1146 |
-
# PROXY_HEALTH_CACHE_TTL_MS=30000
|
| 1147 |
-
|
| 1148 |
-
# Rate limit maximum wait time before failing a request (ms). Default: 120000 (2 min)
|
| 1149 |
-
# Used by: open-sse/services/rateLimitManager.ts
|
| 1150 |
-
# RATE_LIMIT_MAX_WAIT_MS=120000
|
| 1151 |
-
|
| 1152 |
-
# Force the auto-enable rate limit safety net on/off regardless of the persisted
|
| 1153 |
-
# Dashboard setting. Used by: open-sse/services/rateLimitManager.ts.
|
| 1154 |
-
# Accepted values: true|1|on (force on), false|0|off (force off), unset (use Dashboard).
|
| 1155 |
-
# RATE_LIMIT_AUTO_ENABLE=
|
| 1156 |
-
|
| 1157 |
-
# Stagger interval (ms) between provider token healthchecks at startup.
|
| 1158 |
-
# Used by: src/lib/tokenHealthCheck.ts. Default: 3000.
|
| 1159 |
-
# HEALTHCHECK_STAGGER_MS=3000
|
| 1160 |
-
|
| 1161 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1162 |
-
# 22. DEBUGGING
|
| 1163 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1164 |
-
# These variables enable verbose debugging output. NEVER enable in production.
|
| 1165 |
-
|
| 1166 |
-
# Cursor executor verbose debug (decoded SSE chunks, etc.).
|
| 1167 |
-
# CURSOR_STREAM_DEBUG is kept as a backward-compatible alias.
|
| 1168 |
-
# Used by: open-sse/executors/cursor.ts
|
| 1169 |
-
# CURSOR_DEBUG=1
|
| 1170 |
-
|
| 1171 |
-
# Enable verbose trace logging for OmniRoute internals.
|
| 1172 |
-
# Used by: open-sse/handlers/chatCore.ts.
|
| 1173 |
-
# OMNIRROUTE_TRACE=true
|
| 1174 |
-
|
| 1175 |
-
# Standard DEBUG flag (same effect as OMNIRROUTE_TRACE).
|
| 1176 |
-
# DEBUG=true
|
| 1177 |
-
# CURSOR_STREAM_DEBUG=1
|
| 1178 |
-
|
| 1179 |
-
# When CURSOR_DEBUG=1, also append raw decoded chunks to this file path.
|
| 1180 |
-
# CURSOR_DUMP_FILE=/tmp/cursor-stream.log
|
| 1181 |
-
|
| 1182 |
-
# Cursor stream idle timeout (ms). Default: 300000 (5 min).
|
| 1183 |
-
# Used by: open-sse/executors/cursor.ts.
|
| 1184 |
-
# CURSOR_STREAM_TIMEOUT_MS=300000
|
| 1185 |
-
|
| 1186 |
-
# Cursor tool-commit directive toggle. Default-on: when a request declares
|
| 1187 |
-
# tools, a directive is prepended so composer-2.5 reliably issues tool calls
|
| 1188 |
-
# instead of narrating intent. Set to 0 to disable.
|
| 1189 |
-
# Used by: open-sse/executors/cursor.ts.
|
| 1190 |
-
# CURSOR_TOOL_DIRECTIVE=1
|
| 1191 |
-
|
| 1192 |
-
# Per-image fetch timeout (ms) for remote image_url vision input. Default: 15000.
|
| 1193 |
-
# Used by: open-sse/utils/cursorImages.ts.
|
| 1194 |
-
# CURSOR_IMAGE_FETCH_TIMEOUT_MS=15000
|
| 1195 |
-
|
| 1196 |
-
# Cursor state DB path override (for cursor version detection).
|
| 1197 |
-
# Used by: open-sse/utils/cursorVersionDetector.ts. Default: probed automatically.
|
| 1198 |
-
# CURSOR_STATE_DB_PATH=
|
| 1199 |
-
|
| 1200 |
-
# Direct Cursor bearer token used by scripts/ad-hoc/cursor-tap.cjs (developer tooling).
|
| 1201 |
-
# CURSOR_TOKEN=
|
| 1202 |
-
|
| 1203 |
-
# Log Responses API SSE-to-JSON translation details.
|
| 1204 |
-
# DEBUG_RESPONSES_SSE_TO_JSON=true
|
| 1205 |
-
|
| 1206 |
-
# Log request shape (content-type + content-length) for large chat payloads.
|
| 1207 |
-
# Used by: src/app/api/v1/chat/completions/route.ts. Set to "0" to silence.
|
| 1208 |
-
# Default: enabled.
|
| 1209 |
-
# OMNIROUTE_LOG_REQUEST_SHAPE=1
|
| 1210 |
-
|
| 1211 |
-
# Write raw (untruncated) request/response JSON in call log artifacts.
|
| 1212 |
-
# When enabled, serializeArtifactForStorage skips size-based truncation.
|
| 1213 |
-
# Also enabled automatically when APP_LOG_LEVEL=debug.
|
| 1214 |
-
# WARNING: produces large files — use only for temporary debugging.
|
| 1215 |
-
# CHAT_DEBUG_FILE=true
|
| 1216 |
-
|
| 1217 |
-
# Enable E2E test mode — relaxes auth and enables test harness hooks.
|
| 1218 |
-
# NEXT_PUBLIC_OMNIROUTE_E2E_MODE=true
|
| 1219 |
-
|
| 1220 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1221 |
-
# 23. GITHUB INTEGRATION (Issue Reporting)
|
| 1222 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1223 |
-
# Allow users to report issues directly from the Dashboard to GitHub.
|
| 1224 |
-
# Used by: src/app/api/v1/issues/report/route.ts
|
| 1225 |
-
|
| 1226 |
-
# GitHub repository in owner/repo format.
|
| 1227 |
-
# GITHUB_ISSUES_REPO=owner/repo
|
| 1228 |
-
|
| 1229 |
-
# GitHub Personal Access Token with issues:write scope.
|
| 1230 |
-
# GITHUB_ISSUES_TOKEN=ghp_xxxx
|
| 1231 |
-
|
| 1232 |
-
# Generic GitHub access token consumed by issue triage / agent helpers.
|
| 1233 |
-
# Used by: src/app/api/v1/issues/* and src/lib/cloudAgent/* — falls back to
|
| 1234 |
-
# GITHUB_ISSUES_TOKEN when unset.
|
| 1235 |
-
# GITHUB_TOKEN=
|
| 1236 |
-
|
| 1237 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1238 |
-
# 24. PROVIDER QUOTAS, TUNNELS & SANDBOXED SKILLS
|
| 1239 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1240 |
-
# Provider quota endpoints, network tunnels (Tailscale, Ngrok, MITM debug
|
| 1241 |
-
# proxy), 1Proxy egress pool, skills sandbox runtime, and miscellaneous CLI
|
| 1242 |
-
# binaries referenced by the executor layer or the dashboard runtime.
|
| 1243 |
-
|
| 1244 |
-
# ── Alibaba (Bailian) coding plan quota ──
|
| 1245 |
-
# Host/full URL override used by: open-sse/services/bailianQuotaFetcher.ts.
|
| 1246 |
-
# When unset the fetcher uses the production Alibaba endpoints.
|
| 1247 |
-
# ALIBABA_CODING_PLAN_HOST=
|
| 1248 |
-
# ALIBABA_CODING_PLAN_QUOTA_URL=
|
| 1249 |
-
|
| 1250 |
-
# ── Context window tuning ──
|
| 1251 |
-
# Tokens reserved for completion output when computing prompt budgets.
|
| 1252 |
-
# Used by: open-sse/services/contextManager.ts. Default: 1024.
|
| 1253 |
-
# CONTEXT_RESERVE_TOKENS=1024
|
| 1254 |
-
|
| 1255 |
-
# ── Model alias rewriting (legacy compatibility) ──
|
| 1256 |
-
# Toggle the legacy model-alias compatibility layer used by older clients.
|
| 1257 |
-
# Used by: open-sse/services/model.ts. Default: enabled.
|
| 1258 |
-
# MODEL_ALIAS_COMPAT_ENABLED=true
|
| 1259 |
-
|
| 1260 |
-
# ── Devin CLI binary path ──
|
| 1261 |
-
# Used by: open-sse/executors/devin-cli.ts. Default: looked up via PATH.
|
| 1262 |
-
# CLI_DEVIN_BIN=devin
|
| 1263 |
-
|
| 1264 |
-
# ── Command Code (custom CLI) callback ──
|
| 1265 |
-
# Local port used for OAuth-style callbacks from the Command Code CLI helper.
|
| 1266 |
-
# Used by: src/app/api/providers/command-code/auth/shared.ts.
|
| 1267 |
-
# COMMAND_CODE_CALLBACK_PORT=
|
| 1268 |
-
|
| 1269 |
-
# ── MITM debug proxy (development only) ──
|
| 1270 |
-
# Used by: src/mitm/server.cjs — captures upstream traffic for inspection.
|
| 1271 |
-
# MITM_LOCAL_PORT=443
|
| 1272 |
-
# MITM_DISABLE_TLS_VERIFY=0
|
| 1273 |
-
|
| 1274 |
-
# ── 1Proxy egress pool ──
|
| 1275 |
-
# Used by: src/lib/oneproxySync.ts — fetches proxy nodes from the OmniRoute
|
| 1276 |
-
# CrofAI 1Proxy service. Disable, override URL, or tune the import quality.
|
| 1277 |
-
# ONEPROXY_ENABLED=true
|
| 1278 |
-
# ONEPROXY_API_URL=https://1proxy-api.aitradepulse.com
|
| 1279 |
-
# ONEPROXY_MAX_PROXIES=500
|
| 1280 |
-
# ONEPROXY_MIN_QUALITY_THRESHOLD=50
|
| 1281 |
-
|
| 1282 |
-
# ── Free Proxy Pool (1proxy source) ──
|
| 1283 |
-
# Used by: src/lib/freeProxyProviders/oneproxy.ts
|
| 1284 |
-
# Set FREE_PROXY_1PROXY_ENABLED=false to disable this source.
|
| 1285 |
-
# FREE_PROXY_1PROXY_ENABLED=true
|
| 1286 |
-
# FREE_PROXY_1PROXY_API_URL=https://1proxy-api.aitradepulse.com/api/v1/proxies/advanced
|
| 1287 |
-
# FREE_PROXY_1PROXY_MAX=500
|
| 1288 |
-
# FREE_PROXY_1PROXY_MIN_QUALITY=50
|
| 1289 |
-
|
| 1290 |
-
# ── Free Proxy Pool (Proxifly source) ──
|
| 1291 |
-
# Used by: src/lib/freeProxyProviders/proxifly.ts
|
| 1292 |
-
# Enabled by default; set to false to disable.
|
| 1293 |
-
# FREE_PROXY_PROXIFLY_ENABLED=true
|
| 1294 |
-
# FREE_PROXY_PROXIFLY_QUANTITY=100
|
| 1295 |
-
# FREE_PROXY_PROXIFLY_ANONYMITY=elite
|
| 1296 |
-
|
| 1297 |
-
# ── Free Proxy Pool (IPLocate source) ──
|
| 1298 |
-
# Used by: src/lib/freeProxyProviders/iplocate.ts
|
| 1299 |
-
# Opt-in only; must set FREE_PROXY_IPLOCATE_ENABLED=true to activate.
|
| 1300 |
-
# FREE_PROXY_IPLOCATE_ENABLED=false
|
| 1301 |
-
# FREE_PROXY_IPLOCATE_BASE_URL=https://raw.githubusercontent.com/iplocate/free-proxy-list/main/protocols
|
| 1302 |
-
|
| 1303 |
-
# ── Vercel Relay ──
|
| 1304 |
-
# Used by: src/app/api/settings/proxy/vercel-deploy/route.ts
|
| 1305 |
-
# Hides the "Deploy Relay" button when set to false.
|
| 1306 |
-
# NEXT_PUBLIC_VERCEL_RELAY_ENABLED=true
|
| 1307 |
-
# VERCEL_API_BASE=https://api.vercel.com
|
| 1308 |
-
# Default project name pre-filled in the Vercel Relay deploy modal.
|
| 1309 |
-
# NEXT_PUBLIC_VERCEL_RELAY_DEFAULT_PROJECT=omniroute-relay
|
| 1310 |
-
|
| 1311 |
-
# ── Tailscale tunnel binaries ──
|
| 1312 |
-
# Optional explicit paths to tailscale/tailscaled binaries used by the
|
| 1313 |
-
# dashboard's tunnel manager. Used by: src/lib/tailscaleTunnel.ts.
|
| 1314 |
-
# TAILSCALE_BIN=/usr/local/bin/tailscale
|
| 1315 |
-
# TAILSCALED_BIN=/usr/local/bin/tailscaled
|
| 1316 |
-
|
| 1317 |
-
# ── Ngrok tunnel ──
|
| 1318 |
-
# Used by: src/lib/ngrokTunnel.ts — authenticates outbound tunnels.
|
| 1319 |
-
# NGROK_AUTHTOKEN=
|
| 1320 |
-
|
| 1321 |
-
# ── Database backups ──
|
| 1322 |
-
# Used by: src/lib/db/backup.ts.
|
| 1323 |
-
# DB_BACKUP_MAX_FILES=20
|
| 1324 |
-
# DB_BACKUP_RETENTION_DAYS=0
|
| 1325 |
-
|
| 1326 |
-
# ── TLS sidecar override ──
|
| 1327 |
-
# Used by: open-sse/services/chatgptTlsClient.ts tests. Production deployments
|
| 1328 |
-
# should leave this unset; the sidecar is auto-managed.
|
| 1329 |
-
# OMNIROUTE_TLS_PROXY_URL=
|
| 1330 |
-
|
| 1331 |
-
# ── Skills sandbox (experimental) ──
|
| 1332 |
-
# Used by: src/lib/skills/builtins.ts. All values support comma lists where
|
| 1333 |
-
# noted in the source.
|
| 1334 |
-
# SKILLS_MAX_FILE_BYTES=1048576
|
| 1335 |
-
# SKILLS_MAX_HTTP_RESPONSE_BYTES=256000
|
| 1336 |
-
# SKILLS_MAX_SANDBOX_OUTPUT_CHARS=100000
|
| 1337 |
-
# SKILLS_SANDBOX_TIMEOUT_MS=10000
|
| 1338 |
-
# SKILLS_SANDBOX_NETWORK_ENABLED=0
|
| 1339 |
-
# SKILLS_ALLOWED_SANDBOX_IMAGES=
|
| 1340 |
-
|
| 1341 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1342 |
-
# 25. TEST & E2E
|
| 1343 |
-
# ═══════════════════════════════════════════════════════════════════════════════
|
| 1344 |
-
# Used by scripts/dev/run-next-playwright.mjs, scripts/dev/smoke-electron-packaged.mjs,
|
| 1345 |
-
# scripts/dev/run-ecosystem-tests.mjs and scripts/build/uninstall.mjs.
|
| 1346 |
-
# Production deployments should leave every value below unset.
|
| 1347 |
-
|
| 1348 |
-
# E2E bootstrap mode for the Playwright runner. Accepted: auth | fresh | reuse.
|
| 1349 |
-
# Default (when unset): auth.
|
| 1350 |
-
# OMNIROUTE_E2E_BOOTSTRAP_MODE=auth
|
| 1351 |
-
|
| 1352 |
-
# Admin password injected into the Playwright test environment.
|
| 1353 |
-
# Falls back to INITIAL_PASSWORD when unset.
|
| 1354 |
-
# OMNIROUTE_E2E_PASSWORD=
|
| 1355 |
-
|
| 1356 |
-
# Disable the local healthcheck poll during Playwright runs (default: true).
|
| 1357 |
-
# OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK=true
|
| 1358 |
-
|
| 1359 |
-
# Disable the OAuth token healthcheck loop during tests (default: true).
|
| 1360 |
-
# OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true
|
| 1361 |
-
|
| 1362 |
-
# Exclude specific providers from the PROACTIVE token-refresh sweep (comma-separated,
|
| 1363 |
-
# case-insensitive). Targeted alternative to OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: keeps
|
| 1364 |
-
# rotating-cascade providers (Codex/OpenAI share one Auth0 family) on the reactive 401
|
| 1365 |
-
# path only, while short-TTL providers like Kimi-coding keep being refreshed proactively.
|
| 1366 |
-
# OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS=codex,openai
|
| 1367 |
-
|
| 1368 |
-
# Silence healthcheck noise in Playwright stdout (default: true).
|
| 1369 |
-
# OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true
|
| 1370 |
-
|
| 1371 |
-
# Skip the Next.js production build before Playwright starts (CI optimization).
|
| 1372 |
-
# OMNIROUTE_PLAYWRIGHT_SKIP_BUILD=0
|
| 1373 |
-
|
| 1374 |
-
# Skip the OmniRoute uninstall hook (used by CI to keep node_modules intact).
|
| 1375 |
-
# OMNIROUTE_SKIP_UNINSTALL_HOOK=0
|
| 1376 |
-
|
| 1377 |
-
# Ecosystem/protocol test orchestrators wait this long (ms) for the server to
|
| 1378 |
-
# become healthy. Default: 180000.
|
| 1379 |
-
# ECOSYSTEM_SERVER_WAIT_MS=180000
|
| 1380 |
-
|
| 1381 |
-
# Docs translation pipeline (used by scripts/i18n/run-translation.mjs).
|
| 1382 |
-
# OpenAI-compatible base URL, e.g. https://cloud.omniroute.online/v1
|
| 1383 |
-
# OMNIROUTE_TRANSLATION_API_URL=
|
| 1384 |
-
# Bearer token for the translation backend (NEVER commit a real key here).
|
| 1385 |
-
# OMNIROUTE_TRANSLATION_API_KEY=
|
| 1386 |
-
# Model id, e.g. gpt-4o-mini or cx/gpt-5.4-mini.
|
| 1387 |
-
# OMNIROUTE_TRANSLATION_MODEL=gpt-4o-mini
|
| 1388 |
-
# Per-request timeout in milliseconds (default 60000).
|
| 1389 |
-
# OMNIROUTE_TRANSLATION_TIMEOUT_MS=60000
|
| 1390 |
-
# Number of parallel translation requests (default 4).
|
| 1391 |
-
# OMNIROUTE_TRANSLATION_CONCURRENCY=4
|
| 1392 |
-
|
| 1393 |
-
# ─── Cloud Sync hardening (v3.8.6) ──────────────────────────────────────────
|
| 1394 |
-
# Shared secret used to verify the HMAC-SHA256 of the Cloud sync response body
|
| 1395 |
-
# (the Cloud endpoint must sign each response with the same secret and place
|
| 1396 |
-
# the hex digest in the X-Cloud-Sig header). When unset, v3.8.6 logs a warning
|
| 1397 |
-
# but accepts unsigned responses for back-compat. v3.9 will make this required.
|
| 1398 |
-
# OMNIROUTE_CLOUD_SYNC_SECRET=
|
| 1399 |
-
#
|
| 1400 |
-
# Set to "true" to allow the Cloud Sync endpoint to overwrite local OAuth
|
| 1401 |
-
# tokens (accessToken / refreshToken / providerSpecificData). Default OFF —
|
| 1402 |
-
# only non-credential metadata is synced. See docs/security/SOCKET_DEV_FINDINGS.md §5.
|
| 1403 |
-
# OMNIROUTE_CLOUD_SYNC_SECRETS=false
|
| 1404 |
-
|
| 1405 |
-
# ─── Zed import legacy compat (v3.8.6) ────���─────────────────────────────────
|
| 1406 |
-
# Set to "true" to fall back to the v3.8.5 one-step "import everything from
|
| 1407 |
-
# the keychain" behaviour. Default OFF — the new 2-step confirmation flow
|
| 1408 |
-
# requires `confirmedAccounts` in the request body. See SOCKET_DEV_FINDINGS.md §2.
|
| 1409 |
-
# OMNIROUTE_ZED_IMPORT_LEGACY_ONE_STEP=false
|
| 1410 |
-
|
| 1411 |
-
# ─── Build profile (build-time only) ────────────────────────────────────────
|
| 1412 |
-
# Set to "minimal" before `npm run build` to physically remove four optional
|
| 1413 |
-
# privileged modules (MITM cert install, Zed keychain import, Cloud Sync,
|
| 1414 |
-
# 9router installer) from the standalone bundle. The resulting artifact is
|
| 1415 |
-
# intended to be published as `omniroute-secure`. See SECURITY.md.
|
| 1416 |
-
# OMNIROUTE_BUILD_PROFILE=full
|
| 1417 |
-
|
| 1418 |
-
# Electron smoke harness (used by scripts/dev/smoke-electron-packaged.mjs).
|
| 1419 |
-
# ELECTRON_SMOKE_URL=http://127.0.0.1:20128/login
|
| 1420 |
-
# ELECTRON_SMOKE_TIMEOUT_MS=45000
|
| 1421 |
-
# ELECTRON_SMOKE_SETTLE_MS=2000
|
| 1422 |
-
# ELECTRON_SMOKE_APP_EXECUTABLE=
|
| 1423 |
-
# ELECTRON_SMOKE_DATA_DIR=
|
| 1424 |
-
# ELECTRON_SMOKE_KEEP_DATA=0
|
| 1425 |
-
# ELECTRON_SMOKE_STREAM_LOGS=0
|
| 1426 |
-
|
| 1427 |
-
# Playground Studio
|
| 1428 |
-
# Default model used by the improve-prompt route (optional; falls back to model in request body).
|
| 1429 |
-
PLAYGROUND_IMPROVE_PROMPT_DEFAULT_MODEL=
|
| 1430 |
-
# Maximum number of parallel compare columns in the Compare tab.
|
| 1431 |
-
PLAYGROUND_COMPARE_MAX_COLUMNS=4
|
| 1432 |
-
# Memory engine (plan 21)
|
| 1433 |
-
# MEMORY_EMBEDDING_CACHE_TTL_MS=300000 # default 5 min
|
| 1434 |
-
# MEMORY_EMBEDDING_CACHE_MAX=1000 # default 1000 entries
|
| 1435 |
-
# MEMORY_TRANSFORMERS_MODEL=Xenova/all-MiniLM-L6-v2
|
| 1436 |
-
# MEMORY_STATIC_MODEL=minishlab/potion-base-8M # HF repo id (download once)
|
| 1437 |
-
# MEMORY_STATIC_CACHE_DIR= # default <DATA_DIR>/embeddings
|
| 1438 |
-
# MEMORY_VEC_TOP_K=20 # default top-K for vector search
|
| 1439 |
-
# MEMORY_RRF_K=60 # RRF k constant (sqlite-vec hybrid recipe)
|
| 1440 |
-
# HF_HUB_ENDPOINT=https://huggingface.co # override Hugging Face Hub base URL for static potion downloads
|
| 1441 |
-
# AgentBridge + Traffic Inspector (Group A)
|
| 1442 |
-
|
| 1443 |
-
# AgentBridge
|
| 1444 |
-
AGENTBRIDGE_UPSTREAM_CA_CERT=
|
| 1445 |
|
| 1446 |
-
#
|
| 1447 |
-
|
| 1448 |
-
INSPECTOR_HTTP_PROXY_PORT=8080
|
| 1449 |
-
INSPECTOR_HTTP_PROXY_AUTOSTART=false
|
| 1450 |
-
INSPECTOR_TLS_INTERCEPT=false
|
| 1451 |
-
INSPECTOR_SYSTEM_PROXY_GUARD_MINUTES=30
|
| 1452 |
-
INSPECTOR_MAX_BODY_KB=1024
|
| 1453 |
-
INSPECTOR_MASK_SECRETS=true
|
| 1454 |
-
INSPECTOR_LLM_HOSTS_EXTRA=
|
| 1455 |
-
INSPECTOR_INTERNAL_INGEST_TOKEN=
|
| 1456 |
-
# Quota Sharing (Group B — planos 16+22)
|
| 1457 |
-
QUOTA_STORE_DRIVER=sqlite # sqlite | redis
|
| 1458 |
-
# QUOTA_STORE_REDIS_URL= # ex.: redis://localhost:6379 (apenas quando driver=redis)
|
| 1459 |
-
# QUOTA_SATURATION_THRESHOLD=0.5 # 0..1; >= threshold ativa modo strict (sem empréstimo)
|
| 1460 |
-
# QUOTA_SOFT_DEPRIORITIZE_FACTOR=0.7 # 0..1; multiplicador do score quando soft policy ativa
|
| 1461 |
-
# QUOTA_CONSUMPTION_RETENTION_DAYS=14 # GC de buckets quota_consumption.updated_at antigos
|
|
|
|
| 1 |
+
# 9Router environment contract
|
| 2 |
+
# This file reflects actual runtime usage in the current codebase.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
|
| 4 |
+
# Required
|
| 5 |
+
JWT_SECRET=change-me-to-a-long-random-secret
|
| 6 |
+
INITIAL_PASSWORD=change-me
|
| 7 |
+
DATA_DIR=/var/lib/9router
|
| 8 |
|
| 9 |
+
# Recommended runtime variables
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
PORT=20128
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
NODE_ENV=production
|
| 12 |
|
| 13 |
+
# Recommended security and ops variables
|
| 14 |
+
API_KEY_SECRET=endpoint-proxy-api-key-secret
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
MACHINE_ID_SALT=endpoint-proxy-salt
|
| 16 |
+
ENABLE_REQUEST_LOGS=false
|
| 17 |
+
OBSERVABILITY_ENABLED=true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
AUTH_COOKIE_SECURE=false
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
REQUIRE_API_KEY=false
|
| 20 |
|
| 21 |
+
# Cloud sync variables
|
| 22 |
+
# Must point to this running instance so internal sync jobs can call /api/sync/cloud.
|
| 23 |
+
# Server-side preferred variables:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
BASE_URL=http://localhost:20128
|
| 25 |
+
CLOUD_URL=https://9router.com
|
| 26 |
+
# Backward-compatible/public variables:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
NEXT_PUBLIC_BASE_URL=http://localhost:20128
|
| 28 |
+
NEXT_PUBLIC_CLOUD_URL=https://9router.com
|
| 29 |
|
| 30 |
+
# Optional outbound proxy variables for upstream provider calls
|
| 31 |
+
# Lowercase variants are also supported: http_proxy, https_proxy, all_proxy, no_proxy
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
# HTTP_PROXY=http://127.0.0.1:7890
|
| 33 |
# HTTPS_PROXY=http://127.0.0.1:7890
|
| 34 |
# ALL_PROXY=socks5://127.0.0.1:7890
|
| 35 |
# NO_PROXY=localhost,127.0.0.1
|
| 36 |
|
| 37 |
+
# Optional SearXNG endpoint for the built-in unauthenticated web-search provider.
|
| 38 |
+
# SEARXNG_URL=http://searxng:8080/search
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
+
# Currently unused by application runtime (kept as reference)
|
| 41 |
+
# INSTANCE_NAME=9router
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.gitignore
CHANGED
|
@@ -1,58 +1,31 @@
|
|
| 1 |
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
|
| 3 |
-
# project-specific directories
|
| 4 |
-
.omnivscodeagent/
|
| 5 |
-
omnirouteCloud/
|
| 6 |
-
omnirouteSite/
|
| 7 |
-
_cache/
|
| 8 |
-
_ideia/
|
| 9 |
-
_mono_repo/
|
| 10 |
-
_references/
|
| 11 |
-
_tasks/
|
| 12 |
-
.agents/**
|
| 13 |
-
.claude/**
|
| 14 |
-
.gemini/**
|
| 15 |
-
.config/**
|
| 16 |
-
.data/**
|
| 17 |
-
.logs/**
|
| 18 |
-
.tests/**
|
| 19 |
-
.coverage/**
|
| 20 |
-
coverage/
|
| 21 |
-
.dist/**
|
| 22 |
-
.next/**
|
| 23 |
-
.build/**
|
| 24 |
-
.out/**
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
# Memory Bank and Cursor rules (local-only AI agent context)
|
| 28 |
-
memory-bank/
|
| 29 |
-
.cursor/rules/core.mdc
|
| 30 |
-
.cursor/rules/memory-bank.mdc
|
| 31 |
-
|
| 32 |
-
# Claude Code local state — runtime files only; shared commands at .claude/commands/ are tracked
|
| 33 |
-
.claude/scheduled_tasks.lock
|
| 34 |
-
.claude/scheduled_tasks/
|
| 35 |
-
.claude/sessions/
|
| 36 |
-
.claude/state.json
|
| 37 |
-
.claude/settings.local.json
|
| 38 |
-
|
| 39 |
-
# Root-level underscore-prefixed directories (private/draft — never commit)
|
| 40 |
-
/_*/
|
| 41 |
-
|
| 42 |
-
# Draft features documentation (internal only)
|
| 43 |
-
docs/new-features/
|
| 44 |
-
|
| 45 |
# dependencies
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
.
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
!
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
#
|
| 55 |
-
.
|
|
|
|
| 56 |
|
| 57 |
# debug
|
| 58 |
npm-debug.log*
|
|
@@ -63,9 +36,6 @@ yarn-error.log*
|
|
| 63 |
# env files (can opt-in for committing if needed)
|
| 64 |
.env*
|
| 65 |
!.env.example
|
| 66 |
-
# Provider API keys (never commit)
|
| 67 |
-
*.api-key
|
| 68 |
-
.nvidia-api-key
|
| 69 |
|
| 70 |
# vercel
|
| 71 |
.vercel
|
|
@@ -74,128 +44,45 @@ yarn-error.log*
|
|
| 74 |
*.tsbuildinfo
|
| 75 |
next-env.d.ts
|
| 76 |
|
| 77 |
-
|
| 78 |
data/
|
| 79 |
-
.data/
|
| 80 |
logs/*
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
.sisyphus/
|
| 87 |
-
.plans/
|
| 88 |
-
|
| 89 |
-
# open-sse tests
|
| 90 |
open-sse/test/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 91 |
|
| 92 |
-
# Ignore vscode AI rules
|
| 93 |
-
.github/instructions/codacy.instructions.md
|
| 94 |
-
|
| 95 |
-
# Playwright
|
| 96 |
-
.playwright-mcp/
|
| 97 |
-
test-results/
|
| 98 |
-
playwright-report/
|
| 99 |
-
blob-report/
|
| 100 |
-
cloud/
|
| 101 |
-
.tmp/
|
| 102 |
|
| 103 |
-
#
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
app.log
|
| 109 |
-
*.tgz
|
| 110 |
-
.gh-discussions.json
|
| 111 |
-
deploy.sh
|
| 112 |
-
docker-compose.minimal.yml
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
# Backup directories
|
| 116 |
-
app.__qa_backup/
|
| 117 |
-
.app-build-backup-*/
|
| 118 |
-
backup/
|
| 119 |
-
|
| 120 |
-
# Build intermediates (.build/) and shippable standalone (dist/).
|
| 121 |
-
# These are fully reproducible from source; never committed.
|
| 122 |
-
# Layer 1: Next.js now writes to .build/next (was .next); assembled bundle → dist/
|
| 123 |
-
# (Previously /app/ was the standalone output; renamed to /dist/ in Layer 1.)
|
| 124 |
-
/.build/
|
| 125 |
-
/dist/
|
| 126 |
-
/.next/
|
| 127 |
-
|
| 128 |
-
# Electron
|
| 129 |
-
electron/dist-electron/
|
| 130 |
-
electron/node_modules/
|
| 131 |
-
icon.iconset/
|
| 132 |
-
|
| 133 |
-
# VS Code Extension (independent Git repo)
|
| 134 |
-
vscode-extension/
|
| 135 |
-
|
| 136 |
-
# SQLite residual files
|
| 137 |
-
*.sqlite-shm
|
| 138 |
-
*.sqlite-wal
|
| 139 |
-
*.sqlite-journal
|
| 140 |
-
|
| 141 |
-
# IDEA
|
| 142 |
-
.idea/
|
| 143 |
-
|
| 144 |
-
# Local OpenCode agent config
|
| 145 |
-
.config/
|
| 146 |
-
|
| 147 |
-
# Empty/dangling files
|
| 148 |
-
typescript
|
| 149 |
-
|
| 150 |
-
# Gemini Antigravity agent data
|
| 151 |
-
.gemini/
|
| 152 |
-
|
| 153 |
-
# Superpowers plans/specs (internal tooling, not project code)
|
| 154 |
-
docs/superpowers/
|
| 155 |
-
|
| 156 |
-
# GitNexus local index
|
| 157 |
-
.gitnexus
|
| 158 |
-
.worktrees
|
| 159 |
-
bin/omniroute.mjs
|
| 160 |
-
|
| 161 |
-
# Consistent with .dockerignore / .npmignore
|
| 162 |
-
.omc/
|
| 163 |
-
audit-report.json
|
| 164 |
-
bun.lock
|
| 165 |
-
|
| 166 |
-
# Private environment variables for .http-client
|
| 167 |
-
http-client.private.env.json
|
| 168 |
-
|
| 169 |
-
# Note: _ideia/ (feature-triage drafts) is fully covered by the /_*/ rule above
|
| 170 |
-
# and kept as a separate local-only git repo. Never committed to OmniRoute.
|
| 171 |
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
|
| 176 |
-
#
|
| 177 |
-
|
| 178 |
-
.
|
| 179 |
-
|
| 180 |
-
.agents/workflows/port-upstream-issues-ag.md
|
| 181 |
-
.agents/skills/implement-features/
|
| 182 |
-
.claude/commands/implement-features-cc.md
|
| 183 |
-
.claude/commands/port-upstream-features-cc.md
|
| 184 |
-
.claude/commands/port-upstream-issues-cc.md
|
| 185 |
-
.claude/worktrees/
|
| 186 |
-
.codegraph/
|
| 187 |
|
| 188 |
-
#
|
| 189 |
-
.source/
|
| 190 |
-
|
| 191 |
-
# AI agent local settings and configs
|
| 192 |
-
.agents/
|
| 193 |
-
.antigravitycli/
|
| 194 |
.claude/
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
.
|
| 201 |
-
.data-dev/
|
|
|
|
| 1 |
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
| 2 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
# dependencies
|
| 4 |
+
/node_modules
|
| 5 |
+
/.pnp
|
| 6 |
+
.pnp.*
|
| 7 |
+
.yarn/*
|
| 8 |
+
!.yarn/patches
|
| 9 |
+
!.yarn/plugins
|
| 10 |
+
!.yarn/releases
|
| 11 |
+
!.yarn/versions
|
| 12 |
+
|
| 13 |
+
# testing
|
| 14 |
+
/coverage
|
| 15 |
+
|
| 16 |
+
# next.js
|
| 17 |
+
/.next/
|
| 18 |
+
/.next-cli-build/
|
| 19 |
+
/out/
|
| 20 |
+
cli/.build-home/
|
| 21 |
+
product
|
| 22 |
+
# production
|
| 23 |
+
/build
|
| 24 |
+
.idea/
|
| 25 |
|
| 26 |
+
# misc
|
| 27 |
+
.DS_Store
|
| 28 |
+
*.pem
|
| 29 |
|
| 30 |
# debug
|
| 31 |
npm-debug.log*
|
|
|
|
| 36 |
# env files (can opt-in for committing if needed)
|
| 37 |
.env*
|
| 38 |
!.env.example
|
|
|
|
|
|
|
|
|
|
| 39 |
|
| 40 |
# vercel
|
| 41 |
.vercel
|
|
|
|
| 44 |
*.tsbuildinfo
|
| 45 |
next-env.d.ts
|
| 46 |
|
| 47 |
+
.bin/*
|
| 48 |
data/
|
|
|
|
| 49 |
logs/*
|
| 50 |
+
source/*
|
| 51 |
+
.cursor/*
|
| 52 |
+
docs/*
|
| 53 |
+
!docs/ARCHITECTURE.md
|
| 54 |
+
test/*
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
open-sse/test/*
|
| 56 |
+
RM.vn.md
|
| 57 |
+
RM.md
|
| 58 |
+
cursor/*
|
| 59 |
+
PUBLIC.md
|
| 60 |
+
Thanks.md
|
| 61 |
+
PUBLIC.en.md
|
| 62 |
+
PR/*
|
| 63 |
+
package-lock.json
|
| 64 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 65 |
|
| 66 |
+
#Ignore vscode AI rules
|
| 67 |
+
.github/instructions/codacy.instructions.md
|
| 68 |
+
README1.md
|
| 69 |
+
deploy*.sh
|
| 70 |
+
ecosystem.config.*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
|
| 72 |
+
scripts/agSniffer/*
|
| 73 |
+
gitbooks/*
|
| 74 |
+
gitbook/README.md
|
| 75 |
|
| 76 |
+
# Refactor backup reference (do not bundle/lint)
|
| 77 |
+
open-sse.old/
|
| 78 |
+
.graphifyignore
|
| 79 |
+
graphify-out/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
|
| 81 |
+
# Local-only working dirs (notes, vendored repos, scripts, skills)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
.claude/
|
| 83 |
+
.docs/
|
| 84 |
+
.repo/
|
| 85 |
+
.script/
|
| 86 |
+
.codegraph/
|
| 87 |
+
.PR/
|
| 88 |
+
.next-analyze/*
|
|
|
.npmignore
CHANGED
|
@@ -3,101 +3,29 @@ data/
|
|
| 3 |
**/data/
|
| 4 |
**/db.json
|
| 5 |
|
| 6 |
-
#
|
| 7 |
-
app/vscode-extension/
|
| 8 |
-
**/data/
|
| 9 |
-
**/db.json
|
| 10 |
-
|
| 11 |
-
# Source code (pre-built app/ is published instead)
|
| 12 |
src/
|
| 13 |
-
open-sse/
|
| 14 |
docs/
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
images/
|
| 18 |
-
logs/
|
| 19 |
scripts/
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
# Config
|
| 22 |
*.md
|
| 23 |
!README.md
|
| 24 |
.gitignore
|
| 25 |
-
.git/
|
| 26 |
-
.github/
|
| 27 |
-
.husky/
|
| 28 |
-
.vscode/
|
| 29 |
-
.agents/
|
| 30 |
.env*
|
| 31 |
-
|
| 32 |
-
app/.env*
|
| 33 |
eslint.config.mjs
|
| 34 |
-
prettier.config.mjs
|
| 35 |
postcss.config.mjs
|
| 36 |
next.config.mjs
|
| 37 |
tsconfig.json
|
| 38 |
-
tsconfig.typecheck-core.json
|
| 39 |
-
tsconfig.typecheck-noimplicit-core.json
|
| 40 |
-
playwright.config.ts
|
| 41 |
-
vitest.config.ts
|
| 42 |
-
next-env.d.ts
|
| 43 |
-
llm.txt
|
| 44 |
-
|
| 45 |
-
# Docker
|
| 46 |
-
docker-compose*.yml
|
| 47 |
-
Dockerfile
|
| 48 |
-
.dockerignore
|
| 49 |
-
|
| 50 |
-
# Misc
|
| 51 |
-
AGENTS.md
|
| 52 |
-
bun.lock
|
| 53 |
-
|
| 54 |
-
# Build artifacts (pre-built goes inside app/)
|
| 55 |
-
/.next/
|
| 56 |
-
/node_modules/
|
| 57 |
-
|
| 58 |
-
# Ignore large binary files and other build directories
|
| 59 |
-
*.tgz
|
| 60 |
-
*.AppImage
|
| 61 |
-
*.deb
|
| 62 |
-
*.rpm
|
| 63 |
-
electron/
|
| 64 |
-
app/electron/
|
| 65 |
-
app/vscode-extension/
|
| 66 |
-
|
| 67 |
-
# Subprojects
|
| 68 |
-
clipr/
|
| 69 |
-
omnirouteCloud/
|
| 70 |
-
omnirouteSite/
|
| 71 |
-
vscode-extension/
|
| 72 |
|
| 73 |
-
#
|
| 74 |
-
/
|
| 75 |
-
|
| 76 |
-
app/coverage/
|
| 77 |
-
app/logs/
|
| 78 |
-
app/tests/
|
| 79 |
|
| 80 |
-
# Consistent with .gitignore and .dockerignore
|
| 81 |
-
.DS_Store
|
| 82 |
-
.idea/
|
| 83 |
-
.config/
|
| 84 |
-
.data/
|
| 85 |
-
.omnivscodeagent/
|
| 86 |
-
.omc/
|
| 87 |
-
*.sqlite-*
|
| 88 |
-
*.tsbuildinfo
|
| 89 |
-
security-analysis/
|
| 90 |
-
.analysis/
|
| 91 |
-
antigravity-manager-analysis/
|
| 92 |
-
.sisyphus/
|
| 93 |
-
.plans/
|
| 94 |
-
app.__qa_backup/
|
| 95 |
-
.app-build-backup-*/
|
| 96 |
-
.gitnexus
|
| 97 |
-
.worktrees
|
| 98 |
-
.next-playwright/
|
| 99 |
-
test-results/
|
| 100 |
-
playwright-report/
|
| 101 |
-
blob-report/
|
| 102 |
-
coverage/
|
| 103 |
-
@omniroute/
|
|
|
|
| 3 |
**/data/
|
| 4 |
**/db.json
|
| 5 |
|
| 6 |
+
# Development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
src/
|
|
|
|
| 8 |
docs/
|
| 9 |
+
test/
|
| 10 |
+
agents/
|
|
|
|
|
|
|
| 11 |
scripts/
|
| 12 |
+
worker/
|
| 13 |
+
shared-sse/
|
| 14 |
+
copilot-api/
|
| 15 |
+
CLIProxyAPI/
|
| 16 |
|
| 17 |
+
# Config files
|
| 18 |
*.md
|
| 19 |
!README.md
|
| 20 |
.gitignore
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
.env*
|
| 22 |
+
jsconfig.json
|
|
|
|
| 23 |
eslint.config.mjs
|
|
|
|
| 24 |
postcss.config.mjs
|
| 25 |
next.config.mjs
|
| 26 |
tsconfig.json
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
+
# Build artifacts that shouldn't be published
|
| 29 |
+
.next/cache/
|
| 30 |
+
.next/standalone/data/
|
|
|
|
|
|
|
|
|
|
| 31 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CHANGELOG.md
CHANGED
|
The diff for this file is too large to render.
See raw diff
|
|
|
CLAUDE.md
CHANGED
|
@@ -2,452 +2,90 @@
|
|
| 2 |
|
| 3 |
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
-
```
|
| 8 |
-
npm install # Install deps (auto-generates .env from .env.example)
|
| 9 |
-
npm run dev # Dev server at http://localhost:20128
|
| 10 |
-
npm run build # Production build (Next.js 16 standalone)
|
| 11 |
-
npm run lint # ESLint (0 errors expected; warnings are pre-existing)
|
| 12 |
-
npm run typecheck:core # TypeScript check (should be clean)
|
| 13 |
-
npm run typecheck:noimplicit:core # Strict check (no implicit any)
|
| 14 |
-
npm run test:coverage # Unit tests + coverage gate (60/60/60/60 — statements/lines/functions/branches)
|
| 15 |
-
npm run check # lint + test combined
|
| 16 |
-
npm run check:cycles # Detect circular dependencies
|
| 17 |
-
```
|
| 18 |
-
|
| 19 |
-
### Running Tests
|
| 20 |
-
|
| 21 |
-
```bash
|
| 22 |
-
# Single test file (Node.js native test runner — most tests)
|
| 23 |
-
node --import tsx/esm --test tests/unit/your-file.test.ts
|
| 24 |
-
|
| 25 |
-
# Vitest (MCP server, autoCombo, cache)
|
| 26 |
-
npm run test:vitest
|
| 27 |
-
|
| 28 |
-
# All suites
|
| 29 |
-
npm run test:all
|
| 30 |
-
```
|
| 31 |
-
|
| 32 |
-
For full test matrix, see `CONTRIBUTING.md` → "Running Tests". For deep architecture, see `AGENTS.md`.
|
| 33 |
-
|
| 34 |
-
---
|
| 35 |
-
|
| 36 |
-
## Project at a Glance
|
| 37 |
-
|
| 38 |
-
**OmniRoute** — unified AI proxy/router. One endpoint, 160+ LLM providers, auto-fallback.
|
| 39 |
-
|
| 40 |
-
| Layer | Location | Purpose |
|
| 41 |
-
| ------------- | ----------------------- | ------------------------------------------------------------------ |
|
| 42 |
-
| API Routes | `src/app/api/v1/` | Next.js App Router — entry points |
|
| 43 |
-
| Handlers | `open-sse/handlers/` | Request processing (chat, embeddings, etc) |
|
| 44 |
-
| Executors | `open-sse/executors/` | Provider-specific HTTP dispatch |
|
| 45 |
-
| Translators | `open-sse/translator/` | Format conversion (OpenAI↔Claude↔Gemini) |
|
| 46 |
-
| Transformer | `open-sse/transformer/` | Responses API ↔ Chat Completions |
|
| 47 |
-
| Services | `open-sse/services/` | Combo routing, rate limits, caching, etc |
|
| 48 |
-
| Database | `src/lib/db/` | SQLite domain modules (45+ files, 55 migrations) |
|
| 49 |
-
| Domain/Policy | `src/domain/` | Policy engine, cost rules, fallback logic |
|
| 50 |
-
| MCP Server | `open-sse/mcp-server/` | 43 tools (30 base + 3 memory + 4 skills + 6 notion), 3 transports, ~13 scopes |
|
| 51 |
-
| A2A Server | `src/lib/a2a/` | JSON-RPC 2.0 agent protocol |
|
| 52 |
-
| Skills | `src/lib/skills/` | Extensible skill framework |
|
| 53 |
-
| Memory | `src/lib/memory/` | Persistent conversational memory |
|
| 54 |
-
|
| 55 |
-
Monorepo: `src/` (Next.js 16 app), `open-sse/` (streaming engine workspace), `electron/` (desktop app), `tests/`, `bin/` (CLI entry point).
|
| 56 |
-
|
| 57 |
-
---
|
| 58 |
-
|
| 59 |
-
## Request Pipeline
|
| 60 |
-
|
| 61 |
-
```
|
| 62 |
-
Client → /v1/chat/completions (Next.js route)
|
| 63 |
-
→ CORS → Zod validation → auth? → policy check → prompt injection guard
|
| 64 |
-
→ handleChatCore() [open-sse/handlers/chatCore.ts]
|
| 65 |
-
→ cache check → rate limit → combo routing?
|
| 66 |
-
→ resolveComboTargets() → handleSingleModel() per target
|
| 67 |
-
→ translateRequest() → getExecutor() → executor.execute()
|
| 68 |
-
→ fetch() upstream → retry w/ backoff
|
| 69 |
-
→ response translation → SSE stream or JSON
|
| 70 |
-
→ If Responses API: responsesTransformer.ts TransformStream
|
| 71 |
-
```
|
| 72 |
-
|
| 73 |
-
API routes follow a consistent pattern: `Route → CORS preflight → Zod body validation → Optional auth (extractApiKey/isValidApiKey) → API key policy enforcement → Handler delegation (open-sse)`. No global Next.js middleware — interception is route-specific.
|
| 74 |
-
|
| 75 |
-
**Combo routing** (`open-sse/services/combo.ts`): 14 strategies (priority, weighted, fill-first, round-robin, P2C, random, least-used, cost-optimized, reset-aware, strict-random, auto, lkgp, context-optimized, context-relay). Each target calls `handleSingleModel()` which wraps `handleChatCore()` with per-target error handling and circuit breaker checks. See `docs/routing/AUTO-COMBO.md` for the 9-factor Auto-Combo scoring and `docs/architecture/RESILIENCE_GUIDE.md` for the 3 resilience layers.
|
| 76 |
-
|
| 77 |
-
---
|
| 78 |
-
|
| 79 |
-
## Resilience Runtime State
|
| 80 |
-
|
| 81 |
-
OmniRoute has three related but distinct temporary-failure mechanisms. Keep their
|
| 82 |
-
scope separate when debugging routing behavior. See the
|
| 83 |
-
[3-layer resilience diagram](./docs/diagrams/exported/resilience-3layers.svg)
|
| 84 |
-
(source: [docs/diagrams/resilience-3layers.mmd](./docs/diagrams/resilience-3layers.mmd))
|
| 85 |
-
for an at-a-glance map.
|
| 86 |
-
|
| 87 |
-
### Provider Circuit Breaker
|
| 88 |
-
|
| 89 |
-
**Scope**: whole provider, e.g. `glm`, `openai`, `anthropic`.
|
| 90 |
-
|
| 91 |
-
**Purpose**: stop sending traffic to a provider that is repeatedly failing at the
|
| 92 |
-
upstream/service level, so one unhealthy provider does not slow down every request.
|
| 93 |
-
|
| 94 |
-
**Implementation**:
|
| 95 |
-
|
| 96 |
-
- Core class: `src/shared/utils/circuitBreaker.ts`
|
| 97 |
-
- Chat gate/execution wiring: `src/sse/handlers/chatHelpers.ts`, `src/sse/handlers/chat.ts`
|
| 98 |
-
- Runtime status API: `src/app/api/monitoring/health/route.ts`
|
| 99 |
-
- Shared wrappers: `open-sse/services/accountFallback.ts`
|
| 100 |
-
- Persisted state table: `domain_circuit_breakers`
|
| 101 |
-
|
| 102 |
-
**States**:
|
| 103 |
|
| 104 |
-
|
| 105 |
-
- `
|
| 106 |
-
|
| 107 |
-
- `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
|
| 108 |
-
breaker, failure opens it again.
|
| 109 |
|
| 110 |
-
|
| 111 |
|
| 112 |
-
|
| 113 |
-
- API-key providers: threshold `5`, reset timeout `30s`.
|
| 114 |
-
- Local providers: threshold `2`, reset timeout `15s`.
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
Do not trip the whole-provider breaker for normal account/key/model errors like most
|
| 123 |
-
`401`, `403`, or `429` cases. Those usually belong to connection cooldown or model
|
| 124 |
-
lockout. A generic API-key provider `403` should be recoverable unless it is classified
|
| 125 |
-
as a terminal provider/account error.
|
| 126 |
-
|
| 127 |
-
The breaker uses lazy recovery, not a background timer. When `OPEN` expires, reads such
|
| 128 |
-
as `getStatus()`, `canExecute()`, and `getRetryAfterMs()` refresh the state to
|
| 129 |
-
`HALF_OPEN`, so dashboards and combo candidate builders do not keep excluding an
|
| 130 |
-
expired provider forever.
|
| 131 |
-
|
| 132 |
-
### Connection Cooldown
|
| 133 |
-
|
| 134 |
-
**Scope**: one provider connection/account/key.
|
| 135 |
-
|
| 136 |
-
**Purpose**: temporarily skip one bad key/account while allowing other connections for
|
| 137 |
-
the same provider to continue serving requests.
|
| 138 |
-
|
| 139 |
-
**Implementation**:
|
| 140 |
-
|
| 141 |
-
- Write/update path: `src/sse/services/auth.ts::markAccountUnavailable()`
|
| 142 |
-
- Account selection/filtering: `src/sse/services/auth.ts::getProviderCredentials...`
|
| 143 |
-
- Cooldown calculation: `open-sse/services/accountFallback.ts::checkFallbackError()`
|
| 144 |
-
- Settings: `src/lib/resilience/settings.ts`
|
| 145 |
-
|
| 146 |
-
Important fields on provider connections:
|
| 147 |
-
|
| 148 |
-
```ts
|
| 149 |
-
rateLimitedUntil;
|
| 150 |
-
testStatus: "unavailable";
|
| 151 |
-
lastError;
|
| 152 |
-
lastErrorType;
|
| 153 |
-
errorCode;
|
| 154 |
-
backoffLevel;
|
| 155 |
-
```
|
| 156 |
-
|
| 157 |
-
During account selection, a connection is skipped while:
|
| 158 |
-
|
| 159 |
-
```ts
|
| 160 |
-
new Date(rateLimitedUntil).getTime() > Date.now();
|
| 161 |
```
|
|
|
|
|
|
|
|
|
|
| 162 |
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
Default connection cooldown behavior:
|
| 168 |
-
|
| 169 |
-
- OAuth base cooldown: `5s`.
|
| 170 |
-
- API-key base cooldown: `3s`.
|
| 171 |
-
- API-key `429` should prefer upstream retry hints (`Retry-After`, reset headers, or
|
| 172 |
-
parseable reset text) when available.
|
| 173 |
-
- Repeated recoverable failures use exponential backoff:
|
| 174 |
-
|
| 175 |
-
```ts
|
| 176 |
-
baseCooldownMs * 2 ** failureIndex;
|
| 177 |
```
|
| 178 |
|
| 179 |
-
|
| 180 |
-
repeatedly extending the cooldown or double-incrementing `backoffLevel`.
|
| 181 |
-
|
| 182 |
-
Terminal states are not cooldowns. `banned`, `expired`, and `credits_exhausted` are
|
| 183 |
-
intended to stay unavailable until credentials/settings change or an operator resets
|
| 184 |
-
them. Do not overwrite terminal states with transient cooldown state.
|
| 185 |
-
|
| 186 |
-
### Model Lockout
|
| 187 |
-
|
| 188 |
-
**Scope**: provider + connection + model.
|
| 189 |
-
|
| 190 |
-
**Purpose**: avoid disabling a whole connection when only one model is unavailable or
|
| 191 |
-
quota-limited for that connection.
|
| 192 |
-
|
| 193 |
-
Examples:
|
| 194 |
-
|
| 195 |
-
- Per-model quota providers returning `429`.
|
| 196 |
-
- Local providers returning `404` for one missing model.
|
| 197 |
-
- Provider-specific mode/model permission failures such as selected Grok modes.
|
| 198 |
-
|
| 199 |
-
Model lockout lives in `open-sse/services/accountFallback.ts` and lets the same
|
| 200 |
-
connection continue serving other models.
|
| 201 |
-
|
| 202 |
-
### Debugging Guidance
|
| 203 |
-
|
| 204 |
-
- If all keys for a provider are skipped, inspect both provider breaker state and each
|
| 205 |
-
connection's `rateLimitedUntil`/`testStatus`.
|
| 206 |
-
- If a provider appears permanently excluded after the reset window, check whether code
|
| 207 |
-
is reading raw `state` instead of using `getStatus()`/`canExecute()`.
|
| 208 |
-
- If one provider key fails but others should work, prefer connection cooldown over
|
| 209 |
-
provider breaker.
|
| 210 |
-
- If only one model fails, prefer model lockout over connection cooldown.
|
| 211 |
-
- If a state should self-recover, it should have a future timestamp/reset timeout and a
|
| 212 |
-
read path that refreshes expired state. Permanent statuses require manual credential
|
| 213 |
-
or config changes.
|
| 214 |
-
|
| 215 |
-
---
|
| 216 |
-
|
| 217 |
-
## Key Conventions
|
| 218 |
-
|
| 219 |
-
### Code Style
|
| 220 |
-
|
| 221 |
-
- **2 spaces**, semicolons, double quotes, 100 char width, es5 trailing commas (enforced by lint-staged via Prettier)
|
| 222 |
-
- **Imports**: external → internal (`@/`, `@omniroute/open-sse`) → relative
|
| 223 |
-
- **Naming**: files=camelCase/kebab, components=PascalCase, constants=UPPER_SNAKE
|
| 224 |
-
- **ESLint**: `no-eval`, `no-implied-eval`, `no-new-func` = error everywhere; `no-explicit-any` = warn in `open-sse/` and `tests/`
|
| 225 |
-
- **TypeScript**: `strict: false`, target ES2022, module esnext, resolution bundler. Prefer explicit types.
|
| 226 |
-
|
| 227 |
-
### Database
|
| 228 |
-
|
| 229 |
-
- **Always** go through `src/lib/db/` domain modules — **never** write raw SQL in routes or handlers
|
| 230 |
-
- **Never** add logic to `src/lib/localDb.ts` (re-export layer only)
|
| 231 |
-
- **Never** barrel-import from `localDb.ts` — import specific `db/` modules instead
|
| 232 |
-
- DB singleton: `getDbInstance()` from `src/lib/db/core.ts` (WAL journaling)
|
| 233 |
-
- Migrations: `src/lib/db/migrations/` — versioned SQL files, idempotent, run in transactions
|
| 234 |
-
|
| 235 |
-
### Error Handling
|
| 236 |
-
|
| 237 |
-
- try/catch with specific error types, log with pino context
|
| 238 |
-
- Never swallow errors in SSE streams — use abort signals for cleanup
|
| 239 |
-
- Return proper HTTP status codes (4xx/5xx)
|
| 240 |
-
|
| 241 |
-
### Security
|
| 242 |
-
|
| 243 |
-
- **Never** use `eval()`, `new Function()`, or implied eval
|
| 244 |
-
- Validate all inputs with Zod schemas
|
| 245 |
-
- Encrypt credentials at rest (AES-256-GCM)
|
| 246 |
-
- Upstream header denylist: `src/shared/constants/upstreamHeaders.ts` — keep sanitize, Zod schemas, and unit tests aligned when editing
|
| 247 |
-
- **Public upstream credentials** (Gemini/Antigravity/Windsurf-style OAuth client_id/secret + Firebase Web keys extracted from public CLIs): **MUST** be embedded via `resolvePublicCred()` from `open-sse/utils/publicCreds.ts` — **never** as string literals. See `docs/security/PUBLIC_CREDS.md` for the mandatory pattern.
|
| 248 |
-
- **Error responses** (HTTP / SSE / executor / MCP handler): **MUST** route through `buildErrorBody()` or `sanitizeErrorMessage()` from `open-sse/utils/error.ts` — **never** put raw `err.stack` or `err.message` in a response body. See `docs/security/ERROR_SANITIZATION.md`.
|
| 249 |
-
- **Shell commands built from variables**: when calling `exec()`/`spawn()` with a script that needs runtime values, pass them via the `env` option (shell-escaped automatically) — **never** string-interpolate untrusted/external paths into the script body. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
|
| 250 |
-
- **Secure-by-default libraries** ([tldrsec/awesome-secure-defaults](https://github.com/tldrsec/awesome-secure-defaults)): prefer Helmet.js, DOMPurify, ssrf-req-filter, safe-regex, Google Tink over custom implementations whenever adding new security-sensitive surfaces.
|
| 251 |
-
|
| 252 |
-
---
|
| 253 |
-
|
| 254 |
-
## Common Modification Scenarios
|
| 255 |
-
|
| 256 |
-
### Adding a New Provider
|
| 257 |
-
|
| 258 |
-
1. Register in `src/shared/constants/providers.ts` (Zod-validated at load)
|
| 259 |
-
2. Add executor in `open-sse/executors/` if custom logic needed (extend `BaseExecutor`)
|
| 260 |
-
3. Add translator in `open-sse/translator/` if non-OpenAI format
|
| 261 |
-
4. Add OAuth config in `src/lib/oauth/constants/oauth.ts` if OAuth-based — if the upstream CLI ships a public client_id/secret, embed via `resolvePublicCred()` (see `docs/security/PUBLIC_CREDS.md`), **never** as a literal
|
| 262 |
-
5. Register models in `open-sse/config/providerRegistry.ts`
|
| 263 |
-
6. Write tests in `tests/unit/` (include the publicCreds shape assertion if you added a new embedded default)
|
| 264 |
-
|
| 265 |
-
### Adding a New API Route
|
| 266 |
-
|
| 267 |
-
1. Create directory under `src/app/api/v1/your-route/`
|
| 268 |
-
2. Create `route.ts` with `GET`/`POST` handlers
|
| 269 |
-
3. Follow pattern: CORS → Zod body validation → optional auth → handler delegation
|
| 270 |
-
4. Handler goes in `open-sse/handlers/` (import from there, not inline)
|
| 271 |
-
5. Error responses use `buildErrorBody()` / `errorResponse()` from `open-sse/utils/error.ts` (auto-sanitized — never put `err.stack` or `err.message` raw in the body). See `docs/security/ERROR_SANITIZATION.md`.
|
| 272 |
-
6. Add tests — including at least one assertion that error responses do not leak stack traces (`!body.error.message.includes("at /")`)
|
| 273 |
-
|
| 274 |
-
### Adding a New DB Module
|
| 275 |
-
|
| 276 |
-
1. Create `src/lib/db/yourModule.ts` — import `getDbInstance` from `./core.ts`
|
| 277 |
-
2. Export CRUD functions for your domain table(s)
|
| 278 |
-
3. Add migration in `src/lib/db/migrations/` if new tables needed
|
| 279 |
-
4. Re-export from `src/lib/localDb.ts` (add to the re-export list only)
|
| 280 |
-
5. Write tests
|
| 281 |
-
|
| 282 |
-
### Adding a New MCP Tool
|
| 283 |
-
|
| 284 |
-
1. Add tool definition in `open-sse/mcp-server/tools/` with Zod input schema + async handler
|
| 285 |
-
2. Register in tool set (wired by `createMcpServer()`)
|
| 286 |
-
3. Assign to appropriate scope(s)
|
| 287 |
-
4. Write tests (tool invocation logged to `mcp_audit` table)
|
| 288 |
-
|
| 289 |
-
### Adding a New A2A Skill
|
| 290 |
-
|
| 291 |
-
1. Create skill in `src/lib/a2a/skills/` (5 already exist: smart-routing, quota-management, provider-discovery, cost-analysis, health-report)
|
| 292 |
-
2. Skill receives task context (messages, metadata) → returns structured result
|
| 293 |
-
3. Register in `A2A_SKILL_HANDLERS` in `src/lib/a2a/taskExecution.ts`
|
| 294 |
-
4. Expose in `src/app/.well-known/agent.json/route.ts` (Agent Card)
|
| 295 |
-
5. Write tests in `tests/unit/`
|
| 296 |
-
6. Document in `docs/frameworks/A2A-SERVER.md` skill table
|
| 297 |
-
|
| 298 |
-
### Adding a New Cloud Agent
|
| 299 |
-
|
| 300 |
-
1. Create agent class in `src/lib/cloudAgent/agents/` extending `CloudAgentBase` (3 already exist: codex-cloud, devin, jules)
|
| 301 |
-
2. Implement `createTask`, `getStatus`, `approvePlan`, `sendMessage`, `listSources`
|
| 302 |
-
3. Register in `src/lib/cloudAgent/registry.ts`
|
| 303 |
-
4. Add OAuth/credentials handling if needed (`src/lib/oauth/providers/`)
|
| 304 |
-
5. Tests + document in `docs/frameworks/CLOUD_AGENT.md`
|
| 305 |
-
|
| 306 |
-
### Adding a New Embedded Service
|
| 307 |
-
|
| 308 |
-
1. Create installer in `src/lib/services/installers/{name}.ts` modeled on `ninerouter.ts` (use `runNpm` from `installers/utils.ts` — no shell interpolation, hard rule #13).
|
| 309 |
-
2. Register the service in `src/lib/services/bootstrap.ts` (add to `SERVICES[]` array and extend `buildSpawnArgsFactory()`).
|
| 310 |
-
3. Add a DB seed row for the new service in `src/lib/db/migrations/` (`version_manager` table, `status='not_installed'`, `auto_start=0`).
|
| 311 |
-
4. Create 7 API endpoints under `src/app/api/services/{name}/` (`_lib.ts`, `install`, `start`, `stop`, `restart`, `update`, `status`, `auto-start`). All delegate errors through `createErrorResponse()`. The shared `logs` endpoint is already wired via `[name]/logs/route.ts`.
|
| 312 |
-
5. Verify `/api/services/` is in `LOCAL_ONLY_API_PREFIXES` in `src/server/authz/routeGuard.ts`; add a test asserting `isLocalOnlyPath()` returns `true` for the new prefix if you add one (hard rule #17).
|
| 313 |
-
6. Add a UI tab in `src/app/(dashboard)/dashboard/providers/services/tabs/` reusing `ServiceStatusCard`, `ServiceLifecycleButtons`, `ServiceLogsPanel`.
|
| 314 |
-
7. Document in `docs/frameworks/EMBEDDED-SERVICES.md` (update §1 service table + §4 API reference) and `docs/reference/openapi.yaml`.
|
| 315 |
-
8. Write tests: unit (`tests/unit/services/`), integration (`tests/integration/services/`, gated by `RUN_SERVICES_INT=1`), and update `docs/ops/RELEASE_CHECKLIST.md` smoke section.
|
| 316 |
-
|
| 317 |
-
### Adding a New Guardrail / Eval / Skill / Webhook event
|
| 318 |
-
|
| 319 |
-
- Guardrail: `src/lib/guardrails/` → docs: `docs/security/GUARDRAILS.md`
|
| 320 |
-
- Eval suite: `src/lib/evals/` → docs: `docs/frameworks/EVALS.md`
|
| 321 |
-
- Skill (sandbox): `src/lib/skills/` → docs: `docs/frameworks/SKILLS.md`
|
| 322 |
-
- Webhook event: `src/lib/webhookDispatcher.ts` → docs: `docs/frameworks/WEBHOOKS.md`
|
| 323 |
-
|
| 324 |
-
---
|
| 325 |
-
|
| 326 |
-
## Reference Documentation
|
| 327 |
-
|
| 328 |
-
For any non-trivial change, read the matching deep-dive first:
|
| 329 |
-
|
| 330 |
-
| Area | Doc |
|
| 331 |
-
| -------------------------------------------- | ----------------------------------------------------------------- |
|
| 332 |
-
| Repo navigation | `docs/architecture/REPOSITORY_MAP.md` |
|
| 333 |
-
| Architecture | `docs/architecture/ARCHITECTURE.md` |
|
| 334 |
-
| Engineering reference | `docs/architecture/CODEBASE_DOCUMENTATION.md` |
|
| 335 |
-
| Auto-Combo (9-factor scoring, 14 strategies) | `docs/routing/AUTO-COMBO.md` |
|
| 336 |
-
| Resilience (3 mechanisms) | `docs/architecture/RESILIENCE_GUIDE.md` |
|
| 337 |
-
| Reasoning replay | `docs/routing/REASONING_REPLAY.md` |
|
| 338 |
-
| Skills framework | `docs/frameworks/SKILLS.md` |
|
| 339 |
-
| Memory system (FTS5 + Qdrant) | `docs/frameworks/MEMORY.md` |
|
| 340 |
-
| Cloud agents | `docs/frameworks/CLOUD_AGENT.md` |
|
| 341 |
-
| Guardrails (PII / injection / vision) | `docs/security/GUARDRAILS.md` |
|
| 342 |
-
| Public upstream credentials (Gemini/etc.) | `docs/security/PUBLIC_CREDS.md` |
|
| 343 |
-
| Error message sanitization | `docs/security/ERROR_SANITIZATION.md` |
|
| 344 |
-
| Evals | `docs/frameworks/EVALS.md` |
|
| 345 |
-
| Compliance / audit | `docs/security/COMPLIANCE.md` |
|
| 346 |
-
| Webhooks | `docs/frameworks/WEBHOOKS.md` |
|
| 347 |
-
| Authorization pipeline | `docs/architecture/AUTHZ_GUIDE.md` |
|
| 348 |
-
| Stealth (TLS / fingerprint) | `docs/security/STEALTH_GUIDE.md` |
|
| 349 |
-
| Agent protocols (A2A / ACP / Cloud) | `docs/frameworks/AGENT_PROTOCOLS_GUIDE.md` |
|
| 350 |
-
| MCP server | `docs/frameworks/MCP-SERVER.md` |
|
| 351 |
-
| A2A server | `docs/frameworks/A2A-SERVER.md` |
|
| 352 |
-
| API reference + OpenAPI | `docs/reference/API_REFERENCE.md` + `docs/reference/openapi.yaml` |
|
| 353 |
-
| Provider catalog (auto-generated) | `docs/reference/PROVIDER_REFERENCE.md` |
|
| 354 |
-
| Release flow | `docs/ops/RELEASE_CHECKLIST.md` |
|
| 355 |
-
| Embedded services | `docs/frameworks/EMBEDDED-SERVICES.md` |
|
| 356 |
-
|
| 357 |
-
---
|
| 358 |
-
|
| 359 |
-
## Testing
|
| 360 |
-
|
| 361 |
-
| What | Command |
|
| 362 |
-
| ----------------------- | --------------------------------------------------------------------------- |
|
| 363 |
-
| Unit tests | `npm run test:unit` |
|
| 364 |
-
| Single file | `node --import tsx/esm --test tests/unit/file.test.ts` |
|
| 365 |
-
| Vitest (MCP, autoCombo) | `npm run test:vitest` |
|
| 366 |
-
| E2E (Playwright) | `npm run test:e2e` |
|
| 367 |
-
| Protocol E2E (MCP+A2A) | `npm run test:protocols:e2e` |
|
| 368 |
-
| Ecosystem | `npm run test:ecosystem` |
|
| 369 |
-
| Coverage gate | `npm run test:coverage` (60/60/60/60 — statements/lines/functions/branches) |
|
| 370 |
-
| Coverage report | `npm run coverage:report` |
|
| 371 |
-
|
| 372 |
-
**PR rule**: If you change production code in `src/`, `open-sse/`, `electron/`, or `bin/`, you must include or update tests in the same PR.
|
| 373 |
-
|
| 374 |
-
**Test layer preference**: unit first → integration (multi-module or DB state) → e2e (UI/workflow only). Encode bug reproductions as automated tests before or alongside the fix.
|
| 375 |
-
|
| 376 |
-
**Both test runners must pass**: `npm run test:unit` (Node native — most tests) AND `npm run test:vitest` (MCP server, autoCombo, cache) cover **non-overlapping files**. Both must be green before merging. A PR where only one suite passes may silently ship broken MCP tools or routing regressions.
|
| 377 |
-
|
| 378 |
-
**Bug fix / issue triage protocol (Hard Rule #18)**: Every fix for a reported issue must be validated by one of the following — no exceptions:
|
| 379 |
-
1. **TDD (preferred)** — write a failing test reproducing the bug → fix it → confirm the test passes. The test becomes the permanent regression guard. Touch only the files the test proves need changing; nothing more.
|
| 380 |
-
2. **Real-environment test (when TDD is not possible)** — deploy to the production VPS (`root@192.168.0.15`) and run a documented live test. Record the exact command + result in the PR description. Applies to: OAuth upstream flows, Cloudflare/WS upstream behavior, UI-only regressions, hardware-dependent behavior.
|
| 381 |
-
3. "It worked locally without a test" does not count. A fix without a test or a VPS validation record is not a fix — it is a guess.
|
| 382 |
-
|
| 383 |
-
Why this matters: fixing bug A while opening bug B is worse than not fixing at all. The TDD/VPS gate enforces surgical scope — you touch only what the failing test proves is broken. Examples where this paid off: #3090 (claude-web 403), #3113 (WS HTTP fallback), #3052 (heap-guard auto-calibration).
|
| 384 |
-
|
| 385 |
-
**Copilot coverage policy**: When a PR changes production code and coverage is below 60% (statements/lines/functions/branches), do not just report — add or update tests, rerun the coverage gate, then ask for confirmation. Include commands run, changed test files, and final coverage result in the PR report.
|
| 386 |
-
|
| 387 |
-
---
|
| 388 |
-
|
| 389 |
-
## Git Workflow
|
| 390 |
-
|
| 391 |
```bash
|
| 392 |
-
#
|
| 393 |
-
|
| 394 |
-
|
| 395 |
-
|
| 396 |
```
|
| 397 |
-
|
| 398 |
-
|
| 399 |
-
|
| 400 |
-
|
| 401 |
-
|
| 402 |
-
|
| 403 |
-
|
| 404 |
-
- *
|
| 405 |
-
-
|
| 406 |
-
|
| 407 |
-
|
| 408 |
-
|
| 409 |
-
|
| 410 |
-
|
| 411 |
-
-
|
| 412 |
-
|
| 413 |
-
|
| 414 |
-
|
| 415 |
-
|
| 416 |
-
-
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
|
| 439 |
-
|
| 440 |
-
|
| 441 |
-
|
| 442 |
-
|
| 443 |
-
|
| 444 |
-
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
|
| 449 |
-
### 2. SSE Snapshot Handling
|
| 450 |
-
When parsing streaming LLM responses (e.g. Responses API), check if a chunk represents a final snapshot (`done` or `completed` events). Snapshot text must be sanitized directly as a standalone string (bypassing rolling delta buffers) to prevent text duplication at the end of the stream.
|
| 451 |
-
|
| 452 |
-
### 3. Database Handles in Tests
|
| 453 |
-
Ensure that any unit tests that trigger database migrations or establish SQLite connections call `resetDbInstance()` and properly clean up/close all DB handles in a `test.after(...)` hook. Failure to release database connection handles will cause Node's native test runner to hang indefinitely.
|
|
|
|
| 2 |
|
| 3 |
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
| 4 |
|
| 5 |
+
## What this is
|
| 6 |
|
| 7 |
+
9Router (`9router-app`) — a local AI routing gateway + Next.js dashboard. It exposes one OpenAI-compatible endpoint (`/v1/*`) and routes traffic across 40+ upstream providers with format translation, model-combo fallback, multi-account fallback, OAuth/API-key credential management, token refresh, quota/usage tracking, and optional cloud sync.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
+
Two published artifacts live in this one repo:
|
| 10 |
+
- The **dashboard + gateway** (root `package.json`, `9router-app`) — the Next.js server that does the actual routing.
|
| 11 |
+
- The **CLI launcher** (`cli/`, published to npm as `9router`) — a separate package that installs/starts the server and manages the tray. It has its own `package.json`, version, and build.
|
|
|
|
|
|
|
| 12 |
|
| 13 |
+
The code lives in `src/` (Next.js app + dashboard/compat APIs), `open-sse/` (the provider-agnostic routing/translation engine), `cli/` (the launcher package), and `tests/`.
|
| 14 |
|
| 15 |
+
## Commands
|
|
|
|
|
|
|
| 16 |
|
| 17 |
+
Dashboard/gateway (run from repo root):
|
| 18 |
+
```bash
|
| 19 |
+
cp .env.example .env
|
| 20 |
+
npm install
|
| 21 |
+
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev # dev (webpack, port 20127 by default via next dev)
|
| 22 |
+
npm run build && PORT=20128 HOSTNAME=0.0.0.0 npm run start # production
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
```
|
| 24 |
+
- Bun variants: `npm run dev:bun` / `build:bun` / `start:bun`.
|
| 25 |
+
- Default runtime port is **20128** (dashboard at `/dashboard`, API at `/v1`).
|
| 26 |
+
- Lint: `npx eslint .` (config `eslint.config.mjs`, extends `eslint-config-next`).
|
| 27 |
|
| 28 |
+
CLI package (`cli/`):
|
| 29 |
+
```bash
|
| 30 |
+
npm run cli:pack # build + npm pack from root
|
| 31 |
+
cd cli && npm run dev # nodemon watch
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
```
|
| 33 |
|
| 34 |
+
Tests (vitest, in `tests/`, an **independent** ESM package — not wired into root `npm test`):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
```bash
|
| 36 |
+
npm install # ROOT deps first — tests import from src/ which needs `open`, `undici`, etc.
|
| 37 |
+
cd tests && npm install # then tests' own deps (vitest) → tests/node_modules (allowed by tests/.gitignore)
|
| 38 |
+
npx vitest run # all tests; auto-discovers tests/vitest.config.js
|
| 39 |
+
npx vitest run unit/capabilities.test.js # single file (path relative to tests/)
|
| 40 |
```
|
| 41 |
+
> The committed `tests/package.json` `test` script hardcodes Unix paths (`NODE_PATH=/tmp/node_modules …`) — a shared-install workaround from upstream. On Windows (or anywhere), ignore it and use the `npx vitest` form above; `vitest.config.js` resolves the `open-sse`/`@/` aliases from the repo root regardless of where vitest lives.
|
| 42 |
+
>
|
| 43 |
+
> **The suite is NOT expected to be all-green on a plain checkout.** ~938 pass, ~64 fail. Judge regressions with `tests/__baseline__/verify-no-regression.mjs`, not a raw run. Expected red:
|
| 44 |
+
> - 26 catalogued in `tests/__baseline__/known-fails.txt` (rtk, oauth-cursor-auto-import, translator-request-normalization, …).
|
| 45 |
+
> - `unit/embeddings.cloud.test.js` imports `cloud/src/handlers/embeddings.js` — the `cloud/` worker dir is **not in this repo**, so it always fails here.
|
| 46 |
+
> - `unit/xai-oauth-service.test.js` times out (5s) when the xAI endpoint-discovery fetch isn't reachable/mocked.
|
| 47 |
+
> - `real/*.real.test.js` make live provider calls — need credentials, skip otherwise.
|
| 48 |
+
- `*.real.test.js` under `tests/translator/real/` make live provider calls — skip unless credentials are set.
|
| 49 |
+
- Regression baselines: `tests/__baseline__/verify-*.mjs` compare against committed snapshots (providers, aliases, OAuth URLs). Run these after touching provider registry / alias logic.
|
| 50 |
+
|
| 51 |
+
## Architecture
|
| 52 |
+
|
| 53 |
+
Two authoritative docs already exist — read them before working in these areas rather than re-deriving:
|
| 54 |
+
- `docs/ARCHITECTURE.md` — full system: request lifecycle, combo/account fallback, OAuth + token refresh, cloud sync, data model.
|
| 55 |
+
- `open-sse/AGENTS.md` — the routing/translation engine's own conventions and "how to add a provider/executor/translator". **Read this before editing anything under `open-sse/`.**
|
| 56 |
+
|
| 57 |
+
### Request flow (the thing to understand first)
|
| 58 |
+
`src/app/api/v1/*` route (Next rewrite maps `/v1/*` → `/api/v1/*` in `next.config.mjs`)
|
| 59 |
+
→ `src/sse/handlers/chat.js` (parse, combo expansion, account-selection loop)
|
| 60 |
+
→ `open-sse/handlers/chatCore.js` (detect source format, translate request, dispatch to executor, retry/refresh, stream setup)
|
| 61 |
+
→ `open-sse/executors/*` (per-provider upstream call; `default.js` handles any OpenAI-compatible provider)
|
| 62 |
+
→ `open-sse/translator/*` (client format ↔ provider format)
|
| 63 |
+
→ SSE back to client.
|
| 64 |
+
|
| 65 |
+
`src/sse/` is the app-side entry glue; `open-sse/` is the provider-agnostic engine (also usable standalone). Cross that boundary consciously.
|
| 66 |
+
|
| 67 |
+
### Translator engine (`open-sse/translator/`)
|
| 68 |
+
- Pivots through **OpenAI as the intermediate format**. A translator registered on an exact `source:target` pair (e.g. `claude:kiro`) runs as a **direct route**, skipping the lossy double-hop. Prefer a direct route for fragile pairs (thinking blocks, tool ids, non-base64 images, `is_error`).
|
| 69 |
+
- Translators **self-register** via `register(from, to, reqFn, resFn)` as an import side effect — a new translator file MUST be imported in `open-sse/translator/index.js` or it never runs.
|
| 70 |
+
- Never hardcode role/block/model strings — use `open-sse/translator/schema/` and `open-sse/config/` constants. Config-driven and DRY is enforced by convention here.
|
| 71 |
+
|
| 72 |
+
### Provider registry (`open-sse/providers/registry/*`)
|
| 73 |
+
- One file per provider. `providers/registry/index.js` is an **auto-generated** static import list — regenerate it with `scripts/migrate-registry.mjs` / `injectDisplayToRegistry.mjs`, don't hand-edit.
|
| 74 |
+
- Add a provider: copy `providers/REGISTRY_TEMPLATE.js`, add models to `config/providerModels.js`. Only add an executor for non-OpenAI-compatible upstreams.
|
| 75 |
+
|
| 76 |
+
### Persistence — IMPORTANT (ARCHITECTURE.md is stale here)
|
| 77 |
+
State is **no longer `db.json`**. It's a SQLite layer under `src/lib/db/` with an adapter fallback chain (`driver.js`): `bun:sqlite` → `better-sqlite3` (optional native dep) → `node:sqlite` (Node ≥22.5) → `sql.js` (pure-JS fallback, always works). `better-sqlite3` is deliberately in `optionalDependencies` so install never fails without build tools.
|
| 78 |
+
- `src/lib/localDb.js` is a **backward-compat shim** re-exporting `src/lib/db/index.js`. New code should import from `@/lib/db/index.js`; per-entity logic lives in `src/lib/db/repos/*`. Schema/migrations in `src/lib/db/migrations/`.
|
| 79 |
+
- DB file location resolves via `src/lib/db/paths.js` (`DATA_DIR`, else `~/.9router/`).
|
| 80 |
+
- Usage/logs (`src/lib/usageDb.js`, `usage.json` + `log.txt`) still live under `~/.9router` and do **not** follow `DATA_DIR`.
|
| 81 |
+
|
| 82 |
+
### RTK token saver (`open-sse/rtk/`)
|
| 83 |
+
Pre-translate hooks that compress `tool_result` content in-place to cut tokens. **Fail-open**: any error returns null and leaves the body untouched — never throw out of them. Skips `is_error`/`status:"error"` results to preserve traces.
|
| 84 |
+
|
| 85 |
+
## Conventions & gotchas
|
| 86 |
+
|
| 87 |
+
- Plain JavaScript (ESM), no TypeScript. `@/*` path alias → `src/*` (`jsconfig.json`).
|
| 88 |
+
- `custom-server.js` wraps the Next standalone server to derive client IP from the TCP socket and strip attacker-controlled `X-Forwarded-For` — trusting forwarding headers only from a loopback reverse proxy. Preserve this when touching request/IP/rate-limit code.
|
| 89 |
+
- Security-sensitive env: `JWT_SECRET` (session cookie), `INITIAL_PASSWORD` (default `123456` — must override), `API_KEY_SECRET`, `MACHINE_ID_SALT`. Full env contract in `.env.example` and ARCHITECTURE.md's env matrix.
|
| 90 |
+
- Binary/protobuf upstreams (kiro EventStream, cursor protobuf, commandcode NDJSON) don't round-trip through OpenAI — they're handled inside their own executor, not the translator.
|
| 91 |
+
- Versioning: root and `cli/` are versioned independently; changes are logged in `CHANGELOG.md`. Commit style is Conventional Commits (`fix(translator): …`, `feat(...)`).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DOCKER.md
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Docker
|
| 2 |
+
|
| 3 |
+
Run 9Router in a container. Published image: [`decolua/9router`](https://hub.docker.com/r/decolua/9router) — multi-platform `linux/amd64` + `linux/arm64`.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
# 👤 For Users
|
| 8 |
+
|
| 9 |
+
## Quick start
|
| 10 |
+
|
| 11 |
+
```bash
|
| 12 |
+
docker run -d \
|
| 13 |
+
-p 20128:20128 \
|
| 14 |
+
-v "$HOME/.9router:/app/data" \
|
| 15 |
+
-e DATA_DIR=/app/data \
|
| 16 |
+
--name 9router \
|
| 17 |
+
decolua/9router:latest
|
| 18 |
+
```
|
| 19 |
+
|
| 20 |
+
App listens on port `20128`. Open: http://localhost:20128
|
| 21 |
+
|
| 22 |
+
## Manage container
|
| 23 |
+
|
| 24 |
+
```bash
|
| 25 |
+
docker logs -f 9router # view logs
|
| 26 |
+
docker stop 9router # stop
|
| 27 |
+
docker start 9router # start again
|
| 28 |
+
docker rm -f 9router # remove
|
| 29 |
+
```
|
| 30 |
+
|
| 31 |
+
## Data persistence
|
| 32 |
+
|
| 33 |
+
```bash
|
| 34 |
+
-v "$HOME/.9router:/app/data" \
|
| 35 |
+
-e DATA_DIR=/app/data
|
| 36 |
+
```
|
| 37 |
+
|
| 38 |
+
Without `DATA_DIR`, the app falls back to `~/.9router/` (macOS/Linux) or `%APPDATA%\9router\` (Windows). In the container, `DATA_DIR=/app/data` makes the bind mount work.
|
| 39 |
+
|
| 40 |
+
Data layout under `$DATA_DIR/`:
|
| 41 |
+
|
| 42 |
+
```text
|
| 43 |
+
$DATA_DIR/
|
| 44 |
+
├── db/
|
| 45 |
+
│ ├── data.sqlite # main SQLite database
|
| 46 |
+
│ └── backups/ # auto backups
|
| 47 |
+
└── ... # certs, logs, runtime configs
|
| 48 |
+
```
|
| 49 |
+
|
| 50 |
+
Host path: `$HOME/.9router/db/data.sqlite`
|
| 51 |
+
Container path: `/app/data/db/data.sqlite`
|
| 52 |
+
|
| 53 |
+
## Optional env vars
|
| 54 |
+
|
| 55 |
+
```bash
|
| 56 |
+
docker run -d \
|
| 57 |
+
-p 20128:20128 \
|
| 58 |
+
-v "$HOME/.9router:/app/data" \
|
| 59 |
+
-e DATA_DIR=/app/data \
|
| 60 |
+
-e PORT=20128 \
|
| 61 |
+
-e HOSTNAME=0.0.0.0 \
|
| 62 |
+
-e DEBUG=true \
|
| 63 |
+
--name 9router \
|
| 64 |
+
decolua/9router:latest
|
| 65 |
+
```
|
| 66 |
+
|
| 67 |
+
## Optional Headroom sidecar
|
| 68 |
+
|
| 69 |
+
The 9Router image does not bundle Python or Headroom. To use Headroom in Docker, run it as a separate service and point 9Router at that proxy:
|
| 70 |
+
|
| 71 |
+
```yaml
|
| 72 |
+
services:
|
| 73 |
+
9router:
|
| 74 |
+
image: decolua/9router:latest
|
| 75 |
+
ports:
|
| 76 |
+
- "20128:20128"
|
| 77 |
+
volumes:
|
| 78 |
+
- "$HOME/.9router:/app/data"
|
| 79 |
+
environment:
|
| 80 |
+
DATA_DIR: /app/data
|
| 81 |
+
HEADROOM_URL: http://headroom:8787
|
| 82 |
+
depends_on:
|
| 83 |
+
- headroom
|
| 84 |
+
|
| 85 |
+
headroom:
|
| 86 |
+
image: ghcr.io/chopratejas/headroom:latest
|
| 87 |
+
ports:
|
| 88 |
+
- "8787:8787"
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
In the dashboard, open `Endpoint` → `Token Saver` → `Headroom`, confirm the URL is `http://headroom:8787`, recheck status, then enable Headroom.
|
| 92 |
+
|
| 93 |
+
If Headroom runs on the Docker host instead of as a sidecar, use `http://host.docker.internal:8787` on macOS/Windows. On Linux, add `--add-host=host.docker.internal:host-gateway` or the equivalent compose `extra_hosts` entry.
|
| 94 |
+
|
| 95 |
+
## Update to latest
|
| 96 |
+
|
| 97 |
+
```bash
|
| 98 |
+
docker pull decolua/9router:latest
|
| 99 |
+
docker rm -f 9router
|
| 100 |
+
# re-run the quick start command
|
| 101 |
+
```
|
| 102 |
+
|
| 103 |
+
---
|
| 104 |
+
|
| 105 |
+
# 🛠 For Developers
|
| 106 |
+
|
| 107 |
+
## Build image locally (test)
|
| 108 |
+
|
| 109 |
+
```bash
|
| 110 |
+
cd app && docker build -t 9router .
|
| 111 |
+
|
| 112 |
+
docker run --rm -p 20128:20128 \
|
| 113 |
+
-v "$HOME/.9router:/app/data" \
|
| 114 |
+
-e DATA_DIR=/app/data \
|
| 115 |
+
9router
|
| 116 |
+
```
|
| 117 |
+
|
| 118 |
+
## Publish (automatic via CI)
|
| 119 |
+
|
| 120 |
+
Push a git tag `v*` → GitHub Actions builds multi-platform (amd64+arm64) and pushes to:
|
| 121 |
+
- `ghcr.io/decolua/9router:v{version}` + `:latest`
|
| 122 |
+
- `decolua/9router:v{version}` + `:latest`
|
| 123 |
+
|
| 124 |
+
```bash
|
| 125 |
+
# Use scripts/release.js (recommended)
|
| 126 |
+
node scripts/release.js "Release title" "Notes"
|
| 127 |
+
|
| 128 |
+
# Or manually
|
| 129 |
+
git tag v0.4.x && git push origin v0.4.x
|
| 130 |
+
```
|
| 131 |
+
|
| 132 |
+
Workflow: `app/.github/workflows/docker-publish.yml`
|
Dockerfile
CHANGED
|
@@ -1,161 +1,46 @@
|
|
| 1 |
-
#
|
| 2 |
-
|
|
|
|
| 3 |
WORKDIR /app
|
|
|
|
| 4 |
|
| 5 |
-
RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
|
| 6 |
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
|
| 7 |
-
apt-get update \
|
| 8 |
-
&& apt-get install -y --no-install-recommends libsecret-1-0 ca-certificates \
|
| 9 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 10 |
-
|
| 11 |
-
# ── Builder ────────────────────────────────────────────────────────────────
|
| 12 |
FROM base AS builder
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
# apt-get update needed here because base's rm -rf clears the shared cache
|
| 16 |
-
RUN --mount=type=cache,target=/var/cache/apt,sharing=shared \
|
| 17 |
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=shared \
|
| 18 |
-
apt-get update \
|
| 19 |
-
&& apt-get install -y --no-install-recommends python3 make g++ \
|
| 20 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 21 |
-
|
| 22 |
-
COPY package*.json ./
|
| 23 |
-
COPY scripts/build/postinstall.mjs ./scripts/build/postinstall.mjs
|
| 24 |
-
COPY scripts/build/postinstallSupport.mjs ./scripts/build/postinstallSupport.mjs
|
| 25 |
-
COPY scripts/build/native-binary-compat.mjs ./scripts/build/native-binary-compat.mjs
|
| 26 |
-
ENV NPM_CONFIG_LEGACY_PEER_DEPS=true
|
| 27 |
-
# --ignore-scripts blocks broad dependency install/postinstall hooks, closing
|
| 28 |
-
# the supply-chain attack surface where a transitive dep can run arbitrary code
|
| 29 |
-
# at install time. better-sqlite3 still needs a native binding for the target
|
| 30 |
-
# platform, so rebuild and smoke-test only that known runtime dependency below.
|
| 31 |
-
#
|
| 32 |
-
# We REQUIRE a committed package-lock.json so resolved dependency versions
|
| 33 |
-
# are reproducible.
|
| 34 |
-
RUN test -f package-lock.json \
|
| 35 |
-
|| (echo "package-lock.json is required for reproducible Docker builds" >&2 && exit 1)
|
| 36 |
RUN --mount=type=cache,target=/root/.npm \
|
| 37 |
-
npm
|
| 38 |
-
&& npm rebuild better-sqlite3 \
|
| 39 |
-
&& node -e "require('better-sqlite3')(':memory:').close()"
|
| 40 |
-
|
| 41 |
-
# Use Turbopack for significant build speedup
|
| 42 |
-
ENV OMNIROUTE_USE_TURBOPACK=1
|
| 43 |
-
|
| 44 |
COPY . ./
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
# ── Runner base ────────────────────────────────────────────────────────────
|
| 49 |
-
FROM base AS runner-base
|
| 50 |
-
|
| 51 |
-
LABEL org.opencontainers.image.title="omniroute" \
|
| 52 |
-
org.opencontainers.image.description="Unified AI proxy — route any LLM through one endpoint" \
|
| 53 |
-
org.opencontainers.image.url="https://omniroute.online" \
|
| 54 |
-
org.opencontainers.image.source="https://github.com/diegosouzapw/OmniRoute" \
|
| 55 |
-
org.opencontainers.image.licenses="MIT"
|
| 56 |
|
|
|
|
|
|
|
|
|
|
| 57 |
ENV NODE_ENV=production
|
| 58 |
ENV PORT=7860
|
| 59 |
ENV HOSTNAME=0.0.0.0
|
| 60 |
-
ENV
|
| 61 |
-
ENV
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
#
|
| 80 |
-
|
| 81 |
-
# migrations land at <standalone>/migrations via assembleStandalone; point the runtime at them.
|
| 82 |
-
ENV OMNIROUTE_MIGRATIONS_DIR=/app/migrations
|
| 83 |
-
|
| 84 |
-
# Docker healthcheck script — not traced by Next.js standalone output, so copy
|
| 85 |
-
# it explicitly. The HEALTHCHECK CMD references it as `node healthcheck.mjs`.
|
| 86 |
-
COPY --from=builder /app/scripts/dev/healthcheck.mjs ./healthcheck.mjs
|
| 87 |
-
|
| 88 |
-
# Hand /app over to the baked-in `node` non-root user (UID/GID 1000) so the
|
| 89 |
-
# runtime process never holds root privileges. The chown happens after all
|
| 90 |
-
# COPYs so it covers files originally owned by root in the builder stage.
|
| 91 |
-
RUN chown -R node:node /app
|
| 92 |
|
| 93 |
EXPOSE 7860
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
# runner-web) also runs as a non-root user unless they explicitly switch back.
|
| 97 |
-
USER node
|
| 98 |
-
|
| 99 |
-
# Warns if the mounted data volume has wrong ownership
|
| 100 |
-
COPY --chmod=755 scripts/check-permissions.sh /tmp/check-permissions.sh
|
| 101 |
-
ENTRYPOINT ["/tmp/check-permissions.sh"]
|
| 102 |
-
|
| 103 |
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \
|
| 104 |
-
CMD ["node", "healthcheck.mjs"]
|
| 105 |
-
|
| 106 |
-
CMD ["node", "dev/run-standalone.mjs"]
|
| 107 |
-
|
| 108 |
-
# ── Runner Web (web-cookie providers: Gemini Web, Claude Turnstile) ───────────
|
| 109 |
-
#
|
| 110 |
-
# Two image flavors:
|
| 111 |
-
# runner-base → omniroute:VERSION Lean base (~500 MB). No browsers.
|
| 112 |
-
# runner-web → omniroute:VERSION-web +Chromium/Playwright (~800 MB).
|
| 113 |
-
#
|
| 114 |
-
# Use runner-web when you need web-cookie providers (gemini-web, claude-web,
|
| 115 |
-
# claude-turnstile). For all other providers runner-base is sufficient.
|
| 116 |
-
#
|
| 117 |
-
# Build:
|
| 118 |
-
# docker build --target runner-web -t omniroute:web .
|
| 119 |
-
# Compose:
|
| 120 |
-
# build:
|
| 121 |
-
# context: .
|
| 122 |
-
# target: runner-web
|
| 123 |
-
FROM runner-base AS runner-web
|
| 124 |
-
|
| 125 |
-
USER root
|
| 126 |
-
|
| 127 |
-
# Install Playwright browser binaries + OS dependencies under root, then hand
|
| 128 |
-
# ownership of the browsers cache to the node user.
|
| 129 |
-
# PLAYWRIGHT_BROWSERS_PATH overrides the default ~/.cache/ms-playwright so the
|
| 130 |
-
# browsers land under /home/node which persists across image layers and is
|
| 131 |
-
# accessible to the non-root runtime user.
|
| 132 |
-
ENV PLAYWRIGHT_BROWSERS_PATH=/home/node/.cache/ms-playwright
|
| 133 |
-
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
| 134 |
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
| 135 |
-
apt-get update \
|
| 136 |
-
&& npx playwright install chromium --with-deps \
|
| 137 |
-
&& chown -R node:node /home/node/.cache \
|
| 138 |
-
&& rm -rf /var/lib/apt/lists/*
|
| 139 |
-
|
| 140 |
-
USER node
|
| 141 |
-
|
| 142 |
-
FROM runner-base AS runner-cli
|
| 143 |
-
|
| 144 |
-
# Drop back to root briefly so we can install system + global npm packages,
|
| 145 |
-
# then return to the `node` non-root user before the CMD inherited from
|
| 146 |
-
# runner-base runs.
|
| 147 |
-
USER root
|
| 148 |
-
|
| 149 |
-
# Install system dependencies required by openclaw (git+ssh references).
|
| 150 |
-
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
| 151 |
-
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
| 152 |
-
apt-get update \
|
| 153 |
-
&& apt-get install -y --no-install-recommends git ca-certificates docker.io docker-compose \
|
| 154 |
-
&& rm -rf /var/lib/apt/lists/* \
|
| 155 |
-
&& git config --system url."https://github.com/".insteadOf "ssh://git@github.com/"
|
| 156 |
-
|
| 157 |
-
# Install CLI tools globally. Separate layer from apt for better cache reuse.
|
| 158 |
-
RUN --mount=type=cache,target=/root/.npm \
|
| 159 |
-
npm install -g --no-audit --no-fund @openai/codex @anthropic-ai/claude-code droid openclaw@latest
|
| 160 |
-
|
| 161 |
-
USER node
|
|
|
|
| 1 |
+
# syntax=docker/dockerfile:1.7
|
| 2 |
+
ARG NODE_IMAGE=node:22-alpine
|
| 3 |
+
FROM ${NODE_IMAGE} AS base
|
| 4 |
WORKDIR /app
|
| 5 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 6 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
FROM base AS builder
|
| 8 |
+
RUN apk --no-cache upgrade && apk --no-cache add python3 make g++ linux-headers
|
| 9 |
+
COPY package.json ./
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
RUN --mount=type=cache,target=/root/.npm \
|
| 11 |
+
npm install
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
COPY . ./
|
| 13 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 14 |
+
RUN npm run build:hf
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
+
FROM ${NODE_IMAGE} AS runner
|
| 17 |
+
WORKDIR /app
|
| 18 |
+
LABEL org.opencontainers.image.title="9router-hf"
|
| 19 |
ENV NODE_ENV=production
|
| 20 |
ENV PORT=7860
|
| 21 |
ENV HOSTNAME=0.0.0.0
|
| 22 |
+
ENV NEXT_TELEMETRY_DISABLED=1
|
| 23 |
+
ENV DATA_DIR=/app/data
|
| 24 |
+
|
| 25 |
+
COPY --from=builder /app/public ./public
|
| 26 |
+
COPY --from=builder /app/.next/static ./.next/static
|
| 27 |
+
COPY --from=builder /app/.next/standalone ./
|
| 28 |
+
COPY --from=builder /app/custom-server.js ./custom-server.js
|
| 29 |
+
COPY --from=builder /app/hf-server.js ./hf-server.js
|
| 30 |
+
COPY --from=builder /app/open-sse ./open-sse
|
| 31 |
+
COPY --from=builder /app/src/mitm ./src/mitm
|
| 32 |
+
COPY --from=builder /app/node_modules/node-forge ./node_modules/node-forge
|
| 33 |
+
COPY --from=builder /app/node_modules/next ./node_modules/next
|
| 34 |
+
COPY --from=builder /app/node_modules/sql.js ./node_modules/sql.js
|
| 35 |
+
|
| 36 |
+
RUN mkdir -p /app/data && chown -R node:node /app && \
|
| 37 |
+
mkdir -p /app/data-home && chown node:node /app/data-home && \
|
| 38 |
+
ln -sf /app/data-home /root/.9router 2>/dev/null || true
|
| 39 |
+
|
| 40 |
+
RUN apk --no-cache upgrade && apk --no-cache add su-exec && \
|
| 41 |
+
printf '#!/bin/sh\nchown -R node:node /app/data /app/data-home 2>/dev/null\nexec su-exec node "$@"\n' > /entrypoint.sh && \
|
| 42 |
+
chmod +x /entrypoint.sh
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
EXPOSE 7860
|
| 45 |
+
ENTRYPOINT ["/entrypoint.sh"]
|
| 46 |
+
CMD ["node", "hf-server.js"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
MIT License
|
| 2 |
|
| 3 |
-
Copyright (c) 2026
|
| 4 |
|
| 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
of this software and associated documentation files (the "Software"), to deal
|
|
|
|
| 1 |
MIT License
|
| 2 |
|
| 3 |
+
Copyright (c) 2024-2026 decolua and contributors
|
| 4 |
|
| 5 |
Permission is hereby granted, free of charge, to any person obtaining a copy
|
| 6 |
of this software and associated documentation files (the "Software"), to deal
|
README.md
CHANGED
|
@@ -1,10 +1,1526 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<div align="center">
|
| 2 |
+
<img src="./images/9router.png?1" alt="9Router Dashboard" width="800"/>
|
| 3 |
+
|
| 4 |
+
# 9Router - FREE AI Router & Token Saver
|
| 5 |
+
|
| 6 |
+
**Never stop coding. Save 20-40% tokens with RTK + auto-fallback to FREE & cheap AI models.**
|
| 7 |
+
|
| 8 |
+
**Connect All AI Code Tools (Claude Code, Cursor, Antigravity, Copilot, Codex, Gemini, OpenCode, Cline, OpenClaw...) to 40+ AI Providers & 100+ Models.**
|
| 9 |
+
|
| 10 |
+
[](https://www.npmjs.com/package/9router)
|
| 11 |
+
[](https://www.npmjs.com/package/9router)
|
| 12 |
+
[](https://hub.docker.com/r/decolua/9router)
|
| 13 |
+
[](https://github.com/decolua/9router/pkgs/container/9router)
|
| 14 |
+
[](https://github.com/decolua/9router/blob/main/LICENSE)
|
| 15 |
+
|
| 16 |
+
<a href="https://trendshift.io/repositories/22628" target="_blank"><img src="https://trendshift.io/api/badge/repositories/22628" alt="decolua%2F9router | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
| 17 |
+
|
| 18 |
+
[🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [📖 Setup](#-setup-guide) • [🌐 Website](https://9router.com)
|
| 19 |
+
|
| 20 |
+
[🇧🇷 Português (Brasil)](./i18n/README.pt-BR.md) • [🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) • [🇯🇵 日本語](./i18n/README.ja-JP.md) • [🇷🇺 Русский](./i18n/README.ru.md) • [🇹🇭 ไทย](./i18n/README.th.md) • [🇮🇷 فارسی](./i18n/README.fa_IR.md) • [🇮🇩 Indonesia](./i18n/README.id-ID.md) • [🇪🇸 Español](./i18n/README.es.md) • [🇫🇷 Français](./i18n/README.fr.md)
|
| 21 |
+
|
| 22 |
+
</div>
|
| 23 |
+
|
| 24 |
+
---
|
| 25 |
+
|
| 26 |
+
## 🤔 Why 9Router?
|
| 27 |
+
|
| 28 |
+
**Stop wasting money, tokens and hitting limits:**
|
| 29 |
+
|
| 30 |
+
- ❌ Subscription quota expires unused every month
|
| 31 |
+
- ❌ Rate limits stop you mid-coding
|
| 32 |
+
- ❌ Tool outputs (git diff, grep, ls...) burn tokens fast
|
| 33 |
+
- ❌ Expensive APIs ($20-50/month per provider)
|
| 34 |
+
- ❌ Manual switching between providers
|
| 35 |
+
|
| 36 |
+
**9Router solves this:**
|
| 37 |
+
|
| 38 |
+
- ✅ **RTK Token Saver** - Auto-compress tool_result content, save 20-40% tokens per request
|
| 39 |
+
- ✅ **Maximize subscriptions** - Track quota, use every bit before reset
|
| 40 |
+
- ✅ **Auto fallback** - Subscription → Cheap → Free, zero downtime
|
| 41 |
+
- ✅ **Multi-account** - Round-robin between accounts per provider
|
| 42 |
+
- ✅ **Universal** - Works with Claude Code, Codex, Cursor, Cline, any CLI tool
|
| 43 |
+
|
| 44 |
+
---
|
| 45 |
+
|
| 46 |
+
## 🔄 How It Works
|
| 47 |
+
|
| 48 |
+
```
|
| 49 |
+
┌─────────────┐
|
| 50 |
+
│ Your CLI │ (Claude Code, Codex, OpenClaw, Cursor, Cline...)
|
| 51 |
+
│ Tool │
|
| 52 |
+
└──────┬──────┘
|
| 53 |
+
│ http://localhost:20128/v1
|
| 54 |
+
↓
|
| 55 |
+
┌─────────────────────────────────────────────┐
|
| 56 |
+
│ 9Router (Smart Router) │
|
| 57 |
+
│ • RTK Token Saver (cut tool_result tokens) │
|
| 58 |
+
│ • Format translation (OpenAI ↔ Claude) │
|
| 59 |
+
│ • Quota tracking │
|
| 60 |
+
│ • Auto token refresh │
|
| 61 |
+
└──────┬──────────────────────────────────────┘
|
| 62 |
+
│
|
| 63 |
+
├─→ [Tier 1: SUBSCRIPTION] Claude Code, Codex, GitHub Copilot
|
| 64 |
+
│ ↓ quota exhausted
|
| 65 |
+
├─→ [Tier 2: CHEAP] GLM ($0.6/1M), MiniMax ($0.2/1M)
|
| 66 |
+
│ ↓ budget limit
|
| 67 |
+
└─→ [Tier 3: FREE] Kiro, OpenCode Free, Vertex ($300 credits)
|
| 68 |
+
|
| 69 |
+
Result: Never stop coding, minimal cost + 20-40% token savings via RTK
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## ⚡ Quick Start
|
| 75 |
+
|
| 76 |
+
**1. Install globally:**
|
| 77 |
+
|
| 78 |
+
```bash
|
| 79 |
+
npm install -g 9router
|
| 80 |
+
9router
|
| 81 |
+
```
|
| 82 |
+
|
| 83 |
+
🎉 Dashboard opens at `http://localhost:20128`
|
| 84 |
+
|
| 85 |
+
**2. Connect a FREE provider (no signup needed):**
|
| 86 |
+
|
| 87 |
+
Dashboard → Providers → Connect **Kiro AI** (~50 credits/month free: Claude 4.5 + GLM-5 + MiniMax) or **OpenCode Free** (no auth) → Done!
|
| 88 |
+
|
| 89 |
+
**3. Use in your CLI tool:**
|
| 90 |
+
|
| 91 |
+
```
|
| 92 |
+
Claude Code/Codex/OpenClaw/Cursor/Cline Settings:
|
| 93 |
+
Endpoint: http://localhost:20128/v1
|
| 94 |
+
API Key: [copy from dashboard]
|
| 95 |
+
Model: kr/claude-sonnet-4.5
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
**That's it!** Start coding with FREE AI models.
|
| 99 |
+
|
| 100 |
+
**Alternative: run from source (this repository):**
|
| 101 |
+
|
| 102 |
+
This repository package is private (`9router-app`), so source/Docker execution is the expected local development path.
|
| 103 |
+
|
| 104 |
+
```bash
|
| 105 |
+
cp .env.example .env
|
| 106 |
+
npm install
|
| 107 |
+
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
|
| 108 |
+
```
|
| 109 |
+
|
| 110 |
+
Production mode:
|
| 111 |
+
|
| 112 |
+
```bash
|
| 113 |
+
npm run build
|
| 114 |
+
PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start
|
| 115 |
+
```
|
| 116 |
+
|
| 117 |
+
Default URLs:
|
| 118 |
+
|
| 119 |
+
- Dashboard: `http://localhost:20128/dashboard`
|
| 120 |
+
- OpenAI-compatible API: `http://localhost:20128/v1`
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
## Video Guides
|
| 125 |
+
|
| 126 |
+
<div align="center">
|
| 127 |
+
|
| 128 |
+
<table>
|
| 129 |
+
<tr>
|
| 130 |
+
<td align="center" width="320">
|
| 131 |
+
<a href="https://www.youtube.com/watch?v=X69n5Lm06Yw">
|
| 132 |
+
<img src="https://img.youtube.com/vi/X69n5Lm06Yw/maxresdefault.jpg" alt="Tiết kiệm chi phí LLM với 9Router" width="300"/>
|
| 133 |
+
</a><br/>
|
| 134 |
+
<b>🇻🇳 Tiếng Việt</b><br/>
|
| 135 |
+
<sub>Tiết kiệm chi phí LLM cho OpenClaw với 9Router<br/>by <a href="https://www.youtube.com/c/M%C3%ACAIblog">Mì AI</a></sub>
|
| 136 |
+
</td>
|
| 137 |
+
<td align="center" width="320">
|
| 138 |
+
<a href="https://youtu.be/VQAw612S27Y">
|
| 139 |
+
<img src="https://img.youtube.com/vi/VQAw612S27Y/maxresdefault.jpg" alt="9Router + Claude Code FREE Unlimited Setup" width="300"/>
|
| 140 |
+
</a><br/>
|
| 141 |
+
<b>🇵🇰 اردو / हिन्दी</b><br/>
|
| 142 |
+
<sub>9Router + Claude Code FREE Unlimited Setup<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 143 |
+
</td>
|
| 144 |
+
<td align="center" width="320">
|
| 145 |
+
<a href="https://www.youtube.com/watch?v=raEyZPg5xE0">
|
| 146 |
+
<img src="https://img.youtube.com/vi/raEyZPg5xE0/maxresdefault.jpg" alt="9Router Setup Tutorial" width="300"/>
|
| 147 |
+
</a><br/>
|
| 148 |
+
<b>🇺🇸 English</b><br/>
|
| 149 |
+
<sub>9Router + Claude Code FREE Setup<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 150 |
+
</td>
|
| 151 |
+
<td align="center" width="320">
|
| 152 |
+
<a href="https://youtu.be/3dF5GIYMrcQ?si=bAyfyiHbARJQAHj_">
|
| 153 |
+
<img src="https://img.youtube.com/vi/3dF5GIYMrcQ/hqdefault.jpg" alt="9Router Setup Tutorial" width="300"/>
|
| 154 |
+
</a><br/>
|
| 155 |
+
<b>🇺🇸 English</b><br/>
|
| 156 |
+
<sub>9Router + Claude Code FREE Setup<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 157 |
+
</td>
|
| 158 |
+
<td align="center" width="320">
|
| 159 |
+
<a href="https://www.youtube.com/watch?v=o3qYCyjrFYg">
|
| 160 |
+
<img src="https://img.youtube.com/vi/o3qYCyjrFYg/maxresdefault.jpg" alt="Claude Code FREE Forever" width="300"/>
|
| 161 |
+
</a><br/>
|
| 162 |
+
<b>🇺🇸 English</b><br/>
|
| 163 |
+
<sub>Claude Code FREE Forever — Unlimited Models<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 164 |
+
</td>
|
| 165 |
+
</tr>
|
| 166 |
+
<tr>
|
| 167 |
+
<td align="center" width="320">
|
| 168 |
+
<a href="https://www.youtube.com/watch?v=Ttpc26m39Dw">
|
| 169 |
+
<img src="https://img.youtube.com/vi/Ttpc26m39Dw/maxresdefault.jpg" alt="Claude CLI Free Setup" width="300"/>
|
| 170 |
+
</a><br/>
|
| 171 |
+
<b>🇺🇸 English</b><br/>
|
| 172 |
+
<sub>Claude CLI Free Setup with 9Router 🚀<br/>by <a href="https://www.youtube.com/@CodeVerseSoban">CodeVerse Soban</a></sub>
|
| 173 |
+
</td>
|
| 174 |
+
<td align="center" width="320">
|
| 175 |
+
<a href="https://www.youtube.com/watch?v=G-5A_D5Pm6Y">
|
| 176 |
+
<img src="https://img.youtube.com/vi/G-5A_D5Pm6Y/maxresdefault.jpg" alt="Cài đặt OpenClaw Free A-Z" width="300"/>
|
| 177 |
+
</a><br/>
|
| 178 |
+
<b>🇻🇳 Tiếng Việt</b><br/>
|
| 179 |
+
<sub>Cài Đặt OpenClaw Free Từ A-Z + 9Router<br/>by <a href="https://www.youtube.com/@maigia">Mai Gia</a></sub>
|
| 180 |
+
</td>
|
| 181 |
+
<td align="center" width="320">
|
| 182 |
+
<a href="https://www.youtube.com/watch?v=JXmg8_gccgE">
|
| 183 |
+
<img src="https://img.youtube.com/vi/JXmg8_gccgE/maxresdefault.jpg" alt="FREE OpenClaw with Claude Opus" width="300"/>
|
| 184 |
+
</a><br/>
|
| 185 |
+
<b>🇺🇸 English</b><br/>
|
| 186 |
+
<sub>FREE OpenClaw + Claude Opus 4.6<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 187 |
+
</td>
|
| 188 |
+
<td align="center" width="320">
|
| 189 |
+
<a href="https://www.youtube.com/watch?v=CkVZZUSTXAI">
|
| 190 |
+
<img src="https://img.youtube.com/vi/CkVZZUSTXAI/mqdefault.jpg" alt="Claude CLI Free Setup" width="300"/>
|
| 191 |
+
</a><br/>
|
| 192 |
+
<b>🇮🇩 Indonesia</b><br/>
|
| 193 |
+
<sub>Koding 24 Jam Anti Rate Limit! Hemat Token AI 65% | Tutorial Quick Setup 9Router 🚀<br/>by <a href="https://www.youtube.com/@krisswuh">Krisswuh</a></sub>
|
| 194 |
+
</td>
|
| 195 |
+
<td align="center" width="320">
|
| 196 |
+
<a href="https://www.youtube.com/watch?v=TXGv4eofe1I">
|
| 197 |
+
<img src="https://img.youtube.com/vi/TXGv4eofe1I/mqdefault.jpg" alt="Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB" width="300"/>
|
| 198 |
+
</a><br/>
|
| 199 |
+
<b>🇮🇩 Indonesia</b><br/>
|
| 200 |
+
<sub>Cara Deploy 9Router di Hugging Face GRATIS Non-Stop! | Alternatif VPS RAM 16GB<br/>by <a href="https://www.youtube.com/@krisswuh">Krisswuh</a></sub>
|
| 201 |
+
</td>
|
| 202 |
+
</tr>
|
| 203 |
+
<tr>
|
| 204 |
+
<td align="center" width="320">
|
| 205 |
+
<a href="https://www.youtube.com/watch?v=GyX-DLvePW8">
|
| 206 |
+
<img src="https://img.youtube.com/vi/GyX-DLvePW8/hqdefault.jpg" alt="این شکلی از هر API ای استفاده کن برای هوش مصنوعی" width="300"/>
|
| 207 |
+
</a><br/>
|
| 208 |
+
<b>🇮�� Persian-فارسی</b><br/>
|
| 209 |
+
<sub dir="rtl">این شکلی از هر API ای استفاده کن برای هوش مصنوعی<br/>by <a href="https://www.youtube.com/@Matin_SenPai">Matin SenPai</a></sub>
|
| 210 |
+
</td>
|
| 211 |
+
<td align="center" width="320">
|
| 212 |
+
<a href="https://www.youtube.com/watch?v=hPusYX-5Pmw">
|
| 213 |
+
<img src="https://img.youtube.com/vi/hPusYX-5Pmw/maxresdefault.jpg" alt="Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z" width="300"/>
|
| 214 |
+
</a><br/>
|
| 215 |
+
<b>🇻🇳 Tiếng Việt</b><br/>
|
| 216 |
+
<sub>Hướng Dẫn Setup OpenClaw + 9Router: Tạo Bot Zalo AI Tự Động Từ A-Z<br/>by <a href="https://github.com/tuanminhhole">tuanminhhole</a></sub>
|
| 217 |
+
</td>
|
| 218 |
+
<td align="center" width="320"></td>
|
| 219 |
+
<td align="center" width="320"></td>
|
| 220 |
+
<td align="center" width="320"></td>
|
| 221 |
+
</tr>
|
| 222 |
+
</table>
|
| 223 |
+
|
| 224 |
+
</div>
|
| 225 |
+
|
| 226 |
+
> 🎬 **Made a video about 9Router?** Submit a [Pull Request](https://github.com/decolua/9router/pulls) adding your video to this section — we'll merge it!
|
| 227 |
+
|
| 228 |
+
---
|
| 229 |
+
|
| 230 |
+
## 🛠️ Supported CLI Tools
|
| 231 |
+
|
| 232 |
+
9Router works seamlessly with all major AI coding tools:
|
| 233 |
+
|
| 234 |
+
<div align="center">
|
| 235 |
+
<table>
|
| 236 |
+
<tr>
|
| 237 |
+
<td align="center" width="120">
|
| 238 |
+
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
|
| 239 |
+
<b>Claude-Code</b>
|
| 240 |
+
</td>
|
| 241 |
+
<td align="center" width="120">
|
| 242 |
+
<img src="./public/providers/openclaw.png" width="60" alt="OpenClaw"/><br/>
|
| 243 |
+
<b>OpenClaw</b>
|
| 244 |
+
</td>
|
| 245 |
+
<td align="center" width="120">
|
| 246 |
+
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
|
| 247 |
+
<b>Codex</b>
|
| 248 |
+
</td>
|
| 249 |
+
<td align="center" width="120">
|
| 250 |
+
<img src="./public/providers/opencode.png" width="60" alt="OpenCode"/><br/>
|
| 251 |
+
<b>OpenCode</b>
|
| 252 |
+
</td>
|
| 253 |
+
<td align="center" width="120">
|
| 254 |
+
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
|
| 255 |
+
<b>Cursor</b>
|
| 256 |
+
</td>
|
| 257 |
+
<td align="center" width="120">
|
| 258 |
+
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
|
| 259 |
+
<b>Antigravity</b>
|
| 260 |
+
</td>
|
| 261 |
+
</tr>
|
| 262 |
+
<tr>
|
| 263 |
+
<td align="center" width="120">
|
| 264 |
+
<img src="./public/providers/cline.png" width="60" alt="Cline"/><br/>
|
| 265 |
+
<b>Cline</b>
|
| 266 |
+
</td>
|
| 267 |
+
<td align="center" width="120">
|
| 268 |
+
<img src="./public/providers/continue.png" width="60" alt="Continue"/><br/>
|
| 269 |
+
<b>Continue</b>
|
| 270 |
+
</td>
|
| 271 |
+
<td align="center" width="120">
|
| 272 |
+
<img src="./public/providers/droid.png" width="60" alt="Droid"/><br/>
|
| 273 |
+
<b>Droid</b>
|
| 274 |
+
</td>
|
| 275 |
+
<td align="center" width="120">
|
| 276 |
+
<img src="./public/providers/roo.png" width="60" alt="Roo"/><br/>
|
| 277 |
+
<b>Roo</b>
|
| 278 |
+
</td>
|
| 279 |
+
<td align="center" width="120">
|
| 280 |
+
<img src="./public/providers/copilot.png" width="60" alt="Copilot"/><br/>
|
| 281 |
+
<b>Copilot</b>
|
| 282 |
+
</td>
|
| 283 |
+
<td align="center" width="120">
|
| 284 |
+
<img src="./public/providers/kilocode.png" width="60" alt="Kilo Code"/><br/>
|
| 285 |
+
<b>Kilo Code</b>
|
| 286 |
+
</td>
|
| 287 |
+
</tr>
|
| 288 |
+
<tr>
|
| 289 |
+
<td align="center" width="120">
|
| 290 |
+
<img src="./public/providers/opendesign.png" width="60" alt="OpenDesign"/><br/>
|
| 291 |
+
<b>OpenDesign</b>
|
| 292 |
+
</td>
|
| 293 |
+
<td align="center" width="120">
|
| 294 |
+
<img src="./public/providers/jcode.png" width="60" alt="jcode"/><br/>
|
| 295 |
+
<b>jcode</b>
|
| 296 |
+
</td>
|
| 297 |
+
<td align="center" width="120">
|
| 298 |
+
<img src="./public/providers/grok-cli.png" width="60" alt="Grok Build"/><br/>
|
| 299 |
+
<b>Grok Build</b>
|
| 300 |
+
</td>
|
| 301 |
+
<td align="center" width="120">
|
| 302 |
+
<img src="./public/providers/devin-cli.png" width="60" alt="Devin CLI"/><br/>
|
| 303 |
+
<b>Devin CLI</b>
|
| 304 |
+
</td>
|
| 305 |
+
<td align="center" width="120">
|
| 306 |
+
<img src="./public/providers/deepseek-tui.png" width="60" alt="DeepSeek TUI"/><br/>
|
| 307 |
+
<b>DeepSeek TUI</b>
|
| 308 |
+
</td>
|
| 309 |
+
<td align="center" width="120">
|
| 310 |
+
<img src="./public/providers/qwen.png" width="60" alt="Qwen Code"/><br/>
|
| 311 |
+
<b>Qwen Code</b>
|
| 312 |
+
</td>
|
| 313 |
+
</tr>
|
| 314 |
+
</table>
|
| 315 |
+
</div>
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## 🌐 Supported Providers
|
| 320 |
+
|
| 321 |
+
### 🔐 OAuth Providers
|
| 322 |
+
|
| 323 |
+
<div align="center">
|
| 324 |
+
<table>
|
| 325 |
+
<tr>
|
| 326 |
+
<td align="center" width="120">
|
| 327 |
+
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
|
| 328 |
+
<b>Claude-Code</b>
|
| 329 |
+
</td>
|
| 330 |
+
<td align="center" width="120">
|
| 331 |
+
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
|
| 332 |
+
<b>Antigravity</b>
|
| 333 |
+
</td>
|
| 334 |
+
<td align="center" width="120">
|
| 335 |
+
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
|
| 336 |
+
<b>Codex</b>
|
| 337 |
+
</td>
|
| 338 |
+
<td align="center" width="120">
|
| 339 |
+
<img src="./public/providers/github.png" width="60" alt="GitHub"/><br/>
|
| 340 |
+
<b>GitHub</b>
|
| 341 |
+
</td>
|
| 342 |
+
<td align="center" width="120">
|
| 343 |
+
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
|
| 344 |
+
<b>Cursor</b>
|
| 345 |
+
</td>
|
| 346 |
+
<td align="center" width="120">
|
| 347 |
+
<img src="./public/providers/kimchi.png" width="60" alt="Kimchi"/><br/>
|
| 348 |
+
<b>Kimchi</b>
|
| 349 |
+
</td>
|
| 350 |
+
</tr>
|
| 351 |
+
</table>
|
| 352 |
+
</div>
|
| 353 |
+
|
| 354 |
+
### 🆓 Free Providers
|
| 355 |
+
|
| 356 |
+
<div align="center">
|
| 357 |
+
<table>
|
| 358 |
+
<tr>
|
| 359 |
+
<td align="center" width="150">
|
| 360 |
+
<img src="./public/providers/kiro.png" width="70" alt="Kiro"/><br/>
|
| 361 |
+
<b>Kiro AI</b><br/>
|
| 362 |
+
<sub>Claude 4.5 + GLM-5 + MiniMax<br/>50 credits/month free</sub>
|
| 363 |
+
</td>
|
| 364 |
+
<td align="center" width="150">
|
| 365 |
+
<img src="./public/providers/opencode.png" width="70" alt="OpenCode Free"/><br/>
|
| 366 |
+
<b>OpenCode Free</b><br/>
|
| 367 |
+
<sub>No auth • Auto-fetch models<br/>Free (model list varies)</sub>
|
| 368 |
+
</td>
|
| 369 |
+
<td align="center" width="150">
|
| 370 |
+
<img src="./public/providers/gemini.png" width="70" alt="Vertex AI"/><br/>
|
| 371 |
+
<b>Vertex AI</b><br/>
|
| 372 |
+
<sub>Gemini 3 Pro + GLM-5 + DeepSeek<br/>$300 credits free</sub>
|
| 373 |
+
</td>
|
| 374 |
+
</tr>
|
| 375 |
+
</table>
|
| 376 |
+
</div>
|
| 377 |
+
|
| 378 |
+
> **Note:** iFlow, Qwen Code and Gemini CLI free tiers were discontinued in 2026. Use Kiro / OpenCode Free / Vertex instead.
|
| 379 |
+
>
|
| 380 |
+
> **Kiro AI** moved to a paid model in Sep 2025 — the free tier is now capped at **50 credits/month** (plus 500 trial credits for new accounts in the first 30 days). Paid tiers: Pro $20/mo (1,000 credits), Pro+ $40/mo (2,000), Pro Max $100/mo (5,000), Power $200/mo (10,000).
|
| 381 |
+
> **OpenCode Free** model list fluctuates over time (some models free only for limited promos) — subject to change without notice.
|
| 382 |
+
> **Vertex AI**: the $300 free credit for new GCP accounts is still valid, but since Mar 2026 the **Gemini API endpoint no longer consumes these credits** — call the **Vertex AI Studio** endpoint instead.
|
| 383 |
+
|
| 384 |
+
### 🔑 API Key Providers (40+)
|
| 385 |
+
|
| 386 |
+
<div align="center">
|
| 387 |
+
<table>
|
| 388 |
+
<tr>
|
| 389 |
+
<td align="center" width="100">
|
| 390 |
+
<img src="./public/providers/openrouter.png" width="50" alt="OpenRouter"/><br/>
|
| 391 |
+
<sub>OpenRouter</sub>
|
| 392 |
+
</td>
|
| 393 |
+
<td align="center" width="100">
|
| 394 |
+
<img src="./public/providers/glm.png" width="50" alt="GLM"/><br/>
|
| 395 |
+
<sub>GLM</sub>
|
| 396 |
+
</td>
|
| 397 |
+
<td align="center" width="100">
|
| 398 |
+
<img src="./public/providers/kimi.png" width="50" alt="Kimi"/><br/>
|
| 399 |
+
<sub>Kimi</sub>
|
| 400 |
+
</td>
|
| 401 |
+
<td align="center" width="100">
|
| 402 |
+
<img src="./public/providers/minimax.png" width="50" alt="MiniMax"/><br/>
|
| 403 |
+
<sub>MiniMax</sub>
|
| 404 |
+
</td>
|
| 405 |
+
<td align="center" width="100">
|
| 406 |
+
<img src="./public/providers/openai.png" width="50" alt="OpenAI"/><br/>
|
| 407 |
+
<sub>OpenAI</sub>
|
| 408 |
+
</td>
|
| 409 |
+
<td align="center" width="100">
|
| 410 |
+
<img src="./public/providers/anthropic.png" width="50" alt="Anthropic"/><br/>
|
| 411 |
+
<sub>Anthropic</sub>
|
| 412 |
+
</td>
|
| 413 |
+
</tr>
|
| 414 |
+
<tr>
|
| 415 |
+
<td align="center" width="100">
|
| 416 |
+
<img src="./public/providers/gemini.png" width="50" alt="Gemini"/><br/>
|
| 417 |
+
<sub>Gemini</sub>
|
| 418 |
+
</td>
|
| 419 |
+
<td align="center" width="100">
|
| 420 |
+
<img src="./public/providers/deepseek.png" width="50" alt="DeepSeek"/><br/>
|
| 421 |
+
<sub>DeepSeek</sub>
|
| 422 |
+
</td>
|
| 423 |
+
<td align="center" width="100">
|
| 424 |
+
<img src="./public/providers/groq.png" width="50" alt="Groq"/><br/>
|
| 425 |
+
<sub>Groq</sub>
|
| 426 |
+
</td>
|
| 427 |
+
<td align="center" width="100">
|
| 428 |
+
<img src="./public/providers/xai.png" width="50" alt="xAI"/><br/>
|
| 429 |
+
<sub>xAI</sub>
|
| 430 |
+
</td>
|
| 431 |
+
<td align="center" width="100">
|
| 432 |
+
<img src="./public/providers/mistral.png" width="50" alt="Mistral"/><br/>
|
| 433 |
+
<sub>Mistral</sub>
|
| 434 |
+
</td>
|
| 435 |
+
<td align="center" width="100">
|
| 436 |
+
<img src="./public/providers/perplexity.png" width="50" alt="Perplexity"/><br/>
|
| 437 |
+
<sub>Perplexity</sub>
|
| 438 |
+
</td>
|
| 439 |
+
</tr>
|
| 440 |
+
<tr>
|
| 441 |
+
<td align="center" width="100">
|
| 442 |
+
<img src="./public/providers/together.png" width="50" alt="Together"/><br/>
|
| 443 |
+
<sub>Together AI</sub>
|
| 444 |
+
</td>
|
| 445 |
+
<td align="center" width="100">
|
| 446 |
+
<img src="./public/providers/fireworks.png" width="50" alt="Fireworks"/><br/>
|
| 447 |
+
<sub>Fireworks</sub>
|
| 448 |
+
</td>
|
| 449 |
+
<td align="center" width="100">
|
| 450 |
+
<img src="./public/providers/cerebras.png" width="50" alt="Cerebras"/><br/>
|
| 451 |
+
<sub>Cerebras</sub>
|
| 452 |
+
</td>
|
| 453 |
+
<td align="center" width="100">
|
| 454 |
+
<img src="./public/providers/cohere.png" width="50" alt="Cohere"/><br/>
|
| 455 |
+
<sub>Cohere</sub>
|
| 456 |
+
</td>
|
| 457 |
+
<td align="center" width="100">
|
| 458 |
+
<img src="./public/providers/nvidia.png" width="50" alt="NVIDIA"/><br/>
|
| 459 |
+
<sub>NVIDIA</sub>
|
| 460 |
+
</td>
|
| 461 |
+
<td align="center" width="100">
|
| 462 |
+
<img src="./public/providers/siliconflow.png" width="50" alt="SiliconFlow"/><br/>
|
| 463 |
+
<sub>SiliconFlow</sub>
|
| 464 |
+
</td>
|
| 465 |
+
</tr>
|
| 466 |
+
</table>
|
| 467 |
+
<p><i>...and 20+ more providers including Nebius, Chutes, Hyperbolic, and custom OpenAI/Anthropic compatible endpoints</i></p>
|
| 468 |
+
</div>
|
| 469 |
+
|
| 470 |
+
### 🏠 Self-hosted Providers
|
| 471 |
+
|
| 472 |
+
For speech and embeddings served from **your own** machine — whisper.cpp,
|
| 473 |
+
faster-whisper, Speaches, Kokoro-FastAPI, openedai-speech, llama.cpp/llama-server,
|
| 474 |
+
vLLM, Infinity, text-embeddings-inference, or anything else that speaks the OpenAI
|
| 475 |
+
shape.
|
| 476 |
+
|
| 477 |
+
| Provider | Endpoint used | Typical server |
|
| 478 |
+
| --- | --- | --- |
|
| 479 |
+
| **Self-hosted STT** | `/v1/audio/transcriptions` | whisper.cpp, faster-whisper |
|
| 480 |
+
| **Self-hosted TTS** | `/v1/audio/speech` | Kokoro-FastAPI, openedai-speech |
|
| 481 |
+
| **Self-hosted Embedding** | `/v1/embeddings` | llama-server, vLLM, Infinity |
|
| 482 |
+
|
| 483 |
+
Every other speech provider is a named cloud service with a fixed endpoint. These
|
| 484 |
+
three read their address from **each connection**, so one provider can front
|
| 485 |
+
several machines and load-balance across them like any other.
|
| 486 |
+
|
| 487 |
+
Set it on the connection as `providerSpecificData.baseUrl`:
|
| 488 |
+
|
| 489 |
+
| Provider | Give it | Result |
|
| 490 |
+
| --- | --- | --- |
|
| 491 |
+
| Self-hosted STT | the full URL — `http://host:8080/v1/audio/transcriptions` | used as-is |
|
| 492 |
+
| Self-hosted TTS | the server root — `http://host:8880` | `+ /v1/audio/speech` |
|
| 493 |
+
| Self-hosted Embedding | the **OpenAI base**, `/v1` included — `http://host:8080/v1` | `+ /embeddings` |
|
| 494 |
+
|
| 495 |
+
> **Mind the `/v1` on embeddings.** The adapter appends `/embeddings`, so
|
| 496 |
+
> `http://host:8080` resolves to `http://host:8080/embeddings` and misses the
|
| 497 |
+
> OpenAI route — llama-server answers **501**. Give it the same base URL an OpenAI
|
| 498 |
+
> client would use. A full `.../v1/embeddings` is also accepted, so a value pasted
|
| 499 |
+
> from a `curl` example works too.
|
| 500 |
+
|
| 501 |
+
The API key is not checked by most local servers, but the field must be non-empty:
|
| 502 |
+
it is what gives the connection a credentials record, and `baseUrl` lives there.
|
| 503 |
+
Any placeholder works.
|
| 504 |
+
|
| 505 |
+
Self-hosted Embedding has **no cloud fallback by design** — a connection saved
|
| 506 |
+
without a `baseUrl` is reported as a configuration error rather than quietly
|
| 507 |
+
falling back to `api.openai.com`, which would send your input text and API key to
|
| 508 |
+
a third party under a provider named "Self-hosted".
|
| 509 |
+
|
| 510 |
+
---
|
| 511 |
+
|
| 512 |
+
## 💡 Key Features
|
| 513 |
+
|
| 514 |
+
| Feature | What It Does | Why It Matters |
|
| 515 |
+
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------- |
|
| 516 |
+
| 🚀 **RTK Token Saver** ([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | Compress tool outputs (`git diff`, `grep`, `ls`, `tree`...) before sending to LLM | Save **20-40% input tokens** per request |
|
| 517 |
+
| 🧠 **Headroom Token Saver** ([Headroom](https://github.com/chopratejas/headroom)) | Optional external `/v1/compress` proxy before provider routing | Save more context tokens without changing clients |
|
| 518 |
+
| 🪨 **Caveman Mode** ([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | Inject caveman-speak prompt → LLM replies terse, technical substance preserved | Save **up to 65% output tokens** |
|
| 519 |
+
| 🐴 **Ponytail** ([Ponytail](https://github.com/DietrichGebert/ponytail)) | Inject "lazy senior dev" prompt → LLM writes minimal, YAGNI-first code (Lite/Full/Ultra) | **Fewer output tokens, less refactoring** |
|
| 520 |
+
| 🎯 **Smart 3-Tier Fallback** | Auto-route: Subscription → Cheap → Free | Never stop coding, zero downtime |
|
| 521 |
+
| 📊 **Real-Time Quota Tracking** | Live token count + reset countdown | Maximize subscription value |
|
| 522 |
+
| 🔄 **Format Translation** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | Works with any CLI tool |
|
| 523 |
+
| 👥 **Multi-Account Support** | Multiple accounts per provider | Load balancing + redundancy |
|
| 524 |
+
| 🔄 **Auto Token Refresh** | OAuth tokens refresh automatically | No manual re-login needed |
|
| 525 |
+
| 🎨 **Custom Combos** | Create unlimited model combinations | Tailor fallback to your needs |
|
| 526 |
+
| 📝 **Request Logging** | Debug mode with full request/response logs | Troubleshoot issues easily |
|
| 527 |
+
| 💾 **Cloud Sync** | Sync config across devices | Same setup everywhere |
|
| 528 |
+
| 📊 **Usage Analytics** | Track tokens, cost, trends over time | Optimize spending |
|
| 529 |
+
| 🌐 **Deploy Anywhere** | Localhost, VPS, Docker, Cloudflare Workers | Flexible deployment options |
|
| 530 |
+
|
| 531 |
+
Set `X-9Router-Token-Saver: off` to bypass all token savers for one chat request.
|
| 532 |
+
|
| 533 |
+
<details>
|
| 534 |
+
<summary><b>📖 Feature Details</b></summary>
|
| 535 |
+
|
| 536 |
+
### 🚀 RTK Token Saver
|
| 537 |
+
|
| 538 |
+
Tool outputs (`git diff`, `grep`, `find`, `ls`, `tree`, log dumps...) often eat 30-50% of your prompt budget. RTK detects them and applies smart, lossless compression **before** the request hits the LLM:
|
| 539 |
+
|
| 540 |
+
- **Filters:** `git-diff`, `git-status`, `grep`, `find`, `ls`, `tree`, `dedup-log`, `smart-truncate`, `read-numbered`, `search-list`
|
| 541 |
+
- **Auto-detect:** No config needed — RTK peeks the first 1KB of each `tool_result` and picks the right filter.
|
| 542 |
+
- **Safe by design:** If a filter fails, throws, or makes output bigger, RTK silently keeps the original text. Errors never break your request.
|
| 543 |
+
- **Universal:** Works across all formats (OpenAI, Claude, Gemini, Cursor, Kiro, OpenAI Responses) because it runs **before** any format translation.
|
| 544 |
+
- **Default ON:** Toggle anytime in Dashboard → Endpoint settings.
|
| 545 |
+
|
| 546 |
+
```
|
| 547 |
+
Without RTK: 47K tokens sent to LLM
|
| 548 |
+
With RTK: 28K tokens sent to LLM (40% saved · same context · same answer)
|
| 549 |
+
```
|
| 550 |
+
|
| 551 |
+
### 🧠 Headroom Token Saver
|
| 552 |
+
|
| 553 |
+
Headroom is optional and runs separately. 9Router calls Headroom's local `/v1/compress` endpoint, then keeps normal routing, fallback, auth, and usage tracking:
|
| 554 |
+
|
| 555 |
+
```
|
| 556 |
+
Client → 9Router → Headroom /v1/compress → 9Router → provider
|
| 557 |
+
```
|
| 558 |
+
|
| 559 |
+
Local setup:
|
| 560 |
+
|
| 561 |
+
```bash
|
| 562 |
+
pip install "headroom-ai[proxy]"
|
| 563 |
+
headroom proxy --port 8787
|
| 564 |
+
```
|
| 565 |
+
|
| 566 |
+
Enable in Dashboard → Endpoint → Token Saver → Headroom. Default URL: `http://localhost:8787`.
|
| 567 |
+
|
| 568 |
+
Docker examples:
|
| 569 |
+
|
| 570 |
+
```bash
|
| 571 |
+
# Headroom service in same Docker network
|
| 572 |
+
http://headroom:8787
|
| 573 |
+
|
| 574 |
+
# Headroom running on host machine
|
| 575 |
+
http://host.docker.internal:8787
|
| 576 |
+
```
|
| 577 |
+
|
| 578 |
+
If Headroom is down or returns an error, 9Router fails open and sends the original request.
|
| 579 |
+
|
| 580 |
+
### 🐴 Ponytail (Lazy Senior Dev)
|
| 581 |
+
|
| 582 |
+
Ponytail injects a _"lazy senior dev"_ system prompt into every request, biasing the LLM toward minimal, YAGNI-first code — deletion over addition, stdlib over new deps, one-liners over abstractions. Adapted from [DietrichGebert/ponytail](https://github.com/DietrichGebert/ponytail).
|
| 583 |
+
|
| 584 |
+
- **Lite** — Build what's asked, name the lazier alternative.
|
| 585 |
+
- **Full** — YAGNI ladder enforced: stdlib → native → existing deps → one-liner → minimal code.
|
| 586 |
+
- **Ultra** — YAGNI extremist: deletion first, ship the one-liner, challenge the rest of the requirement in the same response.
|
| 587 |
+
|
| 588 |
+
```
|
| 589 |
+
Without Ponytail: verbose code, extra abstractions, "just in case" scaffolding
|
| 590 |
+
With Ponytail: shortest working diff, no unrequested abstractions, fewer tokens
|
| 591 |
+
```
|
| 592 |
+
|
| 593 |
+
Never trades away: input validation, error handling that prevents data loss, security, accessibility, or anything explicitly requested. Enable in Dashboard → Endpoint → Ponytail. Stacks with Caveman (output terseness) and RTK (input compression).
|
| 594 |
+
|
| 595 |
+
### 🎯 Smart 3-Tier Fallback
|
| 596 |
+
|
| 597 |
+
Create combos with automatic fallback:
|
| 598 |
+
|
| 599 |
+
```
|
| 600 |
+
Combo: "my-coding-stack"
|
| 601 |
+
1. cc/claude-opus-4-6 (your subscription)
|
| 602 |
+
2. glm/glm-4.7 (cheap backup, $0.6/1M)
|
| 603 |
+
3. if/kimi-k2-thinking (free fallback)
|
| 604 |
+
|
| 605 |
+
→ Auto switches when quota runs out or errors occur
|
| 606 |
+
```
|
| 607 |
+
|
| 608 |
+
### 📊 Real-Time Quota Tracking
|
| 609 |
+
|
| 610 |
+
- Token consumption per provider
|
| 611 |
+
- Reset countdown (5-hour, daily, weekly)
|
| 612 |
+
- Cost estimation for paid tiers
|
| 613 |
+
- Monthly spending reports
|
| 614 |
+
|
| 615 |
+
### 🔄 Format Translation
|
| 616 |
+
|
| 617 |
+
Seamless translation between formats:
|
| 618 |
+
|
| 619 |
+
- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses**
|
| 620 |
+
- Your CLI tool sends OpenAI format → 9Router translates → Provider receives native format
|
| 621 |
+
- Works with any tool that supports custom OpenAI endpoints
|
| 622 |
+
|
| 623 |
+
### 👥 Multi-Account Support
|
| 624 |
+
|
| 625 |
+
- Add multiple accounts per provider
|
| 626 |
+
- Auto round-robin or priority-based routing
|
| 627 |
+
- Fallback to next account when one hits quota
|
| 628 |
+
|
| 629 |
+
### 🔄 Auto Token Refresh
|
| 630 |
+
|
| 631 |
+
- OAuth tokens automatically refresh before expiration
|
| 632 |
+
- No manual re-authentication needed
|
| 633 |
+
- Seamless experience across all providers
|
| 634 |
+
|
| 635 |
+
### 🎨 Custom Combos
|
| 636 |
+
|
| 637 |
+
- Create unlimited model combinations
|
| 638 |
+
- Mix subscription, cheap, and free tiers
|
| 639 |
+
- Name your combos for easy access
|
| 640 |
+
- Share combos across devices with Cloud Sync
|
| 641 |
+
|
| 642 |
+
### 📝 Request Logging
|
| 643 |
+
|
| 644 |
+
- Enable debug mode for full request/response logs
|
| 645 |
+
- Track API calls, headers, and payloads
|
| 646 |
+
- Troubleshoot integration issues
|
| 647 |
+
- Export logs for analysis
|
| 648 |
+
|
| 649 |
+
### 💾 Cloud Sync
|
| 650 |
+
|
| 651 |
+
- Sync providers, combos, and settings across devices
|
| 652 |
+
- Automatic background sync
|
| 653 |
+
- Secure encrypted storage
|
| 654 |
+
- Access your setup from anywhere
|
| 655 |
+
|
| 656 |
+
#### Cloud Runtime Notes
|
| 657 |
+
|
| 658 |
+
- Prefer server-side cloud variables in production:
|
| 659 |
+
- `BASE_URL` (internal callback URL used by sync scheduler)
|
| 660 |
+
- `CLOUD_URL` (cloud sync endpoint base)
|
| 661 |
+
- `NEXT_PUBLIC_BASE_URL` and `NEXT_PUBLIC_CLOUD_URL` are still supported for compatibility/UI, but server runtime now prioritizes `BASE_URL`/`CLOUD_URL`.
|
| 662 |
+
- Cloud sync requests now use timeout + fail-fast behavior to avoid UI hanging when cloud DNS/network is unavailable.
|
| 663 |
+
|
| 664 |
+
### 📊 Usage Analytics
|
| 665 |
+
|
| 666 |
+
- Track token usage per provider and model
|
| 667 |
+
- Cost estimation and spending trends
|
| 668 |
+
- Monthly reports and insights
|
| 669 |
+
- Optimize your AI spending
|
| 670 |
+
|
| 671 |
+
> **💡 IMPORTANT - Understanding Dashboard Costs:**
|
| 672 |
+
>
|
| 673 |
+
> The "cost" displayed in Usage Analytics is **for tracking and comparison purposes only**.
|
| 674 |
+
> 9Router itself **never charges** you anything. You only pay providers directly (if using paid services).
|
| 675 |
+
>
|
| 676 |
+
> **Example:** If your dashboard shows "$290 total cost" while using Kiro free models, this represents
|
| 677 |
+
> what you would have paid using paid APIs directly. Your actual cost = **$0** (Kiro free tier: ~50 credits/mo).
|
| 678 |
+
>
|
| 679 |
+
> Think of it as a "savings tracker" showing how much you're saving by using free models or
|
| 680 |
+
> routing through 9Router!
|
| 681 |
+
|
| 682 |
+
### 🌐 Deploy Anywhere
|
| 683 |
+
|
| 684 |
+
- 💻 **Localhost** - Default, works offline
|
| 685 |
+
- ☁️ **VPS/Cloud** - Share across devices
|
| 686 |
+
- 🐳 **Docker** - One-command deployment
|
| 687 |
+
- 🚀 **Cloudflare Workers** - Global edge network
|
| 688 |
+
|
| 689 |
+
</details>
|
| 690 |
+
|
| 691 |
+
---
|
| 692 |
+
|
| 693 |
+
## 💰 Pricing at a Glance
|
| 694 |
+
|
| 695 |
+
| Tier | Provider | Cost | Quota Reset | Best For |
|
| 696 |
+
| ------------------- | --------------------- | ------------ | ---------------- | --------------------------------------- |
|
| 697 |
+
| **🚀 TOKEN SAVER** | **RTK (built-in)** | **FREE** | Always on | **Save 20-40% tokens on EVERY request** |
|
| 698 |
+
| **💳 SUBSCRIPTION** | Claude Code (Pro/Max) | $20-200/mo | 5h + weekly | Already subscribed |
|
| 699 |
+
| | Codex (Plus/Pro) | $20-200/mo | 5h + weekly | OpenAI users |
|
| 700 |
+
| | GitHub Copilot | $10-19/mo | Monthly | GitHub users |
|
| 701 |
+
| | Cursor IDE | $20/mo | Monthly | Cursor users |
|
| 702 |
+
| **💰 CHEAP** | GLM-5.1 / GLM-4.7 | $0.6/1M | Daily 10AM | Budget backup |
|
| 703 |
+
| | MiniMax M2.7 | $0.2/1M | 5-hour rolling | Cheapest option |
|
| 704 |
+
| | Kimi K2.5 | $9/mo flat | 10M tokens/mo | Predictable cost |
|
| 705 |
+
| **🆓 FREE** | Kiro AI | $0 | 50 credits/mo | Claude 4.5 + GLM-5 + MiniMax free (paid tiers above) |
|
| 706 |
+
| | OpenCode Free | $0 | Varies* | No auth, auto-fetch models (list changes over time) |
|
| 707 |
+
| | Vertex AI | $300 credits | New GCP accounts | Gemini 3 Pro + DeepSeek + GLM-5 (use Vertex AI Studio endpoint for free credits) |
|
| 708 |
+
|
| 709 |
+
**💡 Pro Tip:** RTK + Kiro AI + OpenCode Free combo = **$0 cost + 20-40% token savings**!
|
| 710 |
+
|
| 711 |
+
---
|
| 712 |
+
|
| 713 |
+
### 📊 Understanding 9Router Costs & Billing
|
| 714 |
+
|
| 715 |
+
**9Router Billing Reality:**
|
| 716 |
+
|
| 717 |
+
✅ **9Router software = FREE forever** (open source, never charges)
|
| 718 |
+
✅ **Dashboard "costs" = Display/tracking only** (not actual bills)
|
| 719 |
+
✅ **You pay providers directly** (subscriptions or API fees)
|
| 720 |
+
✅ **FREE providers stay FREE** (Kiro ~50 credits/mo, OpenCode Free, Vertex $300 credits = $0 within free-tier limits) — note iFlow/Qwen/Gemini CLI free tiers were discontinued in 2026
|
| 721 |
+
❌ **9Router never sends invoices** or charges your card
|
| 722 |
+
|
| 723 |
+
**How Cost Display Works:**
|
| 724 |
+
|
| 725 |
+
The dashboard shows **estimated costs** as if you were using paid APIs directly. This is **not billing** - it's a comparison tool to show your savings.
|
| 726 |
+
|
| 727 |
+
**Example Scenario:**
|
| 728 |
+
|
| 729 |
+
```
|
| 730 |
+
Dashboard Display:
|
| 731 |
+
• Total Requests: 1,662
|
| 732 |
+
• Total Tokens: 47M
|
| 733 |
+
• Display Cost: $290
|
| 734 |
+
|
| 735 |
+
Reality Check:
|
| 736 |
+
• Provider: Kiro (free tier: ~50 credits/mo)
|
| 737 |
+
• Actual Payment: $0.00
|
| 738 |
+
• What $290 Means: Amount you SAVED by using free models!
|
| 739 |
+
```
|
| 740 |
+
|
| 741 |
+
**Payment Rules:**
|
| 742 |
+
|
| 743 |
+
- **Subscription providers** (Claude Code, Codex): Pay them directly via their websites
|
| 744 |
+
- **Cheap providers** (GLM, MiniMax): Pay them directly, 9Router just routes
|
| 745 |
+
- **FREE providers** (iFlow, Kiro, Qwen): Genuinely free forever, no hidden charges
|
| 746 |
+
- **9Router**: Never charges anything, ever
|
| 747 |
+
|
| 748 |
+
---
|
| 749 |
+
|
| 750 |
+
## 🎯 Use Cases
|
| 751 |
+
|
| 752 |
+
### Case 1: "I have Claude Pro subscription"
|
| 753 |
+
|
| 754 |
+
**Problem:** Quota expires unused, rate limits during heavy coding
|
| 755 |
+
|
| 756 |
+
**Solution:**
|
| 757 |
+
|
| 758 |
+
```
|
| 759 |
+
Combo: "maximize-claude"
|
| 760 |
+
1. cc/claude-opus-4-7 (use subscription fully)
|
| 761 |
+
2. glm/glm-5.1 (cheap backup when quota out)
|
| 762 |
+
3. kr/claude-sonnet-4.5 (free emergency fallback)
|
| 763 |
+
|
| 764 |
+
Monthly cost: $20 (subscription) + ~$5 (backup) = $25 total
|
| 765 |
+
vs. $20 + hitting limits = frustration
|
| 766 |
+
```
|
| 767 |
+
|
| 768 |
+
### Case 2: "I want zero cost"
|
| 769 |
+
|
| 770 |
+
**Problem:** Can't afford subscriptions, need reliable AI coding
|
| 771 |
+
|
| 772 |
+
**Solution:**
|
| 773 |
+
|
| 774 |
+
```
|
| 775 |
+
Combo: "free-forever"
|
| 776 |
+
1. kr/claude-sonnet-4.5 (Claude 4.5 free via Kiro, ~50 credits/mo)
|
| 777 |
+
2. kr/glm-5 (GLM-5 free via Kiro)
|
| 778 |
+
3. oc/<auto> (OpenCode Free, no auth)
|
| 779 |
+
|
| 780 |
+
Monthly cost: $0
|
| 781 |
+
Quality: Production-ready models + RTK saves 20-40% tokens
|
| 782 |
+
```
|
| 783 |
+
|
| 784 |
+
### Case 3: "I need 24/7 coding, no interruptions"
|
| 785 |
+
|
| 786 |
+
**Problem:** Deadlines, can't afford downtime
|
| 787 |
+
|
| 788 |
+
**Solution:**
|
| 789 |
+
|
| 790 |
+
```
|
| 791 |
+
Combo: "always-on"
|
| 792 |
+
1. cc/claude-opus-4-7 (best quality)
|
| 793 |
+
2. cx/gpt-5.5 (second subscription)
|
| 794 |
+
3. glm/glm-5.1 (cheap, resets daily)
|
| 795 |
+
4. minimax/MiniMax-M2.7 (cheapest, 5h reset)
|
| 796 |
+
5. kr/claude-sonnet-4.5 (free via Kiro, ~50 credits/mo)
|
| 797 |
+
|
| 798 |
+
Result: 5 layers of fallback = zero downtime
|
| 799 |
+
Monthly cost: $20-200 (subscriptions) + $10-20 (backup)
|
| 800 |
+
```
|
| 801 |
+
|
| 802 |
+
### Case 4: "I want FREE AI in OpenClaw"
|
| 803 |
+
|
| 804 |
+
**Problem:** Need AI assistant in messaging apps (WhatsApp, Telegram, Slack...), completely free
|
| 805 |
+
|
| 806 |
+
**Solution:**
|
| 807 |
+
|
| 808 |
+
```
|
| 809 |
+
Combo: "openclaw-free"
|
| 810 |
+
1. kr/claude-sonnet-4.5 (Claude 4.5 free)
|
| 811 |
+
2. kr/glm-5 (GLM-5 free)
|
| 812 |
+
3. kr/MiniMax-M2.5 (MiniMax free)
|
| 813 |
+
|
| 814 |
+
Monthly cost: $0
|
| 815 |
+
Access via: WhatsApp, Telegram, Slack, Discord, iMessage, Signal...
|
| 816 |
+
```
|
| 817 |
+
|
| 818 |
+
---
|
| 819 |
+
|
| 820 |
+
## ❓ Frequently Asked Questions
|
| 821 |
+
|
| 822 |
+
<details>
|
| 823 |
+
<summary><b>📊 Why does my dashboard show high costs?</b></summary>
|
| 824 |
+
|
| 825 |
+
The dashboard tracks your token usage and displays **estimated costs** as if you were using paid APIs directly. This is **not actual billing** - it's a reference to show how much you're saving by using free models or existing subscriptions through 9Router.
|
| 826 |
+
|
| 827 |
+
**Example:**
|
| 828 |
+
|
| 829 |
+
- **Dashboard shows:** "$290 total cost"
|
| 830 |
+
- **Reality:** You're using Kiro free models (~50 credits/mo)
|
| 831 |
+
- **Your actual cost:** **$0.00**
|
| 832 |
+
- **What $290 means:** Amount you **saved** by using free models instead of paid APIs!
|
| 833 |
+
|
| 834 |
+
The cost display is a "savings tracker" to help you understand your usage patterns and optimization opportunities.
|
| 835 |
+
|
| 836 |
+
</details>
|
| 837 |
+
|
| 838 |
+
<details>
|
| 839 |
+
<summary><b>💳 Will I be charged by 9Router?</b></summary>
|
| 840 |
+
|
| 841 |
+
**No.** 9Router is free, open-source software that runs on your own computer. It never charges you anything.
|
| 842 |
+
|
| 843 |
+
**You only pay:**
|
| 844 |
+
|
| 845 |
+
- ✅ **Subscription providers** (Claude Code $20/mo, Codex $20-200/mo) → Pay them directly on their websites
|
| 846 |
+
- ✅ **Cheap providers** (GLM, MiniMax) → Pay them directly, 9Router just routes your requests
|
| 847 |
+
- ❌ **9Router itself** → **Never charges anything, ever**
|
| 848 |
+
|
| 849 |
+
9Router is a local proxy/router. It doesn't have your credit card, can't send invoices, and has no billing system. It's completely free software.
|
| 850 |
+
|
| 851 |
+
</details>
|
| 852 |
+
|
| 853 |
+
<details>
|
| 854 |
+
<summary><b>🆓 Are FREE providers really unlimited?</b></summary>
|
| 855 |
+
|
| 856 |
+
**Mostly!** The current FREE providers (Kiro, OpenCode Free, Vertex) are genuinely free, but free tiers have limits:
|
| 857 |
+
|
| 858 |
+
These are free services offered by those respective companies:
|
| 859 |
+
|
| 860 |
+
- **Kiro AI**: ~50 credits/month free (plus 500 trial credits for new accounts in the first 30 days) via AWS Builder ID / Google / GitHub OAuth. Paid tiers available above that.
|
| 861 |
+
- **OpenCode Free**: No-auth passthrough proxy, models auto-fetched from `opencode.ai/zen/v1/models`. The free model list fluctuates over time (some models free only for limited promos) — subject to change without notice.
|
| 862 |
+
- **Vertex AI**: $300 free credits for new Google Cloud accounts (90 days). Since Mar 2026 the Gemini API endpoint no longer consumes these credits — use the **Vertex AI Studio** endpoint instead.
|
| 863 |
+
|
| 864 |
+
9Router just routes your requests to them - there's no "catch" or future billing from 9Router itself. They're truly free services, and 9Router makes them easy to use with fallback support.
|
| 865 |
+
|
| 866 |
+
**Discontinued free tiers (no longer recommended):**
|
| 867 |
+
|
| 868 |
+
- ❌ **iFlow**: Was free unlimited, now changed to paid (2026)
|
| 869 |
+
- ❌ **Qwen Code**: Free OAuth tier fully discontinued by Alibaba on 2026-04-15
|
| 870 |
+
- ❌ **Gemini CLI**: Service fully shut down by Google on 2026-06-18 (replaced by the closed-source Antigravity CLI). Discontinued — do not use.
|
| 871 |
+
|
| 872 |
+
</details>
|
| 873 |
+
|
| 874 |
+
<details>
|
| 875 |
+
<summary><b>💰 How do I minimize my actual AI costs?</b></summary>
|
| 876 |
+
|
| 877 |
+
**Free-First Strategy:**
|
| 878 |
+
|
| 879 |
+
1. **Start with 100% free combo:**
|
| 880 |
+
|
| 881 |
+
```
|
| 882 |
+
1. kr/glm-5 (GLM-5 free via Kiro, ~50 credits/mo)
|
| 883 |
+
2. OpenCode Free models (no auth, auto-fetched)
|
| 884 |
+
3. Vertex AI Gemini 3 Pro (using the Vertex AI Studio endpoint with $300 credits)
|
| 885 |
+
```
|
| 886 |
+
|
| 887 |
+
**Cost: $0/month** (within Kiro's free credit cap; OpenCode/Vertex subject to their free-tier limits)
|
| 888 |
+
|
| 889 |
+
2. **Add cheap backup** only if you need it:
|
| 890 |
+
|
| 891 |
+
```
|
| 892 |
+
4. glm/glm-4.7 ($0.6/1M tokens)
|
| 893 |
+
```
|
| 894 |
+
|
| 895 |
+
**Additional cost: Only pay for what you actually use**
|
| 896 |
+
|
| 897 |
+
3. **Use subscription providers last:**
|
| 898 |
+
- Only if you already have them
|
| 899 |
+
- 9Router helps maximize their value through quota tracking
|
| 900 |
+
|
| 901 |
+
**Result:** Most users can operate at $0/month using only free tiers!
|
| 902 |
+
|
| 903 |
+
</details>
|
| 904 |
+
|
| 905 |
+
<details>
|
| 906 |
+
<summary><b>📈 What if my usage suddenly spikes?</b></summary>
|
| 907 |
+
|
| 908 |
+
9Router's smart fallback prevents surprise charges:
|
| 909 |
+
|
| 910 |
+
**Scenario:** You're on a coding sprint and blow through your quotas
|
| 911 |
+
|
| 912 |
+
**Without 9Router:**
|
| 913 |
+
|
| 914 |
+
- ❌ Hit rate limit → Work stops → Frustration
|
| 915 |
+
- ❌ Or: Accidentally rack up huge API bills
|
| 916 |
+
|
| 917 |
+
**With 9Router:**
|
| 918 |
+
|
| 919 |
+
- ✅ Subscription hits limit → Auto-fallback to cheap tier
|
| 920 |
+
- ✅ Cheap tier gets expensive → Auto-fallback to free tier
|
| 921 |
+
- ✅ Never stop coding → Predictable costs
|
| 922 |
+
|
| 923 |
+
**You're in control:** Set spending limits per provider in dashboard, and 9Router respects them.
|
| 924 |
+
|
| 925 |
+
</details>
|
| 926 |
+
|
| 927 |
+
---
|
| 928 |
+
|
| 929 |
+
## 📖 Setup Guide
|
| 930 |
+
|
| 931 |
+
<details>
|
| 932 |
+
<summary><b>🔐 Subscription Providers (Maximize Value)</b></summary>
|
| 933 |
+
|
| 934 |
+
### Claude Code (Pro/Max)
|
| 935 |
+
|
| 936 |
+
```bash
|
| 937 |
+
Dashboard → Providers → Connect Claude Code
|
| 938 |
+
→ OAuth login → Auto token refresh
|
| 939 |
+
→ 5-hour + weekly quota tracking
|
| 940 |
+
|
| 941 |
+
Models:
|
| 942 |
+
cc/claude-opus-4-7
|
| 943 |
+
cc/claude-opus-4-6
|
| 944 |
+
cc/claude-sonnet-4-6
|
| 945 |
+
cc/claude-haiku-4-5-20251001
|
| 946 |
+
```
|
| 947 |
+
|
| 948 |
+
**Pro Tip:** Use Opus for complex tasks, Sonnet for speed. 9Router tracks quota per model!
|
| 949 |
+
|
| 950 |
+
### OpenAI Codex (Plus/Pro)
|
| 951 |
+
|
| 952 |
+
```bash
|
| 953 |
+
Dashboard → Providers → Connect Codex
|
| 954 |
+
→ OAuth login (port 1455)
|
| 955 |
+
→ 5-hour + weekly reset
|
| 956 |
+
|
| 957 |
+
Models:
|
| 958 |
+
cx/gpt-5.5
|
| 959 |
+
cx/gpt-5.4
|
| 960 |
+
cx/gpt-5.3-codex
|
| 961 |
+
cx/gpt-5.2-codex
|
| 962 |
+
```
|
| 963 |
+
|
| 964 |
+
### GitHub Copilot
|
| 965 |
+
|
| 966 |
+
```bash
|
| 967 |
+
Dashboard → Providers → Connect GitHub
|
| 968 |
+
→ OAuth via GitHub
|
| 969 |
+
→ Monthly reset (1st of month)
|
| 970 |
+
|
| 971 |
+
Models:
|
| 972 |
+
gh/gpt-5.4
|
| 973 |
+
gh/claude-opus-4.7
|
| 974 |
+
gh/claude-sonnet-4.6
|
| 975 |
+
gh/gemini-3.1-pro-preview
|
| 976 |
+
gh/grok-code-fast-1
|
| 977 |
+
```
|
| 978 |
+
|
| 979 |
+
### Cursor IDE
|
| 980 |
+
|
| 981 |
+
```bash
|
| 982 |
+
Dashboard → Providers → Connect Cursor
|
| 983 |
+
→ OAuth login
|
| 984 |
+
→ Monthly subscription
|
| 985 |
+
|
| 986 |
+
Models:
|
| 987 |
+
cu/claude-4.6-opus-max
|
| 988 |
+
cu/claude-4.5-sonnet-thinking
|
| 989 |
+
cu/gpt-5.3-codex
|
| 990 |
+
```
|
| 991 |
+
|
| 992 |
+
</details>
|
| 993 |
+
|
| 994 |
+
<details>
|
| 995 |
+
<summary><b>💰 Cheap Providers (Backup)</b></summary>
|
| 996 |
+
|
| 997 |
+
### GLM-5.1 / GLM-4.7 (Daily reset, $0.6/1M)
|
| 998 |
+
|
| 999 |
+
1. Sign up: [Zhipu AI](https://open.bigmodel.cn/)
|
| 1000 |
+
2. Get API key from Coding Plan
|
| 1001 |
+
3. Dashboard → Add API Key:
|
| 1002 |
+
- Provider: `glm`
|
| 1003 |
+
- API Key: `your-key`
|
| 1004 |
+
|
| 1005 |
+
**Use:** `glm/glm-5.1`, `glm/glm-5`, `glm/glm-4.7`
|
| 1006 |
+
|
| 1007 |
+
**Pro Tip:** Coding Plan offers 3× quota at 1/7 cost! Reset daily 10:00 AM.
|
| 1008 |
+
|
| 1009 |
+
### MiniMax M2.7 (5h reset, $0.20/1M)
|
| 1010 |
+
|
| 1011 |
+
1. Sign up: [MiniMax](https://www.minimax.io/)
|
| 1012 |
+
2. Get API key
|
| 1013 |
+
3. Dashboard → Add API Key
|
| 1014 |
+
|
| 1015 |
+
**Use:** `minimax/MiniMax-M2.7`, `minimax/MiniMax-M2.5`
|
| 1016 |
+
|
| 1017 |
+
**Pro Tip:** Cheapest option for long context (1M tokens)!
|
| 1018 |
+
|
| 1019 |
+
### Kimi K2.5 ($9/month flat)
|
| 1020 |
+
|
| 1021 |
+
1. Subscribe: [Moonshot AI](https://platform.moonshot.ai/)
|
| 1022 |
+
2. Get API key
|
| 1023 |
+
3. Dashboard → Add API Key
|
| 1024 |
+
|
| 1025 |
+
**Use:** `kimi/kimi-k2.5`, `kimi/kimi-k2.5-thinking`
|
| 1026 |
+
|
| 1027 |
+
**Pro Tip:** Fixed $9/month for 10M tokens = $0.90/1M effective cost!
|
| 1028 |
+
|
| 1029 |
+
</details>
|
| 1030 |
+
|
| 1031 |
+
<details>
|
| 1032 |
+
<summary><b>🆓 FREE Providers (Recommended)</b></summary>
|
| 1033 |
+
|
| 1034 |
+
### Kiro AI (Claude 4.5 + GLM-5 + MiniMax FREE)
|
| 1035 |
+
|
| 1036 |
+
```bash
|
| 1037 |
+
Dashboard → Connect Kiro
|
| 1038 |
+
→ AWS Builder ID, AWS IAM Identity Center, Google, or GitHub
|
| 1039 |
+
→ Unlimited usage
|
| 1040 |
+
|
| 1041 |
+
Models:
|
| 1042 |
+
kr/claude-sonnet-4.5
|
| 1043 |
+
kr/claude-haiku-4.5
|
| 1044 |
+
kr/glm-5
|
| 1045 |
+
kr/MiniMax-M2.5
|
| 1046 |
+
kr/qwen3-coder-next
|
| 1047 |
+
kr/deepseek-3.2
|
| 1048 |
+
```
|
| 1049 |
+
|
| 1050 |
+
**Pro Tip:** Best free option for Claude. No API key, no payment, fully unlimited.
|
| 1051 |
+
|
| 1052 |
+
### OpenCode Free (No auth, auto-fetch models)
|
| 1053 |
+
|
| 1054 |
+
```bash
|
| 1055 |
+
Dashboard → Connect OpenCode Free
|
| 1056 |
+
→ No login required (passthrough proxy)
|
| 1057 |
+
→ Models auto-fetched from opencode.ai/zen/v1/models
|
| 1058 |
+
```
|
| 1059 |
+
|
| 1060 |
+
**Pro Tip:** Fastest setup. Just connect and start coding.
|
| 1061 |
+
|
| 1062 |
+
### Vertex AI ($300 free credits for new GCP accounts)
|
| 1063 |
+
|
| 1064 |
+
```bash
|
| 1065 |
+
Dashboard → Connect Vertex AI
|
| 1066 |
+
→ Upload Google Cloud Service Account JSON
|
| 1067 |
+
→ Enable Vertex AI API in your GCP project
|
| 1068 |
+
|
| 1069 |
+
Models:
|
| 1070 |
+
vertex/gemini-3.1-pro-preview
|
| 1071 |
+
vertex/gemini-3-flash-preview
|
| 1072 |
+
vertex/gemini-2.5-flash
|
| 1073 |
+
|
| 1074 |
+
Vertex Partner (Anthropic / DeepSeek / GLM / Qwen via Vertex):
|
| 1075 |
+
vertex-partner/glm-5-maas
|
| 1076 |
+
vertex-partner/deepseek-v3.2-maas
|
| 1077 |
+
vertex-partner/qwen3-next-80b-a3b-thinking-maas
|
| 1078 |
+
```
|
| 1079 |
+
|
| 1080 |
+
**Pro Tip:** New Google Cloud accounts get $300 credits free for 90 days. Plenty for daily coding.
|
| 1081 |
+
|
| 1082 |
+
</details>
|
| 1083 |
+
|
| 1084 |
+
<details>
|
| 1085 |
+
<summary><b>🎨 Create Combos</b></summary>
|
| 1086 |
+
|
| 1087 |
+
### Example 1: Maximize Subscription → Cheap Backup
|
| 1088 |
+
|
| 1089 |
+
```
|
| 1090 |
+
Dashboard → Combos → Create New
|
| 1091 |
+
|
| 1092 |
+
Name: premium-coding
|
| 1093 |
+
Models:
|
| 1094 |
+
1. cc/claude-opus-4-7 (Subscription primary)
|
| 1095 |
+
2. glm/glm-5.1 (Cheap backup, $0.6/1M)
|
| 1096 |
+
3. minimax/MiniMax-M2.7 (Cheapest fallback, $0.20/1M)
|
| 1097 |
+
|
| 1098 |
+
Use in CLI: premium-coding
|
| 1099 |
+
|
| 1100 |
+
Monthly cost example (100M tokens):
|
| 1101 |
+
80M via Claude (subscription): $0 extra
|
| 1102 |
+
15M via GLM: $9
|
| 1103 |
+
5M via MiniMax: $1
|
| 1104 |
+
Total: $10 + your subscription
|
| 1105 |
+
```
|
| 1106 |
+
|
| 1107 |
+
### Example 2: Free-Only (Zero Cost)
|
| 1108 |
+
|
| 1109 |
+
```
|
| 1110 |
+
Name: free-combo
|
| 1111 |
+
Models:
|
| 1112 |
+
1. kr/claude-sonnet-4.5 (Claude 4.5 free via Kiro, ~50 credits/mo)
|
| 1113 |
+
2. kr/glm-5 (GLM-5 free via Kiro)
|
| 1114 |
+
3. vertex/gemini-3.1-pro-preview ($300 free credits)
|
| 1115 |
+
|
| 1116 |
+
Cost: $0 forever (+ 20-40% token savings via RTK)!
|
| 1117 |
+
```
|
| 1118 |
+
|
| 1119 |
+
</details>
|
| 1120 |
+
|
| 1121 |
+
<details>
|
| 1122 |
+
<summary><b>🔧 CLI Integration</b></summary>
|
| 1123 |
+
|
| 1124 |
+
### Cursor IDE
|
| 1125 |
+
|
| 1126 |
+
```
|
| 1127 |
+
Settings → Models → Advanced:
|
| 1128 |
+
OpenAI API Base URL: http://localhost:20128/v1
|
| 1129 |
+
OpenAI API Key: [from 9router dashboard]
|
| 1130 |
+
Model: cc/claude-opus-4-7
|
| 1131 |
+
```
|
| 1132 |
+
|
| 1133 |
+
Or use combo: `premium-coding`
|
| 1134 |
+
|
| 1135 |
+
### Claude Code
|
| 1136 |
+
|
| 1137 |
+
Edit `~/.claude/config.json`:
|
| 1138 |
+
|
| 1139 |
+
```json
|
| 1140 |
+
{
|
| 1141 |
+
"anthropic_api_base": "http://localhost:20128/v1",
|
| 1142 |
+
"anthropic_api_key": "your-9router-api-key"
|
| 1143 |
+
}
|
| 1144 |
+
```
|
| 1145 |
+
|
| 1146 |
+
### Codex CLI
|
| 1147 |
+
|
| 1148 |
+
```bash
|
| 1149 |
+
export OPENAI_BASE_URL="http://localhost:20128"
|
| 1150 |
+
export OPENAI_API_KEY="your-9router-api-key"
|
| 1151 |
+
|
| 1152 |
+
codex "your prompt"
|
| 1153 |
+
```
|
| 1154 |
+
|
| 1155 |
+
### OpenClaw
|
| 1156 |
+
|
| 1157 |
+
**Option 1 — Dashboard (recommended):**
|
| 1158 |
+
|
| 1159 |
+
```
|
| 1160 |
+
Dashboard → CLI Tools → OpenClaw → Select Model → Apply
|
| 1161 |
+
```
|
| 1162 |
+
|
| 1163 |
+
**Option 2 — Manual:** Edit `~/.openclaw/openclaw.json`:
|
| 1164 |
+
|
| 1165 |
+
```json
|
| 1166 |
+
{
|
| 1167 |
+
"agents": {
|
| 1168 |
+
"defaults": {
|
| 1169 |
+
"model": {
|
| 1170 |
+
"primary": "9router/kr/claude-sonnet-4.5"
|
| 1171 |
+
}
|
| 1172 |
+
}
|
| 1173 |
+
},
|
| 1174 |
+
"models": {
|
| 1175 |
+
"providers": {
|
| 1176 |
+
"9router": {
|
| 1177 |
+
"baseUrl": "http://127.0.0.1:20128/v1",
|
| 1178 |
+
"apiKey": "sk_9router",
|
| 1179 |
+
"api": "openai-completions",
|
| 1180 |
+
"models": [
|
| 1181 |
+
{
|
| 1182 |
+
"id": "kr/claude-sonnet-4.5",
|
| 1183 |
+
"name": "Claude Sonnet 4.5 (Kiro Free)"
|
| 1184 |
+
}
|
| 1185 |
+
]
|
| 1186 |
+
}
|
| 1187 |
+
}
|
| 1188 |
+
}
|
| 1189 |
+
}
|
| 1190 |
+
```
|
| 1191 |
+
|
| 1192 |
+
> **Note:** OpenClaw only works with local 9Router. Use `127.0.0.1` instead of `localhost` to avoid IPv6 resolution issues.
|
| 1193 |
+
|
| 1194 |
+
### Cline / Continue / RooCode
|
| 1195 |
+
|
| 1196 |
+
```
|
| 1197 |
+
Provider: OpenAI Compatible
|
| 1198 |
+
Base URL: http://localhost:20128/v1
|
| 1199 |
+
API Key: [from dashboard]
|
| 1200 |
+
Model: cc/claude-opus-4-7
|
| 1201 |
+
```
|
| 1202 |
+
|
| 1203 |
+
</details>
|
| 1204 |
+
|
| 1205 |
+
<details>
|
| 1206 |
+
<summary><b>🚀 Deployment</b></summary>
|
| 1207 |
+
|
| 1208 |
+
### VPS Deployment
|
| 1209 |
+
|
| 1210 |
+
```bash
|
| 1211 |
+
# Clone and install
|
| 1212 |
+
git clone https://github.com/decolua/9router.git
|
| 1213 |
+
cd 9router
|
| 1214 |
+
npm install
|
| 1215 |
+
npm run build
|
| 1216 |
+
|
| 1217 |
+
# Configure
|
| 1218 |
+
export JWT_SECRET="your-secure-secret-change-this"
|
| 1219 |
+
export INITIAL_PASSWORD="your-password"
|
| 1220 |
+
export DATA_DIR="/var/lib/9router"
|
| 1221 |
+
export PORT="20128"
|
| 1222 |
+
export HOSTNAME="0.0.0.0"
|
| 1223 |
+
export NODE_ENV="production"
|
| 1224 |
+
export NEXT_PUBLIC_BASE_URL="http://localhost:20128"
|
| 1225 |
+
export NEXT_PUBLIC_CLOUD_URL="https://9router.com"
|
| 1226 |
+
export API_KEY_SECRET="endpoint-proxy-api-key-secret"
|
| 1227 |
+
export MACHINE_ID_SALT="endpoint-proxy-salt"
|
| 1228 |
+
|
| 1229 |
+
# Start
|
| 1230 |
+
npm run start
|
| 1231 |
+
|
| 1232 |
+
# Or use PM2
|
| 1233 |
+
npm install -g pm2
|
| 1234 |
+
pm2 start npm --name 9router -- start
|
| 1235 |
+
pm2 save
|
| 1236 |
+
pm2 startup
|
| 1237 |
+
```
|
| 1238 |
+
|
| 1239 |
+
### Docker
|
| 1240 |
+
|
| 1241 |
+
Published images (multi-platform `linux/amd64` + `linux/arm64`):
|
| 1242 |
+
|
| 1243 |
+
- Docker Hub: [`decolua/9router`](https://hub.docker.com/r/decolua/9router)
|
| 1244 |
+
- GHCR: [`ghcr.io/decolua/9router`](https://github.com/decolua/9router/pkgs/container/9router)
|
| 1245 |
+
|
| 1246 |
+
**Quick start (use published image):**
|
| 1247 |
+
|
| 1248 |
+
```bash
|
| 1249 |
+
docker run -d \
|
| 1250 |
+
--name 9router \
|
| 1251 |
+
-p 20128:20128 \
|
| 1252 |
+
-v "$HOME/.9router:/app/data" \
|
| 1253 |
+
-e DATA_DIR=/app/data \
|
| 1254 |
+
decolua/9router:latest
|
| 1255 |
+
```
|
| 1256 |
+
|
| 1257 |
+
→ Open http://localhost:20128
|
| 1258 |
+
|
| 1259 |
+
**Build from source (dev):**
|
| 1260 |
+
|
| 1261 |
+
```bash
|
| 1262 |
+
git clone https://github.com/decolua/9router.git
|
| 1263 |
+
cd 9router/app
|
| 1264 |
+
docker build -t 9router .
|
| 1265 |
+
docker run -d --name 9router -p 20128:20128 \
|
| 1266 |
+
-v "$HOME/.9router:/app/data" -e DATA_DIR=/app/data 9router
|
| 1267 |
+
```
|
| 1268 |
+
|
| 1269 |
+
**Container defaults:**
|
| 1270 |
+
|
| 1271 |
+
- `PORT=20128`
|
| 1272 |
+
- `HOSTNAME=0.0.0.0`
|
| 1273 |
+
|
| 1274 |
+
**Useful commands:**
|
| 1275 |
+
|
| 1276 |
+
```bash
|
| 1277 |
+
docker logs -f 9router
|
| 1278 |
+
docker restart 9router
|
| 1279 |
+
docker stop 9router && docker rm 9router
|
| 1280 |
+
docker pull decolua/9router:latest # update to latest
|
| 1281 |
+
```
|
| 1282 |
+
|
| 1283 |
+
**Data persistence:** `$HOME/.9router/db/data.sqlite` on host ↔ `/app/data/db/data.sqlite` in container.
|
| 1284 |
+
|
| 1285 |
+
### Environment Variables
|
| 1286 |
+
|
| 1287 |
+
| Variable | Default | Description |
|
| 1288 |
+
| ---------------------------------------------------- | ---------------------------------------- | ----------------------------------------------------------------------------------- |
|
| 1289 |
+
| `JWT_SECRET` | Auto-generated (`~/.9router/jwt-secret`) | JWT signing secret for dashboard auth cookie (override to share across instances) |
|
| 1290 |
+
| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists |
|
| 1291 |
+
| `DATA_DIR` | `~/.9router` | Main app data location (SQLite at `$DATA_DIR/db/data.sqlite`) |
|
| 1292 |
+
| `PORT` | framework default | Service port (`20128` in examples) |
|
| 1293 |
+
| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) |
|
| 1294 |
+
| `NODE_ENV` | runtime default | Set `production` for deploy |
|
| 1295 |
+
| `BASE_URL` | `http://localhost:20128` | Server-side internal base URL used by cloud sync jobs |
|
| 1296 |
+
| `CLOUD_URL` | `https://9router.com` | Server-side cloud sync endpoint base URL |
|
| 1297 |
+
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Backward-compatible/public base URL (prefer `BASE_URL` for server runtime) |
|
| 1298 |
+
| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | Backward-compatible/public cloud URL (prefer `CLOUD_URL` for server runtime) |
|
| 1299 |
+
| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys |
|
| 1300 |
+
| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt for stable machine ID hashing |
|
| 1301 |
+
| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs under `logs/` |
|
| 1302 |
+
| `AUTH_COOKIE_SECURE` | `false` | Force `Secure` auth cookie (set `true` behind HTTPS reverse proxy) |
|
| 1303 |
+
| `REQUIRE_API_KEY` | `false` | Enforce Bearer API key on `/v1/*` routes (recommended for internet-exposed deploys) |
|
| 1304 |
+
| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Optional outbound proxy for upstream provider calls |
|
| 1305 |
+
| `SEARXNG_URL` | `http://localhost:8888/search` | Endpoint for the built-in unauthenticated SearXNG web-search provider |
|
| 1306 |
+
|
| 1307 |
+
Notes:
|
| 1308 |
+
|
| 1309 |
+
- Lowercase proxy variables are also supported: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`.
|
| 1310 |
+
- `.env` is not baked into Docker image (`.dockerignore`); inject runtime config with `--env-file` or `-e`.
|
| 1311 |
+
- On Windows, `APPDATA` can be used for local storage path resolution.
|
| 1312 |
+
- `INSTANCE_NAME` appears in older docs/env templates, but is currently not used at runtime.
|
| 1313 |
+
|
| 1314 |
+
### Runtime Files and Storage
|
| 1315 |
+
|
| 1316 |
+
- Main app state: `${DATA_DIR}/db/data.sqlite` (SQLite — providers, combos, aliases, keys, settings, usage history)
|
| 1317 |
+
- Auto backups: `${DATA_DIR}/db/backups/`
|
| 1318 |
+
- Optional request/translator logs: `<repo>/logs/...` when `ENABLE_REQUEST_LOGS=true`
|
| 1319 |
+
- Both `${DATA_DIR}` and `~/.9router` resolve to the same location in a Docker container — the symlink `/root/.9router -> /app/data` is created at build time.
|
| 1320 |
+
|
| 1321 |
+
</details>
|
| 1322 |
+
|
| 1323 |
+
---
|
| 1324 |
+
|
| 1325 |
+
## 📊 Available Models
|
| 1326 |
+
|
| 1327 |
+
<details>
|
| 1328 |
+
<summary><b>View all available models</b></summary>
|
| 1329 |
+
|
| 1330 |
+
**Claude Code (`cc/`)** - Pro/Max:
|
| 1331 |
+
|
| 1332 |
+
- `cc/claude-opus-4-7`
|
| 1333 |
+
- `cc/claude-opus-4-6`
|
| 1334 |
+
- `cc/claude-sonnet-4-6`
|
| 1335 |
+
- `cc/claude-sonnet-4-5-20250929`
|
| 1336 |
+
- `cc/claude-haiku-4-5-20251001`
|
| 1337 |
+
|
| 1338 |
+
**Codex (`cx/`)** - Plus/Pro:
|
| 1339 |
+
|
| 1340 |
+
- `cx/gpt-5.5`
|
| 1341 |
+
- `cx/gpt-5.4`
|
| 1342 |
+
- `cx/gpt-5.3-codex`
|
| 1343 |
+
- `cx/gpt-5.2-codex`
|
| 1344 |
+
- `cx/gpt-5.1-codex-max`
|
| 1345 |
+
|
| 1346 |
+
**GitHub Copilot (`gh/`)**:
|
| 1347 |
+
|
| 1348 |
+
- `gh/gpt-5.4`
|
| 1349 |
+
- `gh/claude-opus-4.7`
|
| 1350 |
+
- `gh/claude-sonnet-4.6`
|
| 1351 |
+
- `gh/gemini-3.1-pro-preview`
|
| 1352 |
+
- `gh/grok-code-fast-1`
|
| 1353 |
+
|
| 1354 |
+
**Cursor (`cu/`)** - Subscription:
|
| 1355 |
+
|
| 1356 |
+
- `cu/claude-4.6-opus-max`
|
| 1357 |
+
- `cu/claude-4.5-sonnet-thinking`
|
| 1358 |
+
- `cu/gpt-5.3-codex`
|
| 1359 |
+
- `cu/kimi-k2.5`
|
| 1360 |
+
|
| 1361 |
+
**GLM (`glm/`)** - $0.6/1M:
|
| 1362 |
+
|
| 1363 |
+
- `glm/glm-5.1`
|
| 1364 |
+
- `glm/glm-5`
|
| 1365 |
+
- `glm/glm-4.7`
|
| 1366 |
+
|
| 1367 |
+
**MiniMax (`minimax/`)** - $0.2/1M:
|
| 1368 |
+
|
| 1369 |
+
- `minimax/MiniMax-M2.7`
|
| 1370 |
+
- `minimax/MiniMax-M2.5`
|
| 1371 |
+
|
| 1372 |
+
**Kimi (`kimi/`)** - $9/mo flat:
|
| 1373 |
+
|
| 1374 |
+
- `kimi/kimi-k2.5`
|
| 1375 |
+
- `kimi/kimi-k2.5-thinking`
|
| 1376 |
+
|
| 1377 |
+
**Kiro (`kr/`)** - Free (~50 credits/month, paid tiers above):
|
| 1378 |
+
|
| 1379 |
+
- `kr/claude-sonnet-4.5`
|
| 1380 |
+
- `kr/claude-haiku-4.5`
|
| 1381 |
+
- `kr/glm-5`
|
| 1382 |
+
- `kr/MiniMax-M2.5`
|
| 1383 |
+
- `kr/qwen3-coder-next`
|
| 1384 |
+
- `kr/deepseek-3.2`
|
| 1385 |
+
|
| 1386 |
+
**OpenCode Free (`oc/`)** - FREE no-auth:
|
| 1387 |
+
|
| 1388 |
+
- Auto-fetched from `opencode.ai/zen/v1/models`
|
| 1389 |
+
|
| 1390 |
+
**Vertex AI (`vertex/`)** - $300 free credits:
|
| 1391 |
+
|
| 1392 |
+
- `vertex/gemini-3.1-pro-preview`
|
| 1393 |
+
- `vertex/gemini-3-flash-preview`
|
| 1394 |
+
- `vertex/gemini-2.5-flash`
|
| 1395 |
+
- `vertex-partner/glm-5-maas`
|
| 1396 |
+
- `vertex-partner/deepseek-v3.2-maas`
|
| 1397 |
+
|
| 1398 |
+
</details>
|
| 1399 |
+
|
| 1400 |
+
---
|
| 1401 |
+
|
| 1402 |
+
## 🐛 Troubleshooting
|
| 1403 |
+
|
| 1404 |
+
**"Language model did not provide messages"**
|
| 1405 |
+
|
| 1406 |
+
- Provider quota exhausted → Check dashboard quota tracker
|
| 1407 |
+
- Solution: Use combo fallback or switch to cheaper tier
|
| 1408 |
+
|
| 1409 |
+
**Rate limiting**
|
| 1410 |
+
|
| 1411 |
+
- Subscription quota out → Fallback to GLM/MiniMax
|
| 1412 |
+
- Add combo: `cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5`
|
| 1413 |
+
|
| 1414 |
+
**OAuth token expired**
|
| 1415 |
+
|
| 1416 |
+
- Auto-refreshed by 9Router
|
| 1417 |
+
- If issues persist: Dashboard → Provider → Reconnect
|
| 1418 |
+
|
| 1419 |
+
**High costs**
|
| 1420 |
+
|
| 1421 |
+
- Enable RTK in Dashboard → Endpoint settings (default ON, saves 20-40% tokens)
|
| 1422 |
+
- Check usage stats in Dashboard
|
| 1423 |
+
- Switch primary model to GLM/MiniMax
|
| 1424 |
+
- Use free tier (Kiro, OpenCode Free, Vertex) for non-critical tasks
|
| 1425 |
+
|
| 1426 |
+
**Dashboard opens on wrong port**
|
| 1427 |
+
|
| 1428 |
+
- Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
|
| 1429 |
+
|
| 1430 |
+
**First login not working**
|
| 1431 |
+
|
| 1432 |
+
- Check `INITIAL_PASSWORD` in `.env`
|
| 1433 |
+
- If unset, fallback password is `123456`
|
| 1434 |
+
|
| 1435 |
+
**No request logs under `logs/`**
|
| 1436 |
+
|
| 1437 |
+
- Set `ENABLE_REQUEST_LOGS=true`
|
| 1438 |
+
|
| 1439 |
+
---
|
| 1440 |
+
|
| 1441 |
+
## 🛠️ Tech Stack
|
| 1442 |
+
|
| 1443 |
+
- **Runtime**: Node.js 20+
|
| 1444 |
+
- **Framework**: Next.js 16
|
| 1445 |
+
- **UI**: React 19 + Tailwind CSS 4
|
| 1446 |
+
- **Database**: SQLite (better-sqlite3 / node:sqlite / sql.js fallback)
|
| 1447 |
+
- **Streaming**: Server-Sent Events (SSE)
|
| 1448 |
+
- **Auth**: OAuth 2.0 (PKCE) + JWT + API Keys
|
| 1449 |
+
|
| 1450 |
+
---
|
| 1451 |
+
|
| 1452 |
+
## 📝 API Reference
|
| 1453 |
+
|
| 1454 |
+
### Chat Completions
|
| 1455 |
+
|
| 1456 |
+
```bash
|
| 1457 |
+
POST http://localhost:20128/v1/chat/completions
|
| 1458 |
+
Authorization: Bearer your-api-key
|
| 1459 |
+
Content-Type: application/json
|
| 1460 |
+
|
| 1461 |
+
{
|
| 1462 |
+
"model": "cc/claude-opus-4-6",
|
| 1463 |
+
"messages": [
|
| 1464 |
+
{"role": "user", "content": "Write a function to..."}
|
| 1465 |
+
],
|
| 1466 |
+
"stream": true
|
| 1467 |
+
}
|
| 1468 |
+
```
|
| 1469 |
+
|
| 1470 |
+
### List Models
|
| 1471 |
+
|
| 1472 |
+
```bash
|
| 1473 |
+
GET http://localhost:20128/v1/models
|
| 1474 |
+
Authorization: Bearer your-api-key
|
| 1475 |
+
|
| 1476 |
+
→ Returns all models + combos in OpenAI format
|
| 1477 |
+
```
|
| 1478 |
+
|
| 1479 |
+
## 📧 Support
|
| 1480 |
+
|
| 1481 |
+
- **Website**: [9router.com](https://9router.com)
|
| 1482 |
+
- **GitHub**: [github.com/decolua/9router](https://github.com/decolua/9router)
|
| 1483 |
+
- **Issues**: [github.com/decolua/9router/issues](https://github.com/decolua/9router/issues)
|
| 1484 |
+
|
| 1485 |
+
---
|
| 1486 |
+
|
| 1487 |
+
## 👥 Contributors
|
| 1488 |
+
|
| 1489 |
+
Thanks to all contributors who helped make 9Router better!
|
| 1490 |
+
|
| 1491 |
+
[](https://github.com/decolua/9router/graphs/contributors)
|
| 1492 |
+
|
| 1493 |
+
---
|
| 1494 |
+
|
| 1495 |
+
## 📊 Star Chart
|
| 1496 |
+
|
| 1497 |
+
[](https://starchart.cc/decolua/9router)
|
| 1498 |
+
|
| 1499 |
+
## 🔀 Forks
|
| 1500 |
+
|
| 1501 |
+
**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — A full-featured TypeScript fork of 9Router. Adds 36+ providers, 4-tier auto-fallback, multi-modal APIs (images, embeddings, audio, TTS), circuit breaker, semantic cache, LLM evaluations, and a polished dashboard. 368+ unit tests. Available via npm and Docker.
|
| 1502 |
+
|
| 1503 |
+
---
|
| 1504 |
+
|
| 1505 |
+
## 🙏 Acknowledgments
|
| 1506 |
+
|
| 1507 |
+
Built on the shoulders of giants:
|
| 1508 |
+
|
| 1509 |
+
- **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — original Go implementation that inspired this JavaScript port.
|
| 1510 |
+
- **[RTK](https://github.com/rtk-ai/rtk)**  — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request.
|
| 1511 |
+
- **[Caveman](https://github.com/JuliusBrussee/caveman)**  by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — viral _"why use many token when few token do trick"_. 9Router adapts its prompt → **−65% output tokens**.
|
| 1512 |
+
- **[Ponytail](https://github.com/DietrichGebert/ponytail)**  by **[@DietrichGebert](https://github.com/DietrichGebert)** — _"lazy senior dev"_ skill. 9Router injects its YAGNI-first ladder → **fewer tokens, less code, shorter diffs**.
|
| 1513 |
+
|
| 1514 |
+
Huge thanks to these authors — without their work, 9Router's token-saving features wouldn't exist. ⭐ them on GitHub!
|
| 1515 |
+
|
| 1516 |
+
---
|
| 1517 |
+
|
| 1518 |
+
## 📄 License
|
| 1519 |
+
|
| 1520 |
+
MIT License - see [LICENSE](LICENSE) for details.
|
| 1521 |
+
|
| 1522 |
+
---
|
| 1523 |
+
|
| 1524 |
+
<div align="center">
|
| 1525 |
+
<sub>Built with ❤️ for developers who code 24/7</sub>
|
| 1526 |
+
</div>
|
README.zh-CN.md
ADDED
|
@@ -0,0 +1,1315 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
<div align="center">
|
| 3 |
+
<img src="./images/9router.png?1" alt="9Router Dashboard" width="800"/>
|
| 4 |
+
|
| 5 |
+
# 9Router - 免费 AI 路由器与 Token 节省器
|
| 6 |
+
|
| 7 |
+
**编程永不停歇。使用 RTK + 自动切换到免费/低价 AI 模型,节省 20-40% 的 tokens。**
|
| 8 |
+
|
| 9 |
+
**将所有 AI 编程工具(Claude Code、Cursor、Antigravity、Copilot、Codex、Gemini、OpenCode、Cline、OpenClaw...)连接到 40+ AI 提供商和 100+ 模型。**
|
| 10 |
+
|
| 11 |
+
[](https://www.npmjs.com/package/9router)
|
| 12 |
+
[](https://www.npmjs.com/package/9router)
|
| 13 |
+
[](https://github.com/decolua/9router/blob/main/LICENSE)
|
| 14 |
+
|
| 15 |
+
<a href="https://trendshift.io/repositories/22628" target="_blank"><img src="https://trendshift.io/api/badge/repositories/22628" alt="decolua%2F9router | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
| 16 |
+
|
| 17 |
+
[🚀 快速开始](#-快速开始) • [💡 功能特点](#-主要功能) • [📖 设置指南](#-设置指南) • [🌐 网站](https://9router.com)
|
| 18 |
+
|
| 19 |
+
[🇻🇳 Tiếng Việt](./i18n/README.vi.md) • [🇨🇳 中文](./i18n/README.zh-CN.md) • [🇯🇵 日本語](./i18n/README.ja-JP.md)
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
---
|
| 23 |
+
|
| 24 |
+
## 🤔 为什么选择 9Router?
|
| 25 |
+
|
| 26 |
+
**告别浪费金钱、tokens 和触碰限制的困扰:**
|
| 27 |
+
|
| 28 |
+
- ❌ 订阅配额每月到期却未使用
|
| 29 |
+
- ❌ 速率限制在编程中途打断你
|
| 30 |
+
- ❌ 工具输出(git diff、grep、ls...)快速消耗 tokens
|
| 31 |
+
- ❌ 昂贵的 API(每个提供商 $20-50/月)
|
| 32 |
+
- ❌ 需要手动在提供商之间切换
|
| 33 |
+
|
| 34 |
+
**9Router 解决这一切:**
|
| 35 |
+
|
| 36 |
+
- ✅ **RTK Token 节省器** - 自动压缩 tool_result 内容,每次请求节省 20-40% tokens
|
| 37 |
+
- ✅ **充分利用订阅** - 追踪配额,在重置前用尽每一分额度
|
| 38 |
+
- ✅ **自动切换** - 订阅 → 低价 → 免费,零停机时间
|
| 39 |
+
- ✅ **多账户支持** - 按提供商在账户之间轮询
|
| 40 |
+
- ✅ **通用兼容** - 支持 Claude Code、Codex、Cursor、Cline 以及任何 CLI 工具
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## 🔄 工作原理
|
| 45 |
+
|
| 46 |
+
```
|
| 47 |
+
┌─────────────┐
|
| 48 |
+
│ 你的 CLI │ (Claude Code、Codex、OpenClaw、Cursor、Cline...)
|
| 49 |
+
│ 工具 │
|
| 50 |
+
└──────┬──────┘
|
| 51 |
+
│ http://localhost:20128/v1
|
| 52 |
+
↓
|
| 53 |
+
┌─────────────────────────────────────────────┐
|
| 54 |
+
│ 9Router(智能路由器) │
|
| 55 |
+
│ • RTK Token 节省器(减少 tool_result tokens)│
|
| 56 |
+
│ • 格式转换(OpenAI ↔ Claude) │
|
| 57 |
+
│ • 配额追踪 │
|
| 58 |
+
│ • 自动刷新 token │
|
| 59 |
+
└──────┬──────────────────────────────────────┘
|
| 60 |
+
│
|
| 61 |
+
├─→ [第一层:订阅] Claude Code、Codex、GitHub Copilot
|
| 62 |
+
│ ↓ 配额耗尽
|
| 63 |
+
├─→ [第二层:低价] GLM ($0.6/1M)、MiniMax ($0.2/1M)
|
| 64 |
+
│ ↓ 预算超限
|
| 65 |
+
└─→ [第三层:免费] Kiro、OpenCode Free、Vertex ($300 额度)
|
| 66 |
+
|
| 67 |
+
结果:编程永不停歇,最小成本 + 通过 RTK 节省 20-40% tokens
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
---
|
| 71 |
+
|
| 72 |
+
## ⚡ 快速开始
|
| 73 |
+
|
| 74 |
+
**1. 全局安装:**
|
| 75 |
+
|
| 76 |
+
```bash
|
| 77 |
+
npm install -g 9router
|
| 78 |
+
9router
|
| 79 |
+
```
|
| 80 |
+
|
| 81 |
+
🎉 控制面板在 `http://localhost:20128` 打开
|
| 82 |
+
|
| 83 |
+
**2. 连接免费提供商(无需注册):**
|
| 84 |
+
|
| 85 |
+
控制面板 → 提供商 → 连接 **Kiro AI**(约 50 积分/月免费:Claude 4.5 + GLM-5 + MiniMax)或 **OpenCode Free**(无需认证)→ 完成!
|
| 86 |
+
|
| 87 |
+
**3. 在 CLI 工具中使用:**
|
| 88 |
+
|
| 89 |
+
```
|
| 90 |
+
Claude Code/Codex/OpenClaw/Cursor/Cline 设置:
|
| 91 |
+
Endpoint: http://localhost:20128/v1
|
| 92 |
+
API Key: [从控制面板复制]
|
| 93 |
+
Model: kr/claude-sonnet-4.5
|
| 94 |
+
```
|
| 95 |
+
|
| 96 |
+
**就这么简单!** 开始使用免费 AI 模型编程。
|
| 97 |
+
|
| 98 |
+
**替代方案:从源码运行(本仓库):**
|
| 99 |
+
|
| 100 |
+
本仓库的包是私有的(`9router-app`),所以源码/Docker 执行是预期的本地开发方式。
|
| 101 |
+
|
| 102 |
+
```bash
|
| 103 |
+
cp .env.example .env
|
| 104 |
+
npm install
|
| 105 |
+
PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
生产模式:
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
npm run build
|
| 112 |
+
PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
默认 URL:
|
| 116 |
+
- 控制面板:`http://localhost:20128/dashboard`
|
| 117 |
+
- OpenAI 兼容 API:`http://localhost:20128/v1`
|
| 118 |
+
|
| 119 |
+
---
|
| 120 |
+
|
| 121 |
+
## 视频教程
|
| 122 |
+
|
| 123 |
+
<div align="center">
|
| 124 |
+
|
| 125 |
+
<table>
|
| 126 |
+
<tr>
|
| 127 |
+
<td align="center" width="320">
|
| 128 |
+
<a href="https://www.youtube.com/watch?v=raEyZPg5xE0">
|
| 129 |
+
<img src="https://img.youtube.com/vi/raEyZPg5xE0/maxresdefault.jpg" alt="9Router Setup Tutorial" width="300"/>
|
| 130 |
+
</a><br/>
|
| 131 |
+
<b>🇺🇸 English</b><br/>
|
| 132 |
+
<sub>9Router + Claude Code 免费设���<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 133 |
+
</td>
|
| 134 |
+
<td align="center" width="320">
|
| 135 |
+
<a href="https://www.youtube.com/watch?v=X69n5Lm06Yw">
|
| 136 |
+
<img src="https://img.youtube.com/vi/X69n5Lm06Yw/maxresdefault.jpg" alt="Tiết kiệm chi phí LLM với 9Router" width="300"/>
|
| 137 |
+
</a><br/>
|
| 138 |
+
<b>🇻🇳 Tiếng Việt</b><br/>
|
| 139 |
+
<sub>使用 9Router 节省 OpenClaw 的 LLM 成本<br/>by <a href="https://www.youtube.com/c/M%C3%ACAIblog">Mì AI</a></sub>
|
| 140 |
+
</td>
|
| 141 |
+
<td align="center" width="320">
|
| 142 |
+
<a href="https://www.youtube.com/watch?v=o3qYCyjrFYg">
|
| 143 |
+
<img src="https://img.youtube.com/vi/o3qYCyjrFYg/maxresdefault.jpg" alt="Claude Code FREE Forever" width="300"/>
|
| 144 |
+
</a><br/>
|
| 145 |
+
<b>🇺🇸 English</b><br/>
|
| 146 |
+
<sub>Claude Code 免费永久使用 — 无限模型<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 147 |
+
</td>
|
| 148 |
+
</tr>
|
| 149 |
+
<tr>
|
| 150 |
+
<td align="center" width="320">
|
| 151 |
+
<a href="https://www.youtube.com/watch?v=Ttpc26m39Dw">
|
| 152 |
+
<img src="https://img.youtube.com/vi/Ttpc26m39Dw/maxresdefault.jpg" alt="Claude CLI Free Setup" width="300"/>
|
| 153 |
+
</a><br/>
|
| 154 |
+
<b>🇺🇸 English</b><br/>
|
| 155 |
+
<sub>使用 9Router 免费设置 Claude CLI 🚀<br/>by <a href="https://www.youtube.com/@CodeVerseSoban">CodeVerse Soban</a></sub>
|
| 156 |
+
</td>
|
| 157 |
+
<td align="center" width="320">
|
| 158 |
+
<a href="https://www.youtube.com/watch?v=G-5A_D5Pm6Y">
|
| 159 |
+
<img src="https://img.youtube.com/vi/G-5A_D5Pm6Y/maxresdefault.jpg" alt="Cài đặt OpenClaw Free A-Z" width="300"/>
|
| 160 |
+
</a><br/>
|
| 161 |
+
<b>🇻🇳 Tiếng Việt</b><br/>
|
| 162 |
+
<sub>从零开始安装 OpenClaw 免费版 + 9Router<br/>by <a href="https://www.youtube.com/@maigia">Mai Gia</a></sub>
|
| 163 |
+
</td>
|
| 164 |
+
<td align="center" width="320">
|
| 165 |
+
<a href="https://www.youtube.com/watch?v=JXmg8_gccgE">
|
| 166 |
+
<img src="https://img.youtube.com/vi/JXmg8_gccgE/maxresdefault.jpg" alt="FREE OpenClaw with Claude Opus" width="300"/>
|
| 167 |
+
</a><br/>
|
| 168 |
+
<b>🇺🇸 English</b><br/>
|
| 169 |
+
<sub>免费 OpenClaw + Claude Opus 4.6<br/>by <a href="https://www.youtube.com/@BuildAIWithHamid">Build AI With Hamid</a></sub>
|
| 170 |
+
</td>
|
| 171 |
+
</tr>
|
| 172 |
+
</table>
|
| 173 |
+
|
| 174 |
+
</div>
|
| 175 |
+
|
| 176 |
+
> 🎬 **制作了关于 9Router 的视频?** 提交 [Pull Request](https://github.com/decolua/9router/pulls),将你的视频添加到此部分 — 我们会合并它!
|
| 177 |
+
|
| 178 |
+
---
|
| 179 |
+
|
| 180 |
+
## 🛠️ 支持的 CLI 工具
|
| 181 |
+
|
| 182 |
+
9Router 与所有主流 AI 编程工具无缝协作:
|
| 183 |
+
|
| 184 |
+
<div align="center">
|
| 185 |
+
<table>
|
| 186 |
+
<tr>
|
| 187 |
+
<td align="center" width="120">
|
| 188 |
+
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
|
| 189 |
+
<b>Claude-Code</b>
|
| 190 |
+
</td>
|
| 191 |
+
<td align="center" width="120">
|
| 192 |
+
<img src="./public/providers/openclaw.png" width="60" alt="OpenClaw"/><br/>
|
| 193 |
+
<b>OpenClaw</b>
|
| 194 |
+
</td>
|
| 195 |
+
<td align="center" width="120">
|
| 196 |
+
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
|
| 197 |
+
<b>Codex</b>
|
| 198 |
+
</td>
|
| 199 |
+
<td align="center" width="120">
|
| 200 |
+
<img src="./public/providers/opencode.png" width="60" alt="OpenCode"/><br/>
|
| 201 |
+
<b>OpenCode</b>
|
| 202 |
+
</td>
|
| 203 |
+
<td align="center" width="120">
|
| 204 |
+
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
|
| 205 |
+
<b>Cursor</b>
|
| 206 |
+
</td>
|
| 207 |
+
<td align="center" width="120">
|
| 208 |
+
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
|
| 209 |
+
<b>Antigravity</b>
|
| 210 |
+
</td>
|
| 211 |
+
</tr>
|
| 212 |
+
<tr>
|
| 213 |
+
<td align="center" width="120">
|
| 214 |
+
<img src="./public/providers/cline.png" width="60" alt="Cline"/><br/>
|
| 215 |
+
<b>Cline</b>
|
| 216 |
+
</td>
|
| 217 |
+
<td align="center" width="120">
|
| 218 |
+
<img src="./public/providers/continue.png" width="60" alt="Continue"/><br/>
|
| 219 |
+
<b>Continue</b>
|
| 220 |
+
</td>
|
| 221 |
+
<td align="center" width="120">
|
| 222 |
+
<img src="./public/providers/droid.png" width="60" alt="Droid"/><br/>
|
| 223 |
+
<b>Droid</b>
|
| 224 |
+
</td>
|
| 225 |
+
<td align="center" width="120">
|
| 226 |
+
<img src="./public/providers/roo.png" width="60" alt="Roo"/><br/>
|
| 227 |
+
<b>Roo</b>
|
| 228 |
+
</td>
|
| 229 |
+
<td align="center" width="120">
|
| 230 |
+
<img src="./public/providers/copilot.png" width="60" alt="Copilot"/><br/>
|
| 231 |
+
<b>Copilot</b>
|
| 232 |
+
</td>
|
| 233 |
+
<td align="center" width="120">
|
| 234 |
+
<img src="./public/providers/kilocode.png" width="60" alt="Kilo Code"/><br/>
|
| 235 |
+
<b>Kilo Code</b>
|
| 236 |
+
</td>
|
| 237 |
+
</tr>
|
| 238 |
+
</table>
|
| 239 |
+
</div>
|
| 240 |
+
|
| 241 |
+
---
|
| 242 |
+
|
| 243 |
+
## 🌐 支持的提供商
|
| 244 |
+
|
| 245 |
+
### 🔐 OAuth 提供商
|
| 246 |
+
|
| 247 |
+
<div align="center">
|
| 248 |
+
<table>
|
| 249 |
+
<tr>
|
| 250 |
+
<td align="center" width="120">
|
| 251 |
+
<img src="./public/providers/claude.png" width="60" alt="Claude Code"/><br/>
|
| 252 |
+
<b>Claude-Code</b>
|
| 253 |
+
</td>
|
| 254 |
+
<td align="center" width="120">
|
| 255 |
+
<img src="./public/providers/antigravity.png" width="60" alt="Antigravity"/><br/>
|
| 256 |
+
<b>Antigravity</b>
|
| 257 |
+
</td>
|
| 258 |
+
<td align="center" width="120">
|
| 259 |
+
<img src="./public/providers/codex.png" width="60" alt="Codex"/><br/>
|
| 260 |
+
<b>Codex</b>
|
| 261 |
+
</td>
|
| 262 |
+
<td align="center" width="120">
|
| 263 |
+
<img src="./public/providers/github.png" width="60" alt="GitHub"/><br/>
|
| 264 |
+
<b>GitHub</b>
|
| 265 |
+
</td>
|
| 266 |
+
<td align="center" width="120">
|
| 267 |
+
<img src="./public/providers/cursor.png" width="60" alt="Cursor"/><br/>
|
| 268 |
+
<b>Cursor</b>
|
| 269 |
+
</td>
|
| 270 |
+
</tr>
|
| 271 |
+
</table>
|
| 272 |
+
</div>
|
| 273 |
+
|
| 274 |
+
### 🆓 免费提供商
|
| 275 |
+
|
| 276 |
+
<div align="center">
|
| 277 |
+
<table>
|
| 278 |
+
<tr>
|
| 279 |
+
<td align="center" width="150">
|
| 280 |
+
<img src="./public/providers/kiro.png" width="70" alt="Kiro"/><br/>
|
| 281 |
+
<b>Kiro AI</b><br/>
|
| 282 |
+
<sub>Claude 4.5 + GLM-5 + MiniMax<br/>每月 50 积分免费</sub>
|
| 283 |
+
</td>
|
| 284 |
+
<td align="center" width="150">
|
| 285 |
+
<img src="./public/providers/opencode.png" width="70" alt="OpenCode Free"/><br/>
|
| 286 |
+
<b>OpenCode Free</b><br/>
|
| 287 |
+
<sub>无需认证 • 自动获取模型<br/>免费(模型列表会变)</sub>
|
| 288 |
+
</td>
|
| 289 |
+
<td align="center" width="150">
|
| 290 |
+
<img src="./public/providers/gemini.png" width="70" alt="Vertex AI"/><br/>
|
| 291 |
+
<b>Vertex AI</b><br/>
|
| 292 |
+
<sub>Gemini 3 Pro + GLM-5 + DeepSeek<br/>$300 免费额度</sub>
|
| 293 |
+
</td>
|
| 294 |
+
</tr>
|
| 295 |
+
</table>
|
| 296 |
+
</div>
|
| 297 |
+
|
| 298 |
+
> **注意:** iFlow、Qwen Code 和 Gemini CLI 的免费等级已于 2026 年停止。请改用 Kiro / OpenCode Free / Vertex。
|
| 299 |
+
>
|
| 300 |
+
> **Kiro AI** 于 2025 年 9 月转为付费模式 — 免费等级现在上限为**每月 50 积分**(新账户前 30 天另加 500 试用积分)。付费档位:Pro $20/月(1,000 积分)、Pro+ $40/月(2,000)、Pro Max $100/月(5,000)、Power $200/月(10,000)。
|
| 301 |
+
> **OpenCode Free** 的模型列表会随时间变化(部分模型仅限时免费)— 可能随时变更,恕不另行通知。
|
| 302 |
+
> **Vertex AI**:新 GCP 账户的 $300 免费额度仍然有效,但自 2026 年 3 月起 **Gemini API 端点不再消耗这些额度** — 请改用 **Vertex AI Studio** 端点。
|
| 303 |
+
|
| 304 |
+
### 🔑 API Key 提供商(40+)
|
| 305 |
+
|
| 306 |
+
<div align="center">
|
| 307 |
+
<table>
|
| 308 |
+
<tr>
|
| 309 |
+
<td align="center" width="100">
|
| 310 |
+
<img src="./public/providers/openrouter.png" width="50" alt="OpenRouter"/><br/>
|
| 311 |
+
<sub>OpenRouter</sub>
|
| 312 |
+
</td>
|
| 313 |
+
<td align="center" width="100">
|
| 314 |
+
<img src="./public/providers/glm.png" width="50" alt="GLM"/><br/>
|
| 315 |
+
<sub>GLM</sub>
|
| 316 |
+
</td>
|
| 317 |
+
<td align="center" width="100">
|
| 318 |
+
<img src="./public/providers/kimi.png" width="50" alt="Kimi"/><br/>
|
| 319 |
+
<sub>Kimi</sub>
|
| 320 |
+
</td>
|
| 321 |
+
<td align="center" width="100">
|
| 322 |
+
<img src="./public/providers/minimax.png" width="50" alt="MiniMax"/><br/>
|
| 323 |
+
<sub>MiniMax</sub>
|
| 324 |
+
</td>
|
| 325 |
+
<td align="center" width="100">
|
| 326 |
+
<img src="./public/providers/openai.png" width="50" alt="OpenAI"/><br/>
|
| 327 |
+
<sub>OpenAI</sub>
|
| 328 |
+
</td>
|
| 329 |
+
<td align="center" width="100">
|
| 330 |
+
<img src="./public/providers/anthropic.png" width="50" alt="Anthropic"/><br/>
|
| 331 |
+
<sub>Anthropic</sub>
|
| 332 |
+
</td>
|
| 333 |
+
</tr>
|
| 334 |
+
<tr>
|
| 335 |
+
<td align="center" width="100">
|
| 336 |
+
<img src="./public/providers/gemini.png" width="50" alt="Gemini"/><br/>
|
| 337 |
+
<sub>Gemini</sub>
|
| 338 |
+
</td>
|
| 339 |
+
<td align="center" width="100">
|
| 340 |
+
<img src="./public/providers/deepseek.png" width="50" alt="DeepSeek"/><br/>
|
| 341 |
+
<sub>DeepSeek</sub>
|
| 342 |
+
</td>
|
| 343 |
+
<td align="center" width="100">
|
| 344 |
+
<img src="./public/providers/groq.png" width="50" alt="Groq"/><br/>
|
| 345 |
+
<sub>Groq</sub>
|
| 346 |
+
</td>
|
| 347 |
+
<td align="center" width="100">
|
| 348 |
+
<img src="./public/providers/xai.png" width="50" alt="xAI"/><br/>
|
| 349 |
+
<sub>xAI</sub>
|
| 350 |
+
</td>
|
| 351 |
+
<td align="center" width="100">
|
| 352 |
+
<img src="./public/providers/mistral.png" width="50" alt="Mistral"/><br/>
|
| 353 |
+
<sub>Mistral</sub>
|
| 354 |
+
</td>
|
| 355 |
+
<td align="center" width="100">
|
| 356 |
+
<img src="./public/providers/perplexity.png" width="50" alt="Perplexity"/><br/>
|
| 357 |
+
<sub>Perplexity</sub>
|
| 358 |
+
</td>
|
| 359 |
+
</tr>
|
| 360 |
+
<tr>
|
| 361 |
+
<td align="center" width="100">
|
| 362 |
+
<img src="./public/providers/together.png" width="50" alt="Together"/><br/>
|
| 363 |
+
<sub>Together AI</sub>
|
| 364 |
+
</td>
|
| 365 |
+
<td align="center" width="100">
|
| 366 |
+
<img src="./public/providers/fireworks.png" width="50" alt="Fireworks"/><br/>
|
| 367 |
+
<sub>Fireworks</sub>
|
| 368 |
+
</td>
|
| 369 |
+
<td align="center" width="100">
|
| 370 |
+
<img src="./public/providers/cerebras.png" width="50" alt="Cerebras"/><br/>
|
| 371 |
+
<sub>Cerebras</sub>
|
| 372 |
+
</td>
|
| 373 |
+
<td align="center" width="100">
|
| 374 |
+
<img src="./public/providers/cohere.png" width="50" alt="Cohere"/><br/>
|
| 375 |
+
<sub>Cohere</sub>
|
| 376 |
+
</td>
|
| 377 |
+
<td align="center" width="100">
|
| 378 |
+
<img src="./public/providers/nvidia.png" width="50" alt="NVIDIA"/><br/>
|
| 379 |
+
<sub>NVIDIA</sub>
|
| 380 |
+
</td>
|
| 381 |
+
<td align="center" width="100">
|
| 382 |
+
<img src="./public/providers/siliconflow.png" width="50" alt="SiliconFlow"/><br/>
|
| 383 |
+
<sub>SiliconFlow</sub>
|
| 384 |
+
</td>
|
| 385 |
+
</tr>
|
| 386 |
+
</table>
|
| 387 |
+
<p><i>...以及 20+ 更多提供商,包括 Nebius、Chutes、Hyperbolic 和自定义 OpenAI/Anthropic 兼容端点</i></p>
|
| 388 |
+
</div>
|
| 389 |
+
|
| 390 |
+
---
|
| 391 |
+
|
| 392 |
+
## 💡 主要功能
|
| 393 |
+
|
| 394 |
+
| 功能 | 作用 | 为什么重要 |
|
| 395 |
+
|---------|--------------|----------------|
|
| 396 |
+
| 🚀 **RTK Token 节省器**([RTK](https://github.com/rtk-ai/rtk) ⭐40K) | 压缩工具输出(`git diff`、`grep`、`ls`、`tree`...)后再发送给 LLM | 每次请求节省 **20-40% 输入 tokens** |
|
| 397 |
+
| 🪨 **Caveman 模式**([Caveman](https://github.com/JuliusBrussee/caveman) ⭐52K) | 注入 caveman 风格提示词 → LLM 回复简洁,保留技术实质 | 节省 **高达 65% 输出 tokens** |
|
| 398 |
+
| 🎯 **智能三层切换** | 自动路由:订阅 → 低价 → 免费 | 编程永不停歇,零停机时间 |
|
| 399 |
+
| 📊 **实时配额追踪** | 实时 token 计数 + 重置倒计时 | 充分利用订阅价值 |
|
| 400 |
+
| 🔄 **格式转换** | OpenAI ↔ Claude ↔ Gemini ↔ Cursor ↔ Kiro ↔ Vertex | 兼容任何 CLI 工具 |
|
| 401 |
+
| 👥 **多账户支持** | 每个提供商支持多个账户 | 负载均衡 + 冗余备份 |
|
| 402 |
+
| 🔄 **自动 Token 刷新** | OAuth token 自动刷新 | 无需手动重新登录 |
|
| 403 |
+
| 🎨 **自定义组合** | 创建无限模型组合 | 自定义适合你的切换策略 |
|
| 404 |
+
| 📝 **请求日志** | 调试模式下的完整请求/响应日志 | 轻松排查问题 |
|
| 405 |
+
| 💾 **云同步** | 跨设备同步配置 | 处处相同设置 |
|
| 406 |
+
| 📊 **使用分析** | 追踪 tokens、成本、趋势 | 优化开支 |
|
| 407 |
+
| 🌐 **任意部署** | 本地、VPS、Docker、Cloudflare Workers | 灵活部署选项 |
|
| 408 |
+
|
| 409 |
+
<details>
|
| 410 |
+
<summary><b>📖 功能详情</b></summary>
|
| 411 |
+
|
| 412 |
+
### 🚀 RTK Token 节省器
|
| 413 |
+
|
| 414 |
+
工具输出(`git diff`、`grep`、`find`、`ls`、`tree`、日志转储...)通常占用 30-50% 的提示词预算。RTK 在请求到达 LLM 之前检测并应用智能、无损压缩:
|
| 415 |
+
|
| 416 |
+
- **过滤器:** `git-diff`、`git-status`、`grep`、`find`、`ls`、`tree`、`dedup-log`、`smart-truncate`、`read-numbered`、`search-list`
|
| 417 |
+
- **自动检测:** 无需配置 — RTK 检查每个 `tool_result` 的前 1KB,选择合适的过滤器。
|
| 418 |
+
- **安全设计:** 如果过滤器失败、抛出异常或使输出变大,RTK 会静默保留原始文本。错误永远不会中断你的请求。
|
| 419 |
+
- **通用兼容:** 适用于所有格式(OpenAI、Claude、Gemini、Cursor、Kiro、OpenAI Responses),因为它在任何格式转换**之前**运行。
|
| 420 |
+
- **默认开启:** 可随时在控制面板 → 端点设置中切换。
|
| 421 |
+
|
| 422 |
+
```
|
| 423 |
+
不使用 RTK:47K tokens 发送给 LLM
|
| 424 |
+
使用 RTK: 28K tokens 发送给 LLM (节省 40% · 相同上下文 · 相同答案)
|
| 425 |
+
```
|
| 426 |
+
|
| 427 |
+
### 🎯 智能三层切换
|
| 428 |
+
|
| 429 |
+
创建具有自动切换功能的组合:
|
| 430 |
+
|
| 431 |
+
```
|
| 432 |
+
组合:"my-coding-stack"
|
| 433 |
+
1. cc/claude-opus-4-6 (你的订阅)
|
| 434 |
+
2. glm/glm-4.7 (低价备份,$0.6/1M)
|
| 435 |
+
3. if/kimi-k2-thinking (免费备选)
|
| 436 |
+
|
| 437 |
+
→ 配额用完或出错时自动切换
|
| 438 |
+
```
|
| 439 |
+
|
| 440 |
+
### 📊 实时配额追踪
|
| 441 |
+
|
| 442 |
+
- 每个提供商的 token 消耗
|
| 443 |
+
- 重置倒计时(5小时、每日、每周)
|
| 444 |
+
- 付费等级的成本估算
|
| 445 |
+
- 月度支出报告
|
| 446 |
+
|
| 447 |
+
### 🔄 格式转换
|
| 448 |
+
|
| 449 |
+
格式间无缝转换:
|
| 450 |
+
- **OpenAI** ↔ **Claude** ↔ **Gemini** ↔ **Cursor** ↔ **Kiro** ↔ **Vertex** ↔ **Antigravity** ↔ **Ollama** ↔ **OpenAI Responses**
|
| 451 |
+
- 你的 CLI 工具发送 OpenAI 格式 → 9Router 转换 → 提供商接收原生格式
|
| 452 |
+
- 适用于任何支持自定义 OpenAI 端点的工具
|
| 453 |
+
|
| 454 |
+
### 👥 多账户支持
|
| 455 |
+
|
| 456 |
+
- 每个提供商添加多个账户
|
| 457 |
+
- 自动轮询或基于优先级的路由
|
| 458 |
+
- 当一个账户达到配额时切换到下一个
|
| 459 |
+
|
| 460 |
+
### 🔄 自动 Token 刷新
|
| 461 |
+
|
| 462 |
+
- OAuth token 在过期前自动刷新
|
| 463 |
+
- 无需手动重新认证
|
| 464 |
+
- 所有提供商的无缝体验
|
| 465 |
+
|
| 466 |
+
### 🎨 自定义组合
|
| 467 |
+
|
| 468 |
+
- 创建无限模型组合
|
| 469 |
+
- 混合订阅、低价和免费等级
|
| 470 |
+
- 为组合命名以便访问
|
| 471 |
+
- 使用云同步跨设备共享组合
|
| 472 |
+
|
| 473 |
+
### 📝 请求日志
|
| 474 |
+
|
| 475 |
+
- 启用调试模式获取完整请求/响应日志
|
| 476 |
+
- 追踪 API 调用、请求头和载荷
|
| 477 |
+
- 排查集成问题
|
| 478 |
+
- 导出日志进行分析
|
| 479 |
+
|
| 480 |
+
### 💾 云同步
|
| 481 |
+
|
| 482 |
+
- 跨设备同步提供商、组合和设置
|
| 483 |
+
- 自动后台同步
|
| 484 |
+
- 安全加密存储
|
| 485 |
+
- 从任何地方访问你的设置
|
| 486 |
+
|
| 487 |
+
#### 云运行时说明
|
| 488 |
+
|
| 489 |
+
- 生产环境中优先使用服务端云变量:
|
| 490 |
+
- `BASE_URL`(云同步调度程序使用的内部回调 URL)
|
| 491 |
+
- `CLOUD_URL`(云同步端点基础 URL)
|
| 492 |
+
- `NEXT_PUBLIC_BASE_URL` 和 `NEXT_PUBLIC_CLOUD_URL` 仍用于兼容性/UI,但服务端运行时现在优先使用 `BASE_URL`/`CLOUD_URL`。
|
| 493 |
+
- 云同步请求现在使用超时 + 快速失败行为,以避免云 DNS/网络不可用时 UI 挂起。
|
| 494 |
+
|
| 495 |
+
### 📊 使用分���
|
| 496 |
+
|
| 497 |
+
- 追踪每个提供商和模型的 token 使用量
|
| 498 |
+
- 成本估算和支出趋势
|
| 499 |
+
- 月度报告和洞察
|
| 500 |
+
- 优化你的 AI 支出
|
| 501 |
+
|
| 502 |
+
> **💡 重要 - 了解控制面板成本:**
|
| 503 |
+
>
|
| 504 |
+
> 使用分析中显示的"成本"**仅用于追踪和比较目的**。
|
| 505 |
+
> 9Router 本身**永远不会向你收费**。你只直接向提供商付款(如果使用付费服务)。
|
| 506 |
+
>
|
| 507 |
+
> **示例:** 如果你的控制面板显示使用 Kiro 免费模型时"总成本 $290",这代表你如果直接使用付费 API 需要支付的金额。你的实际成本 = **$0**(Kiro 免费等级:约 50 积分/月)。
|
| 508 |
+
>
|
| 509 |
+
> 把它想象成一个"节省追踪器",展示你通过使用免费模型或通过 9Router 路由节省了多少钱!
|
| 510 |
+
|
| 511 |
+
### 🌐 任意部署
|
| 512 |
+
|
| 513 |
+
- 💻 **本地** - 默认,离线可用
|
| 514 |
+
- ☁️ **VPS/云** - 跨设备共享
|
| 515 |
+
- 🐳 **Docker** - 一键部署
|
| 516 |
+
- 🚀 **Cloudflare Workers** - 全球边缘网络
|
| 517 |
+
|
| 518 |
+
</details>
|
| 519 |
+
|
| 520 |
+
---
|
| 521 |
+
|
| 522 |
+
## 💰 价格一览
|
| 523 |
+
|
| 524 |
+
| 等级 | 提供商 | 成本 | 配额重置 | 适用场景 |
|
| 525 |
+
|------|----------|------|-------------|----------|
|
| 526 |
+
| **🚀 TOKEN 节省器** | **RTK(内置)** | **免费** | 始终开启 | **每次请求节省 20-40% tokens** |
|
| 527 |
+
| **💳 订阅** | Claude Code (Pro/Max) | $20-200/月 | 5小时 + 每周 | 已有订阅的用户 |
|
| 528 |
+
| | Codex (Plus/Pro) | $20-200/月 | 5小时 + 每周 | OpenAI 用户 |
|
| 529 |
+
| | GitHub Copilot | $10-19/月 | 每月 | GitHub 用户 |
|
| 530 |
+
| | Cursor IDE | $20/月 | 每月 | Cursor 用户 |
|
| 531 |
+
| **💰 低价** | GLM-5.1 / GLM-4.7 | $0.6/1M | 每日 10AM | 预算备份 |
|
| 532 |
+
| | MiniMax M2.7 | $0.2/1M | 5小时滚动 | 最便宜选项 |
|
| 533 |
+
| | Kimi K2.5 | $9/月固定 | 10M tokens/月 | 可预测成本 |
|
| 534 |
+
| **🆓 免费** | Kiro AI | $0 | 50 积分/月 | Claude 4.5 + GLM-5 + MiniMax 免费(之上为付费档位) |
|
| 535 |
+
| | OpenCode Free | $0 | varies* | 无需认证,自动获取模型(列表会变化) |
|
| 536 |
+
| | Vertex AI | $300 额度 | 新 GCP 账户 | Gemini 3 Pro + DeepSeek + GLM-5(使用 Vertex AI Studio 端点消耗免费额度) |
|
| 537 |
+
|
| 538 |
+
**💡 专业提示:** RTK + Kiro AI + OpenCode Free 组合 = **$0 成本 + 节省 20-40% tokens**!
|
| 539 |
+
|
| 540 |
+
---
|
| 541 |
+
|
| 542 |
+
### 📊 理解 9Router 成本与计费
|
| 543 |
+
|
| 544 |
+
**9Router 计费真相:**
|
| 545 |
+
|
| 546 |
+
✅ **9Router 软件 = 永久免费**(开源,绝不收费)
|
| 547 |
+
✅ **控制面板"成本" = 仅用于显示/追踪**(不是实际账单)
|
| 548 |
+
✅ **你直接向提供商付款**(订阅或 API 费用)
|
| 549 |
+
✅ **免费提供商保持免费**(Kiro 约 50 积分/月、OpenCode Free、Vertex $300 额度 = 在免费额度内 $0)— 注意 iFlow/Qwen/Gemini CLI 免费等级已于 2026 年停止
|
| 550 |
+
❌ **9Router 永不发送发票** 或扣款
|
| 551 |
+
|
| 552 |
+
**成本显示如何工作:**
|
| 553 |
+
|
| 554 |
+
控制面板显示**估算成本**,如同你直接使用付费 API。这**不是计费** — 它是一个比较工具,展示你的节省。
|
| 555 |
+
|
| 556 |
+
**示例场景:**
|
| 557 |
+
```
|
| 558 |
+
控制面板显示:
|
| 559 |
+
• 总请求数:1,662
|
| 560 |
+
• 总 Tokens:47M
|
| 561 |
+
• 显示成本:$290
|
| 562 |
+
|
| 563 |
+
实际检查:
|
| 564 |
+
• 提供商:Kiro(免费等级:约 50 积分/月)
|
| 565 |
+
• 实际支付:$0.00
|
| 566 |
+
• $290 意味着什么:通过使用免费模型节省的金额!
|
| 567 |
+
```
|
| 568 |
+
|
| 569 |
+
**付款规则:**
|
| 570 |
+
- **订阅提供商**(Claude Code、Codex):通过他们的网站直接付款
|
| 571 |
+
- **低价提供商**(GLM、MiniMax):直接付款,9Router 只做路由
|
| 572 |
+
- **免费提供商**(Kiro、OpenCode Free、Vertex):真正的免费,在免费额度内无隐藏费用
|
| 573 |
+
- **9Router**:从不收取任何费用,永远不会
|
| 574 |
+
|
| 575 |
+
---
|
| 576 |
+
|
| 577 |
+
## 🎯 使用场景
|
| 578 |
+
|
| 579 |
+
### 场景 1:"我有 Claude Pro 订阅"
|
| 580 |
+
|
| 581 |
+
**问题:** 配额到期未用完,繁忙编码时遇到速率限制
|
| 582 |
+
|
| 583 |
+
**解决方案:**
|
| 584 |
+
```
|
| 585 |
+
组合:"maximize-claude"
|
| 586 |
+
1. cc/claude-opus-4-7 (充分利用订阅)
|
| 587 |
+
2. glm/glm-5.1 (配额用完时的低价备份)
|
| 588 |
+
3. kr/claude-sonnet-4.5 (免费紧急备选)
|
| 589 |
+
|
| 590 |
+
月成本:$20(订阅)+ ~$5(备份)= $25 总计
|
| 591 |
+
对比:$20 + 遇到限制 = 沮丧
|
| 592 |
+
```
|
| 593 |
+
|
| 594 |
+
### 场景 2:"我想零成本"
|
| 595 |
+
|
| 596 |
+
**问题:** 负担不起订阅,需要可靠的 AI 编码
|
| 597 |
+
|
| 598 |
+
**解决方案:**
|
| 599 |
+
```
|
| 600 |
+
组合:"free-forever"
|
| 601 |
+
1. kr/claude-sonnet-4.5 (通过 Kiro 免费使用 Claude 4.5,约 50 积分/月)
|
| 602 |
+
2. kr/glm-5 (通过 Kiro 免费使用 GLM-5)
|
| 603 |
+
3. oc/<auto> (OpenCode Free,无需认证)
|
| 604 |
+
|
| 605 |
+
月成本:$0
|
| 606 |
+
质量:生产级模型 + RTK 节省 20-40% tokens
|
| 607 |
+
```
|
| 608 |
+
|
| 609 |
+
### 场景 3:"我需要 24/7 编码,不中断"
|
| 610 |
+
|
| 611 |
+
**问题:** 截止日期紧迫,不能承受停机
|
| 612 |
+
|
| 613 |
+
**解决方案:**
|
| 614 |
+
```
|
| 615 |
+
组合:"always-on"
|
| 616 |
+
1. cc/claude-opus-4-7 (最佳质量)
|
| 617 |
+
2. cx/gpt-5.5 (第二个订阅)
|
| 618 |
+
3. glm/glm-5.1 (低价,每日重置)
|
| 619 |
+
4. minimax/MiniMax-M2.7 (最便宜,5小时重置)
|
| 620 |
+
5. kr/claude-sonnet-4.5 (通过 Kiro 免费使用,约 50 积分/月)
|
| 621 |
+
|
| 622 |
+
结果:5 层切换 = 零停机时间
|
| 623 |
+
月成本:$20-200(订阅)+ $10-20(备份)
|
| 624 |
+
```
|
| 625 |
+
|
| 626 |
+
### 场景 4:"我想在 OpenClaw 中使用免费 AI"
|
| 627 |
+
|
| 628 |
+
**问题:** 需要在消息应用(WhatsApp、Telegram、Slack...)中使用 AI 助手,完全免费
|
| 629 |
+
|
| 630 |
+
**解决方案:**
|
| 631 |
+
```
|
| 632 |
+
组合:"openclaw-free"
|
| 633 |
+
1. kr/claude-sonnet-4.5 (Claude 4.5 免费)
|
| 634 |
+
2. kr/glm-5 (GLM-5 免费)
|
| 635 |
+
3. kr/MiniMax-M2.5 (MiniMax 免费)
|
| 636 |
+
|
| 637 |
+
月成本:$0
|
| 638 |
+
访问方式:WhatsApp、Telegram、Slack、Discord、iMessage、Signal...
|
| 639 |
+
```
|
| 640 |
+
|
| 641 |
+
---
|
| 642 |
+
|
| 643 |
+
## ❓ 常见问题
|
| 644 |
+
|
| 645 |
+
<details>
|
| 646 |
+
<summary><b>📊 为什么我的控制面板显示高成本?</b></summary>
|
| 647 |
+
|
| 648 |
+
控制面板追踪你的 token 使用情况,并显示**估算成本**,如同你直接使用付费 API。这**不是实际计费** — 它是一个参考,展示你通过使用免费模型或通过 9Router 路由现有订阅节省了多少钱。
|
| 649 |
+
|
| 650 |
+
**示例:**
|
| 651 |
+
- **控制面板显示:** "$290 总成本"
|
| 652 |
+
- **实际情况:** 你在使用 Kiro 免费模型(约 50 积分/月)
|
| 653 |
+
- **你的实际成本:** **$0.00**
|
| 654 |
+
- **$290 的含义:** 你通过使用免费模型而不是付费 API **节省**的金额!
|
| 655 |
+
|
| 656 |
+
成本显示是一个"节省追踪器",帮助你了解使用模式和优化机会。
|
| 657 |
+
|
| 658 |
+
</details>
|
| 659 |
+
|
| 660 |
+
<details>
|
| 661 |
+
<summary><b>💳 9Router 会扣我的钱吗?</b></summary>
|
| 662 |
+
|
| 663 |
+
**不会。** 9Router 是在你自己的电脑上运行的开源软件。它永远不会向你收取任何费用。
|
| 664 |
+
|
| 665 |
+
**你只需支付:**
|
| 666 |
+
- ✅ **订阅提供商**(Claude Code $20/月、Codex $20-200/月)→ 在他们的网站上直接付款
|
| 667 |
+
- ✅ **低价提供商**(GLM、MiniMax)→ 直接付款,9Router 只是路由你的请求
|
| 668 |
+
- ❌ **9Router 本身** → **永不收费,永远不会**
|
| 669 |
+
|
| 670 |
+
9Router 是一个本地代理/路由器。它没有你的信用卡,不能发送发票,也没有计费系统。它是完全免费的软件。
|
| 671 |
+
|
| 672 |
+
</details>
|
| 673 |
+
|
| 674 |
+
<details>
|
| 675 |
+
<summary><b>🆓 免费提供商真的是无限量的吗?</b></summary>
|
| 676 |
+
|
| 677 |
+
**基本上是!** 当前的免费提供商(Kiro、OpenCode Free、Vertex)是真正的免费,但免费等级有上限:
|
| 678 |
+
|
| 679 |
+
这些是各公司提供的免费服务:
|
| 680 |
+
- **Kiro AI**:通过 AWS Builder ID / Google / GitHub OAuth 使用,免费等级约**每月 50 积分**(新账户前 30 天另加 500 试用积分)。之上提供付费档位。
|
| 681 |
+
- **OpenCode Free**:无认证直连代理,模型从 `opencode.ai/zen/v1/models` 自动获取。免费模型列表会随时间变化(部分模型仅限时免费)— 可能随时变更。
|
| 682 |
+
- **Vertex AI**:新 Google Cloud 账户可获得 $300 免费额度(90 天)。自 2026 年 3 月起 Gemini API 端点不再消耗这些额度 — 请改用 **Vertex AI Studio** 端点。
|
| 683 |
+
|
| 684 |
+
9Router 只是路由你的请求到它们 — 没有"陷阱"或未来的计费。它们是真正的免费服务,9Router 让它们易于使用并支持切换。
|
| 685 |
+
|
| 686 |
+
**已停止的免费等级(不再推荐):**
|
| 687 |
+
- ❌ **iFlow**:曾是免费无限量,现在改为付费(2026)
|
| 688 |
+
- ❌ **Qwen Code**:阿里巴巴于 2026-04-15 完全停止免费 OAuth 等级
|
| 689 |
+
- ❌ **Gemini CLI**:Google 已于 2026-06-18 完全停止服务(由闭源的 Antigravity CLI 取代)。已停止 — 请勿使用。
|
| 690 |
+
|
| 691 |
+
</details>
|
| 692 |
+
|
| 693 |
+
<details>
|
| 694 |
+
<summary><b>💰 如何最小化我的实际 AI 成本?</b></summary>
|
| 695 |
+
|
| 696 |
+
**免费优先策略:**
|
| 697 |
+
|
| 698 |
+
1. **从 100% 免费组合开始:**
|
| 699 |
+
```
|
| 700 |
+
1. kr/glm-5 (通过 Kiro 免费使用 GLM-5,约 50 积分/月)
|
| 701 |
+
2. OpenCode Free 模型(无认证,自动获取)
|
| 702 |
+
3. Vertex AI Gemini 3 Pro(使用 Vertex AI Studio 端点 + $300 额度)
|
| 703 |
+
```
|
| 704 |
+
**成本:$0/月**(在 Kiro 免费积分上限内;OpenCode/Vertex 受各自免费等级限制)
|
| 705 |
+
|
| 706 |
+
2. **仅在需要时添加低价备份:**
|
| 707 |
+
```
|
| 708 |
+
4. glm/glm-4.7 ($0.6/1M tokens)
|
| 709 |
+
```
|
| 710 |
+
**额外成本:只为实际使用的部分付费**
|
| 711 |
+
|
| 712 |
+
3. **最后使用订阅提供商:**
|
| 713 |
+
- 仅当你已有订阅时
|
| 714 |
+
- 9Router 通过配额追踪帮助最大化其价值
|
| 715 |
+
|
| 716 |
+
**结果:** 大多数用户可以仅使用免费等级以 $0/月运行!
|
| 717 |
+
|
| 718 |
+
</details>
|
| 719 |
+
|
| 720 |
+
<details>
|
| 721 |
+
<summary><b>📈 如果我的使用量突然激增怎么办?</b></summary>
|
| 722 |
+
|
| 723 |
+
9Router 的智能切换可以防止意外费用:
|
| 724 |
+
|
| 725 |
+
**场景:** 你正在进行编码冲刺,用尽了配额
|
| 726 |
+
|
| 727 |
+
**没有 9Router:**
|
| 728 |
+
- ❌ 达到速率限制 → 工作停止 → 沮丧
|
| 729 |
+
- ❌ 或者:不慎累积大量 API 账单
|
| 730 |
+
|
| 731 |
+
**有 9Router:**
|
| 732 |
+
- ✅ 订阅达到限制 → 自动切换到低价等级
|
| 733 |
+
- ✅ 低价等级变得昂贵 → 自动切换到免费等级
|
| 734 |
+
- ✅ 编程永不停歇 → 可预测的成本
|
| 735 |
+
|
| 736 |
+
**你掌控一切:** 在控制面板中设置每个提供商的支出限制,9Router 会遵守它们。
|
| 737 |
+
|
| 738 |
+
</details>
|
| 739 |
+
|
| 740 |
+
---
|
| 741 |
+
|
| 742 |
+
## 📖 设置指南
|
| 743 |
+
|
| 744 |
+
<details>
|
| 745 |
+
<summary><b>🔐 订阅提供商(充分利用价值)</b></summary>
|
| 746 |
+
|
| 747 |
+
### Claude Code (Pro/Max)
|
| 748 |
+
|
| 749 |
+
```bash
|
| 750 |
+
控制面板 → 提供商 → 连接 Claude Code
|
| 751 |
+
→ OAuth 登录 → 自动 token 刷新
|
| 752 |
+
→ 5小时 + 每周配额追踪
|
| 753 |
+
|
| 754 |
+
模型:
|
| 755 |
+
cc/claude-opus-4-7
|
| 756 |
+
cc/claude-opus-4-6
|
| 757 |
+
cc/claude-sonnet-4-6
|
| 758 |
+
cc/claude-haiku-4-5-20251001
|
| 759 |
+
```
|
| 760 |
+
|
| 761 |
+
**专业提示:** 复杂任务使用 Opus,追求速度使用 Sonnet。9Router 按模型追踪配额!
|
| 762 |
+
|
| 763 |
+
### OpenAI Codex (Plus/Pro)
|
| 764 |
+
|
| 765 |
+
```bash
|
| 766 |
+
控制面板 → 提供商 → 连接 Codex
|
| 767 |
+
→ OAuth 登录(端口 1455)
|
| 768 |
+
→ 5小时 + 每周重置
|
| 769 |
+
|
| 770 |
+
模型:
|
| 771 |
+
cx/gpt-5.5
|
| 772 |
+
cx/gpt-5.4
|
| 773 |
+
cx/gpt-5.3-codex
|
| 774 |
+
cx/gpt-5.2-codex
|
| 775 |
+
```
|
| 776 |
+
|
| 777 |
+
### GitHub Copilot
|
| 778 |
+
|
| 779 |
+
```bash
|
| 780 |
+
控制面板 → 提供商 → 连接 GitHub
|
| 781 |
+
→ 通过 GitHub 进行 OAuth
|
| 782 |
+
→ 每月重置(每月 1 日)
|
| 783 |
+
|
| 784 |
+
模型:
|
| 785 |
+
gh/gpt-5.4
|
| 786 |
+
gh/claude-opus-4.7
|
| 787 |
+
gh/claude-sonnet-4.6
|
| 788 |
+
gh/gemini-3.1-pro-preview
|
| 789 |
+
gh/grok-code-fast-1
|
| 790 |
+
```
|
| 791 |
+
|
| 792 |
+
### Cursor IDE
|
| 793 |
+
|
| 794 |
+
```bash
|
| 795 |
+
控制面板 → 提供商 → 连接 Cursor
|
| 796 |
+
→ OAuth 登录
|
| 797 |
+
→ 每月订阅
|
| 798 |
+
|
| 799 |
+
模型:
|
| 800 |
+
cu/claude-4.6-opus-max
|
| 801 |
+
cu/claude-4.5-sonnet-thinking
|
| 802 |
+
cu/gpt-5.3-codex
|
| 803 |
+
```
|
| 804 |
+
|
| 805 |
+
</details>
|
| 806 |
+
|
| 807 |
+
<details>
|
| 808 |
+
<summary><b>💰 低价提供商(备份)</b></summary>
|
| 809 |
+
|
| 810 |
+
### GLM-5.1 / GLM-4.7(每日重置,$0.6/1M)
|
| 811 |
+
|
| 812 |
+
1. 注册:[Zhipu AI](https://open.bigmodel.cn/)
|
| 813 |
+
2. 从编程计划获取 API key
|
| 814 |
+
3. 控制面板 → 添加 API Key:
|
| 815 |
+
- 提供商:`glm`
|
| 816 |
+
- API Key:`your-key`
|
| 817 |
+
|
| 818 |
+
**使用:** `glm/glm-5.1`、`glm/glm-5`、`glm/glm-4.7`
|
| 819 |
+
|
| 820 |
+
**专业提示:** 编程计划提供 3 倍配额,成本仅为 1/7!每日 10:00 AM 重置。
|
| 821 |
+
|
| 822 |
+
### MiniMax M2.7(5小时重置,$0.20/1M)
|
| 823 |
+
|
| 824 |
+
1. 注册:[MiniMax](https://www.minimax.io/)
|
| 825 |
+
2. 获取 API key
|
| 826 |
+
3. 控制面板 → 添加 API Key
|
| 827 |
+
|
| 828 |
+
**使用:** `minimax/MiniMax-M2.7`、`minimax/MiniMax-M2.5`
|
| 829 |
+
|
| 830 |
+
**专业提示:** 长上下文(1M tokens)的最便宜选项!
|
| 831 |
+
|
| 832 |
+
### Kimi K2.5($9/月固定)
|
| 833 |
+
|
| 834 |
+
1. 订阅:[Moonshot AI](https://platform.moonshot.ai/)
|
| 835 |
+
2. 获取 API key
|
| 836 |
+
3. 控制面板 → 添加 API Key
|
| 837 |
+
|
| 838 |
+
**使用:** `kimi/kimi-k2.5`、`kimi/kimi-k2.5-thinking`
|
| 839 |
+
|
| 840 |
+
**专业提示:** 每月 $9 固定费用获得 10M tokens = 实际成本 $0.90/1M!
|
| 841 |
+
|
| 842 |
+
</details>
|
| 843 |
+
|
| 844 |
+
<details>
|
| 845 |
+
<summary><b>🆓 免费提供商(推荐)</b></summary>
|
| 846 |
+
|
| 847 |
+
### Kiro AI(Claude 4.5 + GLM-5 + MiniMax 免费)
|
| 848 |
+
|
| 849 |
+
```bash
|
| 850 |
+
控制面板 → 连接 Kiro
|
| 851 |
+
→ AWS Builder ID、AWS IAM Identity Center、Google 或 GitHub
|
| 852 |
+
→ 无限量使用
|
| 853 |
+
|
| 854 |
+
模型:
|
| 855 |
+
kr/claude-sonnet-4.5
|
| 856 |
+
kr/claude-haiku-4.5
|
| 857 |
+
kr/glm-5
|
| 858 |
+
kr/MiniMax-M2.5
|
| 859 |
+
kr/qwen3-coder-next
|
| 860 |
+
kr/deepseek-3.2
|
| 861 |
+
```
|
| 862 |
+
|
| 863 |
+
**专业提示:** Claude 最佳免费选项。无需 API key,无需付款,完全无限量。
|
| 864 |
+
|
| 865 |
+
### OpenCode Free(无需认证,自动获取模型)
|
| 866 |
+
|
| 867 |
+
```bash
|
| 868 |
+
控制面板 → 连接 OpenCode Free
|
| 869 |
+
→ 无需登录(直连代理)
|
| 870 |
+
→ 模型从 opencode.ai/zen/v1/models 自动获取
|
| 871 |
+
```
|
| 872 |
+
|
| 873 |
+
**专业提示:** 最快的设置。连接后即可开始编码。
|
| 874 |
+
|
| 875 |
+
### Vertex AI(新 GCP 账户 $300 免费额度)
|
| 876 |
+
|
| 877 |
+
```bash
|
| 878 |
+
控制面板 → 连接 Vertex AI
|
| 879 |
+
→ 上传 Google Cloud 服务账户 JSON
|
| 880 |
+
→ 在你的 GCP 项目中启用 Vertex AI API
|
| 881 |
+
|
| 882 |
+
模型:
|
| 883 |
+
vertex/gemini-3.1-pro-preview
|
| 884 |
+
vertex/gemini-3-flash-preview
|
| 885 |
+
vertex/gemini-2.5-flash
|
| 886 |
+
|
| 887 |
+
Vertex 合作伙伴(通过 Vertex 提供 Anthropic / DeepSeek / GLM / Qwen):
|
| 888 |
+
vertex-partner/glm-5-maas
|
| 889 |
+
vertex-partner/deepseek-v3.2-maas
|
| 890 |
+
vertex-partner/qwen3-next-80b-a3b-thinking-maas
|
| 891 |
+
```
|
| 892 |
+
|
| 893 |
+
**专业提示:** 新 Google Cloud 账户可获得 90 天内 $300 免费额度。足够日常编码使用。
|
| 894 |
+
|
| 895 |
+
</details>
|
| 896 |
+
|
| 897 |
+
<details>
|
| 898 |
+
<summary><b>🎨 创建组合</b></summary>
|
| 899 |
+
|
| 900 |
+
### 示例 1:充分利用订阅 → 低价备份
|
| 901 |
+
|
| 902 |
+
```
|
| 903 |
+
控制面板 → 组合 → 创建新组合
|
| 904 |
+
|
| 905 |
+
名称:premium-coding
|
| 906 |
+
模型:
|
| 907 |
+
1. cc/claude-opus-4-7 (订阅主用)
|
| 908 |
+
2. glm/glm-5.1 (低价备份,$0.6/1M)
|
| 909 |
+
3. minimax/MiniMax-M2.7 (最便宜的备选,$0.20/1M)
|
| 910 |
+
|
| 911 |
+
在 CLI 中使用:premium-coding
|
| 912 |
+
|
| 913 |
+
月度成本示例(100M tokens):
|
| 914 |
+
80M 通过 Claude(订阅):$0 额外费用
|
| 915 |
+
15M 通过 GLM:$9
|
| 916 |
+
5M 通过 MiniMax:$1
|
| 917 |
+
总计:$10 + 你的订阅费用
|
| 918 |
+
```
|
| 919 |
+
|
| 920 |
+
### 示例 2:仅免费(零成本)
|
| 921 |
+
|
| 922 |
+
```
|
| 923 |
+
名称:free-combo
|
| 924 |
+
模型:
|
| 925 |
+
1. kr/claude-sonnet-4.5 (通过 Kiro 免费使用 Claude 4.5,约 50 积分/月)
|
| 926 |
+
2. kr/glm-5 (通过 Kiro 免费使用 GLM-5)
|
| 927 |
+
3. vertex/gemini-3.1-pro-preview ($300 免费额度)
|
| 928 |
+
|
| 929 |
+
成本:通过 RTK 永久 $0(+ 节省 20-40% tokens)!
|
| 930 |
+
```
|
| 931 |
+
|
| 932 |
+
</details>
|
| 933 |
+
|
| 934 |
+
<details>
|
| 935 |
+
<summary><b>🔧 CLI 集成</b></summary>
|
| 936 |
+
|
| 937 |
+
### Cursor IDE
|
| 938 |
+
|
| 939 |
+
```
|
| 940 |
+
设置 → 模型 → 高级:
|
| 941 |
+
OpenAI API Base URL:http://localhost:20128/v1
|
| 942 |
+
OpenAI API Key:[来自 9router 控制面板]
|
| 943 |
+
Model:cc/claude-opus-4-7
|
| 944 |
+
```
|
| 945 |
+
|
| 946 |
+
或使用组合:`premium-coding`
|
| 947 |
+
|
| 948 |
+
### Claude Code
|
| 949 |
+
|
| 950 |
+
编辑 `~/.claude/config.json`:
|
| 951 |
+
|
| 952 |
+
```json
|
| 953 |
+
{
|
| 954 |
+
"anthropic_api_base": "http://localhost:20128/v1",
|
| 955 |
+
"anthropic_api_key": "your-9router-api-key"
|
| 956 |
+
}
|
| 957 |
+
```
|
| 958 |
+
|
| 959 |
+
### Codex CLI
|
| 960 |
+
|
| 961 |
+
```bash
|
| 962 |
+
export OPENAI_BASE_URL="http://localhost:20128"
|
| 963 |
+
export OPENAI_API_KEY="your-9router-api-key"
|
| 964 |
+
|
| 965 |
+
codex "your prompt"
|
| 966 |
+
```
|
| 967 |
+
|
| 968 |
+
### OpenClaw
|
| 969 |
+
|
| 970 |
+
**选项 1 — 控制面板(推荐):**
|
| 971 |
+
|
| 972 |
+
```
|
| 973 |
+
控制面板 → CLI 工具 → OpenClaw → 选择模型 → 应用
|
| 974 |
+
```
|
| 975 |
+
|
| 976 |
+
**选项 2 — 手动:** 编辑 `~/.openclaw/openclaw.json`:
|
| 977 |
+
|
| 978 |
+
```json
|
| 979 |
+
{
|
| 980 |
+
"agents": {
|
| 981 |
+
"defaults": {
|
| 982 |
+
"model": {
|
| 983 |
+
"primary": "9router/kr/claude-sonnet-4.5"
|
| 984 |
+
}
|
| 985 |
+
}
|
| 986 |
+
},
|
| 987 |
+
"models": {
|
| 988 |
+
"providers": {
|
| 989 |
+
"9router": {
|
| 990 |
+
"baseUrl": "http://127.0.0.1:20128/v1",
|
| 991 |
+
"apiKey": "sk_9router",
|
| 992 |
+
"api": "openai-completions",
|
| 993 |
+
"models": [
|
| 994 |
+
{
|
| 995 |
+
"id": "kr/claude-sonnet-4.5",
|
| 996 |
+
"name": "Claude Sonnet 4.5 (Kiro Free)"
|
| 997 |
+
}
|
| 998 |
+
]
|
| 999 |
+
}
|
| 1000 |
+
}
|
| 1001 |
+
}
|
| 1002 |
+
}
|
| 1003 |
+
```
|
| 1004 |
+
|
| 1005 |
+
> **注意:** OpenClaw 仅适用于本地 9Router。使用 `127.0.0.1` 而不是 `localhost` 以避免 IPv6 解析问题。
|
| 1006 |
+
|
| 1007 |
+
### Cline / Continue / RooCode
|
| 1008 |
+
|
| 1009 |
+
```
|
| 1010 |
+
Provider:OpenAI 兼容
|
| 1011 |
+
Base URL:http://localhost:20128/v1
|
| 1012 |
+
API Key:[来自控制面板]
|
| 1013 |
+
Model:cc/claude-opus-4-7
|
| 1014 |
+
```
|
| 1015 |
+
|
| 1016 |
+
</details>
|
| 1017 |
+
|
| 1018 |
+
<details>
|
| 1019 |
+
<summary><b>🚀 部署</b></summary>
|
| 1020 |
+
|
| 1021 |
+
### VPS 部署
|
| 1022 |
+
|
| 1023 |
+
```bash
|
| 1024 |
+
# 克隆并安装
|
| 1025 |
+
git clone https://github.com/decolua/9router.git
|
| 1026 |
+
cd 9router
|
| 1027 |
+
npm install
|
| 1028 |
+
npm run build
|
| 1029 |
+
|
| 1030 |
+
# 配置
|
| 1031 |
+
export JWT_SECRET="your-secure-secret-change-this"
|
| 1032 |
+
export INITIAL_PASSWORD="your-password"
|
| 1033 |
+
export DATA_DIR="/var/lib/9router"
|
| 1034 |
+
export PORT="20128"
|
| 1035 |
+
export HOSTNAME="0.0.0.0"
|
| 1036 |
+
export NODE_ENV="production"
|
| 1037 |
+
export NEXT_PUBLIC_BASE_URL="http://localhost:20128"
|
| 1038 |
+
export NEXT_PUBLIC_CLOUD_URL="https://9router.com"
|
| 1039 |
+
export API_KEY_SECRET="endpoint-proxy-api-key-secret"
|
| 1040 |
+
export MACHINE_ID_SALT="endpoint-proxy-salt"
|
| 1041 |
+
|
| 1042 |
+
# 启动
|
| 1043 |
+
npm run start
|
| 1044 |
+
|
| 1045 |
+
# 或使用 PM2
|
| 1046 |
+
npm install -g pm2
|
| 1047 |
+
pm2 start npm --name 9router -- start
|
| 1048 |
+
pm2 save
|
| 1049 |
+
pm2 startup
|
| 1050 |
+
```
|
| 1051 |
+
|
| 1052 |
+
### Docker
|
| 1053 |
+
|
| 1054 |
+
```bash
|
| 1055 |
+
# 构建镜像(从仓库根目录)
|
| 1056 |
+
docker build -t 9router .
|
| 1057 |
+
|
| 1058 |
+
# 运行容器(当前设置使用的命令)
|
| 1059 |
+
docker run -d \
|
| 1060 |
+
--name 9router \
|
| 1061 |
+
-p 20128:20128 \
|
| 1062 |
+
--env-file /root/dev/9router/.env \
|
| 1063 |
+
-v 9router-data:/app/data \
|
| 1064 |
+
-v 9router-usage:/root/.9router \
|
| 1065 |
+
9router
|
| 1066 |
+
```
|
| 1067 |
+
|
| 1068 |
+
便携命令(如果你已经在仓库根目录):
|
| 1069 |
+
|
| 1070 |
+
```bash
|
| 1071 |
+
docker run -d \
|
| 1072 |
+
--name 9router \
|
| 1073 |
+
-p 20128:20128 \
|
| 1074 |
+
--env-file ./.env \
|
| 1075 |
+
-v 9router-data:/app/data \
|
| 1076 |
+
-v 9router-usage:/root/.9router \
|
| 1077 |
+
9router
|
| 1078 |
+
```
|
| 1079 |
+
|
| 1080 |
+
容器默认值:
|
| 1081 |
+
- `PORT=20128`
|
| 1082 |
+
- `HOSTNAME=0.0.0.0`
|
| 1083 |
+
|
| 1084 |
+
常用命令:
|
| 1085 |
+
|
| 1086 |
+
```bash
|
| 1087 |
+
docker logs -f 9router
|
| 1088 |
+
docker restart 9router
|
| 1089 |
+
docker stop 9router && docker rm 9router
|
| 1090 |
+
```
|
| 1091 |
+
|
| 1092 |
+
### 环境变量
|
| 1093 |
+
|
| 1094 |
+
| 变量 | 默认值 | 描述 |
|
| 1095 |
+
|----------|---------|-------------|
|
| 1096 |
+
| `JWT_SECRET` | 自动生成(`~/.9router/jwt-secret`) | 用于控制面板 auth cookie 的 JWT 签名密钥(设置可在多实例间共享) |
|
| 1097 |
+
| `INITIAL_PASSWORD` | `123456` | 当没有保存的哈希时首次登录的密码 |
|
| 1098 |
+
| `DATA_DIR` | `~/.9router` | 主应用数据库位置(`db.json`) |
|
| 1099 |
+
| `PORT` | 框架默认值 | 服务端口(示例中为 `20128`) |
|
| 1100 |
+
| `HOSTNAME` | 框架默认值 | 绑定主机(Docker 默认为 `0.0.0.0`) |
|
| 1101 |
+
| `NODE_ENV` | 运行时默认值 | 设置 `production` 用于部署 |
|
| 1102 |
+
| `BASE_URL` | `http://localhost:20128` | 云同步任务使用的服务端内部基础 URL |
|
| 1103 |
+
| `CLOUD_URL` | `https://9router.com` | 服务端云同步端点基础 URL |
|
| 1104 |
+
| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | 向后兼容/公开基础 URL(服务端运行时优先使用 `BASE_URL`) |
|
| 1105 |
+
| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | 向后兼容/公开云 URL(服务端运行时优先使用 `CLOUD_URL`) |
|
| 1106 |
+
| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | 生成 API key 的 HMAC 密钥 |
|
| 1107 |
+
| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | 稳定机器 ID 哈希的盐值 |
|
| 1108 |
+
| `ENABLE_REQUEST_LOGS` | `false` | 在 `logs/` 下启用请求/响应日志 |
|
| 1109 |
+
| `AUTH_COOKIE_SECURE` | `false` | 强制 `Secure` auth cookie(在 HTTPS 反向代理后面设置为 `true`) |
|
| 1110 |
+
| `REQUIRE_API_KEY` | `false` | 在 `/v1/*` 路由上强制使用 Bearer API key(面向互联网部署时推荐) |
|
| 1111 |
+
| `HTTP_PROXY`、`HTTPS_PROXY`、`ALL_PROXY`、`NO_PROXY` | 空 | 用于上游提供商调用的可选出站代理 |
|
| 1112 |
+
|
| 1113 |
+
注意:
|
| 1114 |
+
- 也支持小写代理变量:`http_proxy`、`https_proxy`、`all_proxy`、`no_proxy`。
|
| 1115 |
+
- `.env` 不会打包到 Docker 镜像中(`.dockerignore`);使用 `--env-file` 或 `-e` 注入运行时配置。
|
| 1116 |
+
- 在 Windows 上,`APPDATA` 可用于本地存储路径解析。
|
| 1117 |
+
- `INSTANCE_NAME` 出现在较旧的文档/环境变量模板中,但当前运行时未使用。
|
| 1118 |
+
|
| 1119 |
+
### 运行时文件和存储
|
| 1120 |
+
|
| 1121 |
+
- 主应用状态:`${DATA_DIR}/db.json`(提供商、组合、别名、密钥、设置),由 `src/lib/localDb.js` 管理。
|
| 1122 |
+
- 使用历史和日志:`${DATA_DIR}/usage.json` 和 `${DATA_DIR}/log.txt`,由 `src/lib/usageDb.js` 管理。
|
| 1123 |
+
- 可选的请求/翻译器日志:`ENABLE_REQUEST_LOGS=true` 时位于 `<repo>/logs/...`。
|
| 1124 |
+
- `${DATA_DIR}` 和 `~/.9router` 在 Docker 容器中解析到同一位置 — 符号链接 `/root/.9router -> /app/data` 在构建时创建。
|
| 1125 |
+
|
| 1126 |
+
</details>
|
| 1127 |
+
|
| 1128 |
+
---
|
| 1129 |
+
|
| 1130 |
+
## 📊 可用模型
|
| 1131 |
+
|
| 1132 |
+
<details>
|
| 1133 |
+
<summary><b>查看所有可用模型</b></summary>
|
| 1134 |
+
|
| 1135 |
+
**Claude Code(`cc/`)** - Pro/Max:
|
| 1136 |
+
- `cc/claude-opus-4-7`
|
| 1137 |
+
- `cc/claude-opus-4-6`
|
| 1138 |
+
- `cc/claude-sonnet-4-6`
|
| 1139 |
+
- `cc/claude-sonnet-4-5-20250929`
|
| 1140 |
+
- `cc/claude-haiku-4-5-20251001`
|
| 1141 |
+
|
| 1142 |
+
**Codex(`cx/`)** - Plus/Pro:
|
| 1143 |
+
- `cx/gpt-5.5`
|
| 1144 |
+
- `cx/gpt-5.4`
|
| 1145 |
+
- `cx/gpt-5.3-codex`
|
| 1146 |
+
- `cx/gpt-5.2-codex`
|
| 1147 |
+
- `cx/gpt-5.1-codex-max`
|
| 1148 |
+
|
| 1149 |
+
**GitHub Copilot(`gh/`)**:
|
| 1150 |
+
- `gh/gpt-5.4`
|
| 1151 |
+
- `gh/claude-opus-4.7`
|
| 1152 |
+
- `gh/claude-sonnet-4.6`
|
| 1153 |
+
- `gh/gemini-3.1-pro-preview`
|
| 1154 |
+
- `gh/grok-code-fast-1`
|
| 1155 |
+
|
| 1156 |
+
**Cursor(`cu/`)** - 订阅:
|
| 1157 |
+
- `cu/claude-4.6-opus-max`
|
| 1158 |
+
- `cu/claude-4.5-sonnet-thinking`
|
| 1159 |
+
- `cu/gpt-5.3-codex`
|
| 1160 |
+
- `cu/kimi-k2.5`
|
| 1161 |
+
|
| 1162 |
+
**GLM(`glm/`)** - $0.6/1M:
|
| 1163 |
+
- `glm/glm-5.1`
|
| 1164 |
+
- `glm/glm-5`
|
| 1165 |
+
- `glm/glm-4.7`
|
| 1166 |
+
|
| 1167 |
+
**MiniMax(`minimax/`)** - $0.2/1M:
|
| 1168 |
+
- `minimax/MiniMax-M2.7`
|
| 1169 |
+
- `minimax/MiniMax-M2.5`
|
| 1170 |
+
|
| 1171 |
+
**Kimi(`kimi/`)** - $9/月固定:
|
| 1172 |
+
- `kimi/kimi-k2.5`
|
| 1173 |
+
- `kimi/kimi-k2.5-thinking`
|
| 1174 |
+
|
| 1175 |
+
**Kiro(`kr/`)** - 免费(约 50 积分/月,之上为付费档位):
|
| 1176 |
+
- `kr/claude-sonnet-4.5`
|
| 1177 |
+
- `kr/claude-haiku-4.5`
|
| 1178 |
+
- `kr/glm-5`
|
| 1179 |
+
- `kr/MiniMax-M2.5`
|
| 1180 |
+
- `kr/qwen3-coder-next`
|
| 1181 |
+
- `kr/deepseek-3.2`
|
| 1182 |
+
|
| 1183 |
+
**OpenCode Free(`oc/`)** - 免费无需认证:
|
| 1184 |
+
- 从 `opencode.ai/zen/v1/models` 自动获取
|
| 1185 |
+
|
| 1186 |
+
**Vertex AI(`vertex/`)** - $300 免费额度:
|
| 1187 |
+
- `vertex/gemini-3.1-pro-preview`
|
| 1188 |
+
- `vertex/gemini-3-flash-preview`
|
| 1189 |
+
- `vertex/gemini-2.5-flash`
|
| 1190 |
+
- `vertex-partner/glm-5-maas`
|
| 1191 |
+
- `vertex-partner/deepseek-v3.2-maas`
|
| 1192 |
+
|
| 1193 |
+
</details>
|
| 1194 |
+
|
| 1195 |
+
---
|
| 1196 |
+
|
| 1197 |
+
## 🐛 故障排除
|
| 1198 |
+
|
| 1199 |
+
**"语言模型未提供消息"**
|
| 1200 |
+
- 提供商配额耗尽 → 检查控制面板配额追踪器
|
| 1201 |
+
- 解决方案:使用组合切换或切换到更便宜的等级
|
| 1202 |
+
|
| 1203 |
+
**速率限制**
|
| 1204 |
+
- 订阅配额用完 → 切换到 GLM/MiniMax
|
| 1205 |
+
- 添加组合:`cc/claude-opus-4-7 → glm/glm-5.1 → kr/claude-sonnet-4.5`
|
| 1206 |
+
|
| 1207 |
+
**OAuth token 已过期**
|
| 1208 |
+
- 9Router 自动刷新
|
| 1209 |
+
- 如果问题持续:控制面板 → 提供商 → 重新连接
|
| 1210 |
+
|
| 1211 |
+
**高成本**
|
| 1212 |
+
- 在控制面板 → 端点设置中启用 RTK(默认开启,节省 20-40% tokens)
|
| 1213 |
+
- 在控制面板中检查使用统计
|
| 1214 |
+
- 将主模型切换到 GLM/MiniMax
|
| 1215 |
+
- 对于非关键任务使用免费等级(Kiro、OpenCode Free、Vertex)
|
| 1216 |
+
|
| 1217 |
+
**控制面板在错误端口打开**
|
| 1218 |
+
- 设置 `PORT=20128` 和 `NEXT_PUBLIC_BASE_URL=http://localhost:20128`
|
| 1219 |
+
|
| 1220 |
+
**首次登录不工作**
|
| 1221 |
+
- 检查 `.env` 中的 `INITIAL_PASSWORD`
|
| 1222 |
+
- 如果未设置,回退密码是 `123456`
|
| 1223 |
+
|
| 1224 |
+
**`logs/` 下没有请求日志**
|
| 1225 |
+
- 设置 `ENABLE_REQUEST_LOGS=true`
|
| 1226 |
+
|
| 1227 |
+
---
|
| 1228 |
+
|
| 1229 |
+
## 🛠️ 技术栈
|
| 1230 |
+
|
| 1231 |
+
- **运行时**:Node.js 20+
|
| 1232 |
+
- **框架**:Next.js 16
|
| 1233 |
+
- **UI**:React 19 + Tailwind CSS 4
|
| 1234 |
+
- **数据库**:LowDB(基于 JSON 文件)
|
| 1235 |
+
- **流式传输**:Server-Sent Events (SSE)
|
| 1236 |
+
- **认证**:OAuth 2.0 (PKCE) + JWT + API Keys
|
| 1237 |
+
|
| 1238 |
+
---
|
| 1239 |
+
|
| 1240 |
+
## 📝 API 参考
|
| 1241 |
+
|
| 1242 |
+
### 聊天补全
|
| 1243 |
+
|
| 1244 |
+
```bash
|
| 1245 |
+
POST http://localhost:20128/v1/chat/completions
|
| 1246 |
+
Authorization: Bearer your-api-key
|
| 1247 |
+
Content-Type: application/json
|
| 1248 |
+
|
| 1249 |
+
{
|
| 1250 |
+
"model": "cc/claude-opus-4-6",
|
| 1251 |
+
"messages": [
|
| 1252 |
+
{"role": "user", "content": "Write a function to..."}
|
| 1253 |
+
],
|
| 1254 |
+
"stream": true
|
| 1255 |
+
}
|
| 1256 |
+
```
|
| 1257 |
+
|
| 1258 |
+
### 列出模型
|
| 1259 |
+
|
| 1260 |
+
```bash
|
| 1261 |
+
GET http://localhost:20128/v1/models
|
| 1262 |
+
Authorization: Bearer your-api-key
|
| 1263 |
+
|
| 1264 |
+
→ 以 OpenAI 格式返回所有模型和组合
|
| 1265 |
+
```
|
| 1266 |
+
|
| 1267 |
+
## 📧 支持
|
| 1268 |
+
|
| 1269 |
+
- **网站**:[9router.com](https://9router.com)
|
| 1270 |
+
- **GitHub**:[github.com/decolua/9router](https://github.com/decolua/9router)
|
| 1271 |
+
- **问题**:[github.com/decolua/9router/issues](https://github.com/decolua/9router/issues)
|
| 1272 |
+
|
| 1273 |
+
---
|
| 1274 |
+
|
| 1275 |
+
## 👥 贡献者
|
| 1276 |
+
|
| 1277 |
+
感谢所有帮助改进 9Router 的贡献者!
|
| 1278 |
+
|
| 1279 |
+
[](https://github.com/decolua/9router/graphs/contributors)
|
| 1280 |
+
|
| 1281 |
+
---
|
| 1282 |
+
|
| 1283 |
+
## 📊 Star 图表
|
| 1284 |
+
|
| 1285 |
+
[](https://starchart.cc/decolua/9router)
|
| 1286 |
+
|
| 1287 |
+
|
| 1288 |
+
|
| 1289 |
+
## 🔀 分支
|
| 1290 |
+
|
| 1291 |
+
**[OmniRoute](https://github.com/diegosouzapw/OmniRoute)** — 9Router 的全功能 TypeScript 分支。增加了 36+ 提供商、4 层自动切换、多模态 API(图像、嵌入、音频、TTS)、断路器、语义缓存、LLM 评估和精美的控制面板。368+ 单元测试。可通过 npm 和 Docker 使用。
|
| 1292 |
+
|
| 1293 |
+
---
|
| 1294 |
+
|
| 1295 |
+
## 🙏 致谢
|
| 1296 |
+
|
| 1297 |
+
站在巨人的肩膀上构建:
|
| 1298 |
+
|
| 1299 |
+
- **CLIProxyAPI** — 启发了这个 JavaScript 移植的原始 Go 实现。
|
| 1300 |
+
- **[RTK](https://github.com/rtk-ai/rtk)**  — Rust token 节省器。9Router 将其压缩管道移植到 JS → 每次请求 **减少 20-40% 输入 tokens**。
|
| 1301 |
+
- **[Caveman](https://github.com/JuliusBrussee/caveman)**  by **[@JuliusBrussee](https://github.com/JuliusBrussee)** — 病毒式传播的 *"为什么用很多 token 当少的 token 就能搞定"*。9Router 适配其提示词 → **减少 65% 输出 tokens**。
|
| 1302 |
+
|
| 1303 |
+
非常感谢这些作者 — 没有他们的工作,9Router 的 token 节省功能就不会存在。在 GitHub 上给他们加星!
|
| 1304 |
+
|
| 1305 |
+
---
|
| 1306 |
+
|
| 1307 |
+
## 📄 许可证
|
| 1308 |
+
|
| 1309 |
+
MIT 许可证 — 详见 [LICENSE](LICENSE)。
|
| 1310 |
+
|
| 1311 |
+
---
|
| 1312 |
+
|
| 1313 |
+
<div align="center">
|
| 1314 |
+
<sub>用 ❤️ 为 24/7 编程的开发者构建</sub>
|
| 1315 |
+
</div>
|
README_HUGGINGFACE.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Run 9Router on Hugging Face Spaces
|
| 2 |
+
|
| 3 |
+
## What this fork adds
|
| 4 |
+
|
| 5 |
+
- `hf-server.js` — launch wrapper that respects Hugging Face Spaces' default `PORT=7860` and `HOSTNAME=0.0.0.0`.
|
| 6 |
+
- `package.json` adds `build:hf` / `start:hf` scripts.
|
| 7 |
+
- `Dockerfile` now defaults to `PORT=7860` and includes the SQLite WASM dependency needed for serverless persistence.
|
| 8 |
+
|
| 9 |
+
## Deploy
|
| 10 |
+
|
| 11 |
+
1. Create a new Hugging Face Space.
|
| 12 |
+
2. Set the Space SDK to **Docker**.
|
| 13 |
+
3. Push this repo to the Space.
|
| 14 |
+
4. In Space Settings → Variables, set at least:
|
| 15 |
+
- `JWT_SECRET`
|
| 16 |
+
- `INITIAL_PASSWORD`
|
| 17 |
+
- `API_KEY_SECRET`
|
| 18 |
+
- `MACHINE_ID_SALT`
|
| 19 |
+
5. The build will run `npm install` + `npm run build` automatically. On success, HF serves the app on the Space URL at port 7860.
|
| 20 |
+
|
| 21 |
+
## Notes
|
| 22 |
+
|
| 23 |
+
- Data persists in the container filesystem for the life of the Space. For long-term persistence, attach an HF dataset or volume if your Space plan supports it, or sync via the built-in cloud sync if you run 9router.com.
|
| 24 |
+
- Headroom is not bundled by default. If you need token saving, run Headroom externally and set `HEADROOM_URL`.
|
| 25 |
+
- Default admin password is `123456`. Override `INITIAL_PASSWORD` immediately.
|
captain-definition
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schemaVersion": 2,
|
| 3 |
+
"dockerfilePath": "./Dockerfile"
|
| 4 |
+
}
|
custom-server.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const http = require("http");
|
| 2 |
+
const path = require("path");
|
| 3 |
+
const fs = require("fs");
|
| 4 |
+
const crypto = require("crypto");
|
| 5 |
+
const { pathToFileURL } = require("url");
|
| 6 |
+
|
| 7 |
+
const origCreate = http.createServer.bind(http);
|
| 8 |
+
|
| 9 |
+
// Per-process secret proving x-9r-real-ip was stamped below rather than sent by the client.
|
| 10 |
+
// A bare `next start` / `next dev` never loads this file, so it cannot produce a matching
|
| 11 |
+
// header even though the env var is inherited by child processes. Named like x-9r-cli-token
|
| 12 |
+
// so the request-detail header sanitizer redacts it too.
|
| 13 |
+
const PEER_TOKEN = crypto.randomBytes(24).toString("hex");
|
| 14 |
+
process.env.NINEROUTER_PEER_TOKEN = PEER_TOKEN;
|
| 15 |
+
|
| 16 |
+
let backgroundRefreshStarted = false;
|
| 17 |
+
|
| 18 |
+
function startBackgroundTokenRefreshFromCustomServer() {
|
| 19 |
+
if (backgroundRefreshStarted) return;
|
| 20 |
+
backgroundRefreshStarted = true;
|
| 21 |
+
// Prefer source path (repo / standalone that still has src). Fail-open if missing
|
| 22 |
+
// — initializeApp also starts the same scheduler when the Next app boots.
|
| 23 |
+
const modPath = path.join(__dirname, "src", "sse", "services", "backgroundTokenRefresh.js");
|
| 24 |
+
import(pathToFileURL(modPath).href)
|
| 25 |
+
.then((m) => {
|
| 26 |
+
try {
|
| 27 |
+
m.startBackgroundTokenRefresh();
|
| 28 |
+
} catch (e) {
|
| 29 |
+
console.error("[BackgroundTokenRefresh] start failed:", e && e.message ? e.message : e);
|
| 30 |
+
}
|
| 31 |
+
const stop = () => {
|
| 32 |
+
try {
|
| 33 |
+
m.stopBackgroundTokenRefresh();
|
| 34 |
+
} catch {
|
| 35 |
+
/* ignore */
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
process.once("SIGINT", stop);
|
| 39 |
+
process.once("SIGTERM", stop);
|
| 40 |
+
})
|
| 41 |
+
.catch((e) => {
|
| 42 |
+
// Expected in published CLI standalone (src/ not on disk). App bootstrap covers it.
|
| 43 |
+
if (process.env.DEBUG_BACKGROUND_TOKEN_REFRESH) {
|
| 44 |
+
console.error("[BackgroundTokenRefresh] import failed:", e && e.message ? e.message : e);
|
| 45 |
+
}
|
| 46 |
+
});
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
// Wrap Next standalone HTTP server: derive client IP from the TCP socket
|
| 50 |
+
// (unspoofable) and strip client-supplied forwarding headers so downstream
|
| 51 |
+
// rate-limiting keys on the real peer address instead of attacker-controlled XFF.
|
| 52 |
+
http.createServer = (...args) => {
|
| 53 |
+
const handler = args.find((a) => typeof a === "function");
|
| 54 |
+
const rest = args.filter((a) => typeof a !== "function");
|
| 55 |
+
if (!handler) return origCreate(...args);
|
| 56 |
+
const wrapped = (req, res) => {
|
| 57 |
+
const socketIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
|
| 58 |
+
const xff = req.headers["x-forwarded-for"];
|
| 59 |
+
const xRealIp = req.headers["x-real-ip"];
|
| 60 |
+
const viaProxy = !!(xff || xRealIp);
|
| 61 |
+
const isLoopbackProxy = socketIp === "127.0.0.1" || socketIp === "::1" || socketIp === "::ffff:127.0.0.1";
|
| 62 |
+
// Trust forwarding headers only when the TCP peer is a local reverse proxy.
|
| 63 |
+
// Direct/public sockets remain keyed by the unspoofable peer address.
|
| 64 |
+
const proxyIp = xRealIp || (xff ? String(xff).split(",")[0].trim() : "");
|
| 65 |
+
const ip = isLoopbackProxy && proxyIp ? proxyIp : socketIp;
|
| 66 |
+
delete req.headers["x-9r-real-ip"];
|
| 67 |
+
delete req.headers["x-forwarded-for"];
|
| 68 |
+
delete req.headers["x-9r-via-proxy"];
|
| 69 |
+
delete req.headers["x-9r-peer-token"];
|
| 70 |
+
req.headers["x-9r-real-ip"] = ip;
|
| 71 |
+
req.headers["x-9r-peer-token"] = PEER_TOKEN;
|
| 72 |
+
if (viaProxy) req.headers["x-9r-via-proxy"] = "1";
|
| 73 |
+
return handler(req, res);
|
| 74 |
+
};
|
| 75 |
+
const server = origCreate(...rest, wrapped);
|
| 76 |
+
server.once("listening", () => {
|
| 77 |
+
startBackgroundTokenRefreshFromCustomServer();
|
| 78 |
+
});
|
| 79 |
+
const origEmit = server.emit;
|
| 80 |
+
// JBR 25 sends h2c upgrades that the HTTP/1.1 server would otherwise close.
|
| 81 |
+
server.emit = function (event, ...eventArgs) {
|
| 82 |
+
const [req, socket, head] = eventArgs;
|
| 83 |
+
if (event !== "upgrade" || String(req.headers.upgrade || "").toLowerCase() !== "h2c") {
|
| 84 |
+
return origEmit.call(this, event, ...eventArgs);
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
const contentLength = Number(req.headers["content-length"] || 0);
|
| 88 |
+
if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
|
| 89 |
+
socket.destroy();
|
| 90 |
+
return true;
|
| 91 |
+
}
|
| 92 |
+
const chunks = [head];
|
| 93 |
+
let received = head.length;
|
| 94 |
+
const serve = () => {
|
| 95 |
+
// Replay the upgraded request through the existing HTTP/1.1 handler.
|
| 96 |
+
const replay = new http.IncomingMessage(socket);
|
| 97 |
+
Object.assign(replay, { method: req.method, url: req.url, headers: req.headers, complete: true });
|
| 98 |
+
if (received) replay.push(Buffer.concat(chunks, received).subarray(0, contentLength));
|
| 99 |
+
replay.push(null);
|
| 100 |
+
const res = new http.ServerResponse(replay);
|
| 101 |
+
res.shouldKeepAlive = false;
|
| 102 |
+
res.assignSocket(socket);
|
| 103 |
+
res.once("finish", () => socket.end());
|
| 104 |
+
Promise.resolve().then(() => wrapped(replay, res)).catch((error) => {
|
| 105 |
+
console.error("Failed to downgrade h2c request", error);
|
| 106 |
+
socket.destroy();
|
| 107 |
+
});
|
| 108 |
+
};
|
| 109 |
+
if (received >= contentLength) serve();
|
| 110 |
+
else {
|
| 111 |
+
socket.on("data", function readBody(chunk) {
|
| 112 |
+
chunks.push(chunk);
|
| 113 |
+
received += chunk.length;
|
| 114 |
+
if (received < contentLength) return;
|
| 115 |
+
socket.off("data", readBody);
|
| 116 |
+
serve();
|
| 117 |
+
});
|
| 118 |
+
socket.resume();
|
| 119 |
+
}
|
| 120 |
+
delete req.headers.upgrade;
|
| 121 |
+
delete req.headers["http2-settings"];
|
| 122 |
+
req.headers.connection = "close";
|
| 123 |
+
return true;
|
| 124 |
+
};
|
| 125 |
+
return server;
|
| 126 |
+
};
|
| 127 |
+
|
| 128 |
+
if (require.main === module) {
|
| 129 |
+
const standalone = path.join(__dirname, "server.js");
|
| 130 |
+
if (fs.existsSync(standalone)) {
|
| 131 |
+
require(standalone);
|
| 132 |
+
} else {
|
| 133 |
+
// Repo checkout has no standalone build next to us. `next start` builds its HTTP
|
| 134 |
+
// server in-process, so the wrapper above still sanitizes every request.
|
| 135 |
+
const nextBin = require.resolve("next/dist/bin/next");
|
| 136 |
+
process.argv = [process.argv[0], nextBin, "start", ...process.argv.slice(2)];
|
| 137 |
+
require(nextBin);
|
| 138 |
+
}
|
| 139 |
+
}
|
docker-compose.yml
CHANGED
|
@@ -1,178 +1,30 @@
|
|
| 1 |
-
# ──────────────────────────────────────────────────────────────────────
|
| 2 |
-
# OmniRoute — Docker Compose
|
| 3 |
-
# ──────────────────────────────────────────────────────────────────────
|
| 4 |
-
#
|
| 5 |
-
# Profiles:
|
| 6 |
-
# base → minimal image, no CLI tools
|
| 7 |
-
# web → runner-web (+Chromium/Playwright) for web-cookie providers
|
| 8 |
-
# cli → CLIs installed inside the container (portable)
|
| 9 |
-
# host → runner-base + host-mounted CLI binaries (Linux-first)
|
| 10 |
-
# cliproxyapi → CLIProxyAPI sidecar on port 8317
|
| 11 |
-
#
|
| 12 |
-
# Usage:
|
| 13 |
-
# docker compose --profile base up -d
|
| 14 |
-
# docker compose --profile web up -d # gemini-web / claude-web / claude-turnstile
|
| 15 |
-
# docker compose --profile cli up -d
|
| 16 |
-
# docker compose --profile host up -d
|
| 17 |
-
# docker compose --profile cliproxyapi up -d
|
| 18 |
-
# docker compose --profile cli --profile cliproxyapi up -d
|
| 19 |
-
#
|
| 20 |
-
# Before first run, copy .env.example → .env and edit your secrets.
|
| 21 |
-
# ──────────────────────────────────────────────────────────────────────
|
| 22 |
-
|
| 23 |
-
x-common: &common
|
| 24 |
-
restart: unless-stopped
|
| 25 |
-
stop_grace_period: 40s
|
| 26 |
-
env_file: .env
|
| 27 |
-
environment:
|
| 28 |
-
- DATA_DIR=/app/data # Must match the volume mount below
|
| 29 |
-
- PORT=${PORT:-20128}
|
| 30 |
-
- DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
|
| 31 |
-
- API_PORT=${API_PORT:-20129}
|
| 32 |
-
- API_HOST=${API_HOST:-0.0.0.0}
|
| 33 |
-
- REDIS_URL=${REDIS_URL:-redis://redis:6379}
|
| 34 |
-
volumes:
|
| 35 |
-
- ./data:/app/data
|
| 36 |
-
healthcheck:
|
| 37 |
-
test: ["CMD", "node", "healthcheck.mjs"]
|
| 38 |
-
interval: 30s
|
| 39 |
-
timeout: 5s
|
| 40 |
-
retries: 3
|
| 41 |
-
start_period: 15s
|
| 42 |
-
|
| 43 |
services:
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
restart: unless-stopped
|
| 49 |
ports:
|
| 50 |
-
- "
|
| 51 |
volumes:
|
| 52 |
-
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
test: ["CMD", "redis-cli", "ping"]
|
| 56 |
-
interval: 10s
|
| 57 |
-
timeout: 5s
|
| 58 |
-
retries: 3
|
| 59 |
-
|
| 60 |
-
# ── Profile: base (minimal, no CLI tools) ──────────────────────────
|
| 61 |
-
omniroute-base:
|
| 62 |
-
<<: *common
|
| 63 |
-
container_name: omniroute
|
| 64 |
-
build:
|
| 65 |
-
context: .
|
| 66 |
-
target: runner-base
|
| 67 |
-
image: omniroute:base
|
| 68 |
-
ports:
|
| 69 |
-
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
| 70 |
-
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
| 71 |
-
profiles:
|
| 72 |
-
- base
|
| 73 |
-
|
| 74 |
-
# ── Profile: web (runner-web + Chromium/Playwright) ────────────────
|
| 75 |
-
# Required for web-cookie providers (gemini-web, claude-web, claude-turnstile).
|
| 76 |
-
# The default `base` image ships without Chromium, so those providers fail
|
| 77 |
-
# with "Executable doesn't exist at .../ms-playwright/chromium..." (#2832).
|
| 78 |
-
omniroute-web:
|
| 79 |
-
<<: *common
|
| 80 |
-
container_name: omniroute
|
| 81 |
-
build:
|
| 82 |
-
context: .
|
| 83 |
-
target: runner-web
|
| 84 |
-
image: omniroute:web
|
| 85 |
-
ports:
|
| 86 |
-
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
| 87 |
-
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
| 88 |
-
profiles:
|
| 89 |
-
- web
|
| 90 |
-
|
| 91 |
-
# ── Profile: cli (CLIs installed inside container) ─────────────────
|
| 92 |
-
omniroute-cli:
|
| 93 |
-
<<: *common
|
| 94 |
-
container_name: omniroute
|
| 95 |
-
build:
|
| 96 |
-
context: .
|
| 97 |
-
target: runner-cli
|
| 98 |
-
image: omniroute:cli
|
| 99 |
-
ports:
|
| 100 |
-
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
| 101 |
-
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
| 102 |
-
volumes:
|
| 103 |
-
- ./data:/app/data
|
| 104 |
-
- /var/run/docker.sock:/var/run/docker.sock
|
| 105 |
-
- /usr/libexec/docker/cli-plugins:/usr/libexec/docker/cli-plugins:ro
|
| 106 |
-
- ${AUTO_UPDATE_HOST_REPO_DIR:-.}:/workspace/omniroute:rw
|
| 107 |
-
profiles:
|
| 108 |
-
- cli
|
| 109 |
-
|
| 110 |
-
# ── Profile: host (host-mounted CLI binaries, Linux-first) ────────
|
| 111 |
-
omniroute-host:
|
| 112 |
-
<<: *common
|
| 113 |
-
container_name: omniroute
|
| 114 |
-
build:
|
| 115 |
-
context: .
|
| 116 |
-
target: runner-base
|
| 117 |
-
image: omniroute:base
|
| 118 |
-
ports:
|
| 119 |
-
- "${DASHBOARD_PORT:-20128}:${DASHBOARD_PORT:-20128}"
|
| 120 |
-
- "${API_PORT:-20129}:${API_PORT:-20129}"
|
| 121 |
environment:
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
# - CLI_CONTINUE_BIN=cn
|
| 135 |
-
volumes:
|
| 136 |
-
- ./data:/app/data
|
| 137 |
-
# ── Host binary mounts (read-only) ──
|
| 138 |
-
# Adjust paths below to match YOUR host system.
|
| 139 |
-
- ~/.local/bin:/host-local/bin:ro
|
| 140 |
-
# Node global binaries (adjust node version path)
|
| 141 |
-
# - ~/.nvm/versions/node/v24.14.1/bin:/host-node/bin:ro
|
| 142 |
-
# ── Host config mounts (read-write) ──
|
| 143 |
-
- ~/.codex:/host-home/.codex:rw
|
| 144 |
-
- ~/.claude:/host-home/.claude:rw
|
| 145 |
-
- ~/.factory:/host-home/.factory:rw
|
| 146 |
-
- ~/.openclaw:/host-home/.openclaw:rw
|
| 147 |
-
- ~/.cursor:/host-home/.cursor:rw
|
| 148 |
-
- ~/.config/cursor:/host-home/.config/cursor:rw
|
| 149 |
-
profiles:
|
| 150 |
-
- host
|
| 151 |
-
|
| 152 |
-
# ── Profile: cliproxyapi (CLIProxyAPI as sidecar) ─────────────────
|
| 153 |
-
cliproxyapi:
|
| 154 |
-
container_name: cliproxyapi
|
| 155 |
-
image: ghcr.io/router-for-me/cliproxyapi:v6.9.7
|
| 156 |
-
restart: unless-stopped
|
| 157 |
ports:
|
| 158 |
-
- "
|
| 159 |
-
volumes:
|
| 160 |
-
- cliproxyapi-data:/root/.cli-proxy-api
|
| 161 |
-
environment:
|
| 162 |
-
- PORT=${CLIPROXYAPI_PORT:-8317}
|
| 163 |
-
- HOST=0.0.0.0
|
| 164 |
-
healthcheck:
|
| 165 |
-
test:
|
| 166 |
-
["CMD", "wget", "--spider", "-q", "http://127.0.0.1:${CLIPROXYAPI_PORT:-8317}/v1/models"]
|
| 167 |
-
interval: 30s
|
| 168 |
-
timeout: 5s
|
| 169 |
-
retries: 3
|
| 170 |
-
start_period: 10s
|
| 171 |
-
profiles:
|
| 172 |
-
- cliproxyapi
|
| 173 |
|
| 174 |
volumes:
|
| 175 |
-
|
| 176 |
-
name:
|
| 177 |
-
redis-data:
|
| 178 |
-
name: omniroute-redis-data
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
services:
|
| 2 |
+
9router:
|
| 3 |
+
image: decolua/9router:latest
|
| 4 |
+
container_name: 9router
|
| 5 |
+
restart: always
|
|
|
|
| 6 |
ports:
|
| 7 |
+
- "20128:20128"
|
| 8 |
volumes:
|
| 9 |
+
- 9router-data:/app/data
|
| 10 |
+
env_file:
|
| 11 |
+
- .env
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
environment:
|
| 13 |
+
DATA_DIR: /app/data
|
| 14 |
+
PORT: "20128"
|
| 15 |
+
HOSTNAME: "0.0.0.0"
|
| 16 |
+
NODE_ENV: production
|
| 17 |
+
HEADROOM_URL: http://headroom:8787
|
| 18 |
+
depends_on:
|
| 19 |
+
- headroom
|
| 20 |
+
|
| 21 |
+
headroom:
|
| 22 |
+
image: ghcr.io/chopratejas/headroom:latest
|
| 23 |
+
container_name: headroom
|
| 24 |
+
restart: always
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
ports:
|
| 26 |
+
- "8787:8787"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
|
| 28 |
volumes:
|
| 29 |
+
9router-data:
|
| 30 |
+
name: 9router-data
|
|
|
|
|
|
eslint.config.mjs
CHANGED
|
@@ -1,102 +1,16 @@
|
|
|
|
|
| 1 |
import nextVitals from "eslint-config-next/core-web-vitals";
|
| 2 |
-
import tseslint from "typescript-eslint";
|
| 3 |
|
| 4 |
-
|
| 5 |
-
const eslintConfig = [
|
| 6 |
...nextVitals,
|
| 7 |
-
//
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
paths: [
|
| 17 |
-
{
|
| 18 |
-
name: "prop-types",
|
| 19 |
-
message: "PropTypes are deprecated. Use TypeScript types/interfaces instead.",
|
| 20 |
-
},
|
| 21 |
-
],
|
| 22 |
-
},
|
| 23 |
-
],
|
| 24 |
-
},
|
| 25 |
-
},
|
| 26 |
-
// i18n: ham toLowerCase().includes() arama pattern'ini engelle
|
| 27 |
-
// (Türkçe İ/ı karakterlerini bozar — matchesSearch kullanılmalı).
|
| 28 |
-
// "warn" (error değil): kuralın eklendiği anda kod tabanında zaten bu pattern'i
|
| 29 |
-
// kullanan ~19 satır var; aşamalı temizlik için uyarı seviyesinde tutuluyor
|
| 30 |
-
// (proje politikası: 0 error, warning'ler tolere edilir).
|
| 31 |
-
{
|
| 32 |
-
files: ["src/app/**/*.{ts,tsx}", "src/components/**/*.{ts,tsx}"],
|
| 33 |
-
rules: {
|
| 34 |
-
"no-restricted-syntax": [
|
| 35 |
-
"warn",
|
| 36 |
-
{
|
| 37 |
-
selector:
|
| 38 |
-
"CallExpression[callee.property.name='includes'][callee.object.callee.property.name='toLowerCase']",
|
| 39 |
-
message:
|
| 40 |
-
"Türkçe-güvenli arama için matchesSearch() kullan (@/shared/utils/turkishText). Ham toLowerCase().includes() İ/ı karakterlerini bozar.",
|
| 41 |
-
},
|
| 42 |
-
],
|
| 43 |
-
},
|
| 44 |
-
},
|
| 45 |
-
// Relaxed rules for open-sse and tests (incremental adoption)
|
| 46 |
-
{
|
| 47 |
-
files: ["open-sse/**/*.ts", "tests/**/*.mjs", "tests/**/*.ts"],
|
| 48 |
-
plugins: {
|
| 49 |
-
"@typescript-eslint": tseslint.plugin,
|
| 50 |
-
},
|
| 51 |
-
rules: {
|
| 52 |
-
"@typescript-eslint/no-explicit-any": "warn",
|
| 53 |
-
"@next/next/no-assign-module-variable": "off",
|
| 54 |
-
"react-hooks/rules-of-hooks": "off",
|
| 55 |
-
},
|
| 56 |
-
},
|
| 57 |
-
// Global ignores — keep ESLint scoped to source files only
|
| 58 |
-
{
|
| 59 |
-
ignores: [
|
| 60 |
-
// Next.js build output (distDir now .build/next; keep .next for legacy)
|
| 61 |
-
".next/**",
|
| 62 |
-
".build/**",
|
| 63 |
-
"src/.next/**",
|
| 64 |
-
"out/**",
|
| 65 |
-
"build/**",
|
| 66 |
-
"dist/**",
|
| 67 |
-
"coverage/**",
|
| 68 |
-
"next-env.d.ts",
|
| 69 |
-
// Scripts and binaries
|
| 70 |
-
"scripts/**",
|
| 71 |
-
"bin/**",
|
| 72 |
-
// Dependencies
|
| 73 |
-
"node_modules/**",
|
| 74 |
-
".worktrees/**",
|
| 75 |
-
// Nested git worktrees created by review/resolve skills live under
|
| 76 |
-
// .claude/ (gitignored). They hold other sessions' in-progress work and
|
| 77 |
-
// their files move mid-scan, so never lint them from the main checkout.
|
| 78 |
-
".claude/**",
|
| 79 |
-
".omnivscodeagent/**",
|
| 80 |
-
// VS Code extension and its large test fixtures
|
| 81 |
-
"vscode-extension/**",
|
| 82 |
-
"_references/**",
|
| 83 |
-
"_mono_repo/**",
|
| 84 |
-
// Electron app
|
| 85 |
-
"electron/**",
|
| 86 |
-
// Docs
|
| 87 |
-
"docs/**",
|
| 88 |
-
// Open-SSE compiled/bundled output
|
| 89 |
-
"open-sse/mcp-server/dist/**",
|
| 90 |
-
// Playwright test output
|
| 91 |
-
"playwright-report/**",
|
| 92 |
-
"test-results/**",
|
| 93 |
-
// Legacy app/ and QA backup dirs (renamed to dist/ in Layer 1)
|
| 94 |
-
"app/**",
|
| 95 |
-
"app.__qa_backup/**",
|
| 96 |
-
// CLI package copy directory
|
| 97 |
-
"clipr/**",
|
| 98 |
-
],
|
| 99 |
-
},
|
| 100 |
-
];
|
| 101 |
|
| 102 |
export default eslintConfig;
|
|
|
|
| 1 |
+
import { defineConfig, globalIgnores } from "eslint/config";
|
| 2 |
import nextVitals from "eslint-config-next/core-web-vitals";
|
|
|
|
| 3 |
|
| 4 |
+
const eslintConfig = defineConfig([
|
|
|
|
| 5 |
...nextVitals,
|
| 6 |
+
// Override default ignores of eslint-config-next.
|
| 7 |
+
globalIgnores([
|
| 8 |
+
// Default ignores of eslint-config-next:
|
| 9 |
+
".next/**",
|
| 10 |
+
"out/**",
|
| 11 |
+
"build/**",
|
| 12 |
+
"next-env.d.ts",
|
| 13 |
+
]),
|
| 14 |
+
]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
|
| 16 |
export default eslintConfig;
|
hf-server.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const http = require("http");
|
| 2 |
+
const path = require("path");
|
| 3 |
+
const fs = require("fs");
|
| 4 |
+
const crypto = require("crypto");
|
| 5 |
+
const { pathToFileURL } = require("url");
|
| 6 |
+
|
| 7 |
+
const origCreate = http.createServer.bind(http);
|
| 8 |
+
|
| 9 |
+
// HuggingFace Spaces container default
|
| 10 |
+
const PORT = Number(process.env.PORT || 7860);
|
| 11 |
+
const HOSTNAME = process.env.HOSTNAME || "0.0.0.0";
|
| 12 |
+
|
| 13 |
+
// Per-process secret proving x-9r-real-ip was stamped below rather than sent by the client.
|
| 14 |
+
const PEER_TOKEN = crypto.randomBytes(24).toString("hex");
|
| 15 |
+
process.env.NINEROUTER_PEER_TOKEN = PEER_TOKEN;
|
| 16 |
+
|
| 17 |
+
let backgroundRefreshStarted = false;
|
| 18 |
+
|
| 19 |
+
function startBackgroundTokenRefreshFromHfServer() {
|
| 20 |
+
if (backgroundRefreshStarted) return;
|
| 21 |
+
backgroundRefreshStarted = true;
|
| 22 |
+
|
| 23 |
+
const modPath = path.join(__dirname, "src", "sse", "services", "backgroundTokenRefresh.js");
|
| 24 |
+
import(pathToFileURL(modPath).href)
|
| 25 |
+
.then((m) => {
|
| 26 |
+
try {
|
| 27 |
+
m.startBackgroundTokenRefresh();
|
| 28 |
+
} catch (e) {
|
| 29 |
+
console.error("[BackgroundTokenRefresh] start failed:", e && e.message ? e.message : e);
|
| 30 |
+
}
|
| 31 |
+
const stop = () => {
|
| 32 |
+
try {
|
| 33 |
+
m.stopBackgroundTokenRefresh();
|
| 34 |
+
} catch {
|
| 35 |
+
/* ignore */
|
| 36 |
+
}
|
| 37 |
+
};
|
| 38 |
+
process.once("SIGINT", stop);
|
| 39 |
+
process.once("SIGTERM", stop);
|
| 40 |
+
})
|
| 41 |
+
.catch((e) => {
|
| 42 |
+
console.error("[BackgroundTokenRefresh] import failed:", e && e.message ? e.message : e);
|
| 43 |
+
});
|
| 44 |
+
}
|
| 45 |
+
|
| 46 |
+
http.createServer = (...args) => {
|
| 47 |
+
const handler = args.find((a) => typeof a === "function");
|
| 48 |
+
const rest = args.filter((a) => typeof a !== "function");
|
| 49 |
+
if (!handler) return origCreate(...args);
|
| 50 |
+
const wrapped = (req, res) => {
|
| 51 |
+
const socketIp = req.socket && req.socket.remoteAddress ? req.socket.remoteAddress : "";
|
| 52 |
+
const xff = req.headers["x-forwarded-for"];
|
| 53 |
+
const xRealIp = req.headers["x-real-ip"];
|
| 54 |
+
const viaProxy = !!(xff || xRealIp);
|
| 55 |
+
const isLoopbackProxy = socketIp === "127.0.0.1" || socketIp === "::1" || socketIp === "::ffff:127.0.0.1";
|
| 56 |
+
const proxyIp = xRealIp || (xff ? String(xff).split(",")[0].trim() : "");
|
| 57 |
+
const ip = isLoopbackProxy && proxyIp ? proxyIp : socketIp;
|
| 58 |
+
delete req.headers["x-9r-real-ip"];
|
| 59 |
+
delete req.headers["x-forwarded-for"];
|
| 60 |
+
delete req.headers["x-9r-via-proxy"];
|
| 61 |
+
delete req.headers["x-9r-peer-token"];
|
| 62 |
+
req.headers["x-9r-real-ip"] = ip;
|
| 63 |
+
req.headers["x-9r-peer-token"] = PEER_TOKEN;
|
| 64 |
+
if (viaProxy) req.headers["x-9r-via-proxy"] = "1";
|
| 65 |
+
return handler(req, res);
|
| 66 |
+
};
|
| 67 |
+
const server = origCreate(...rest, wrapped);
|
| 68 |
+
server.once("listening", () => {
|
| 69 |
+
startBackgroundTokenRefreshFromHfServer();
|
| 70 |
+
});
|
| 71 |
+
const origEmit = server.emit;
|
| 72 |
+
server.emit = function (event, ...eventArgs) {
|
| 73 |
+
const [req, socket, head] = eventArgs;
|
| 74 |
+
if (event !== "upgrade" || String(req.headers.upgrade || "").toLowerCase() !== "h2c") {
|
| 75 |
+
return origEmit.call(this, event, ...eventArgs);
|
| 76 |
+
}
|
| 77 |
+
const contentLength = Number(req.headers["content-length"] || 0);
|
| 78 |
+
if (!Number.isSafeInteger(contentLength) || contentLength < 0) {
|
| 79 |
+
socket.destroy();
|
| 80 |
+
return true;
|
| 81 |
+
}
|
| 82 |
+
const chunks = [head];
|
| 83 |
+
let received = head.length;
|
| 84 |
+
const serve = () => {
|
| 85 |
+
const replay = new http.IncomingMessage(socket);
|
| 86 |
+
Object.assign(replay, { method: req.method, url: req.url, headers: req.headers, complete: true });
|
| 87 |
+
if (received) replay.push(Buffer.concat(chunks, received).subarray(0, contentLength));
|
| 88 |
+
replay.push(null);
|
| 89 |
+
const res = new http.ServerResponse(replay);
|
| 90 |
+
res.shouldKeepAlive = false;
|
| 91 |
+
res.assignSocket(socket);
|
| 92 |
+
res.once("finish", () => socket.end());
|
| 93 |
+
Promise.resolve().then(() => wrapped(replay, res)).catch((error) => {
|
| 94 |
+
console.error("Failed to downgrade h2c request", error);
|
| 95 |
+
socket.destroy();
|
| 96 |
+
});
|
| 97 |
+
};
|
| 98 |
+
if (received >= contentLength) serve();
|
| 99 |
+
else {
|
| 100 |
+
socket.on("data", function readBody(chunk) {
|
| 101 |
+
chunks.push(chunk);
|
| 102 |
+
received += chunk.length;
|
| 103 |
+
if (received < contentLength) return;
|
| 104 |
+
socket.off("data", readBody);
|
| 105 |
+
serve();
|
| 106 |
+
});
|
| 107 |
+
socket.resume();
|
| 108 |
+
}
|
| 109 |
+
delete req.headers.upgrade;
|
| 110 |
+
delete req.headers["http2-settings"];
|
| 111 |
+
req.headers.connection = "close";
|
| 112 |
+
return true;
|
| 113 |
+
};
|
| 114 |
+
return server;
|
| 115 |
+
};
|
| 116 |
+
|
| 117 |
+
if (require.main === module) {
|
| 118 |
+
const standalone = path.join(__dirname, "server.js");
|
| 119 |
+
if (fs.existsSync(standalone)) {
|
| 120 |
+
require(standalone);
|
| 121 |
+
} else {
|
| 122 |
+
const nextBin = require.resolve("next/dist/bin/next");
|
| 123 |
+
process.argv = [process.argv[0], nextBin, "start", "--port", String(PORT), "--hostname", HOSTNAME, ...process.argv.slice(2)];
|
| 124 |
+
require(nextBin);
|
| 125 |
+
}
|
| 126 |
+
}
|
jsconfig.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"baseUrl": ".",
|
| 4 |
+
"paths": {
|
| 5 |
+
"@/*": ["./src/*"],
|
| 6 |
+
"open-sse": ["./open-sse"],
|
| 7 |
+
"open-sse/*": ["./open-sse/*"]
|
| 8 |
+
},
|
| 9 |
+
"module": "ESNext",
|
| 10 |
+
"moduleResolution": "bundler"
|
| 11 |
+
}
|
| 12 |
+
}
|
next.config.mjs
CHANGED
|
@@ -1,508 +1,98 @@
|
|
| 1 |
-
import createNextIntlPlugin from "next-intl/plugin";
|
| 2 |
-
import { createMDX } from "fumadocs-mdx/next";
|
| 3 |
-
import { dirname } from "node:path";
|
| 4 |
import { fileURLToPath } from "node:url";
|
|
|
|
| 5 |
|
| 6 |
-
const withNextIntl = createNextIntlPlugin("./src/i18n/request.ts");
|
| 7 |
-
const distDir = process.env.NEXT_DIST_DIR || ".build/next";
|
| 8 |
const projectRoot = dirname(fileURLToPath(import.meta.url));
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
"base-uri 'self'",
|
| 16 |
-
"object-src 'none'",
|
| 17 |
-
"frame-ancestors 'none'",
|
| 18 |
-
"form-action 'self'",
|
| 19 |
-
scriptSrc,
|
| 20 |
-
"style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
|
| 21 |
-
"font-src 'self' https://fonts.gstatic.com data:",
|
| 22 |
-
"img-src 'self' data: blob: https:",
|
| 23 |
-
"media-src 'self' data: blob:",
|
| 24 |
-
"connect-src 'self' http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: wss:",
|
| 25 |
-
"worker-src 'self' blob:",
|
| 26 |
-
"manifest-src 'self'",
|
| 27 |
-
].join("; ");
|
| 28 |
-
const securityHeaders = [
|
| 29 |
-
{
|
| 30 |
-
key: "Content-Security-Policy",
|
| 31 |
-
value: contentSecurityPolicy,
|
| 32 |
-
},
|
| 33 |
-
{
|
| 34 |
-
key: "X-Frame-Options",
|
| 35 |
-
value: "DENY",
|
| 36 |
-
},
|
| 37 |
-
{
|
| 38 |
-
key: "X-Content-Type-Options",
|
| 39 |
-
value: "nosniff",
|
| 40 |
-
},
|
| 41 |
-
{
|
| 42 |
-
key: "Referrer-Policy",
|
| 43 |
-
value: "strict-origin-when-cross-origin",
|
| 44 |
-
},
|
| 45 |
-
{
|
| 46 |
-
key: "Permissions-Policy",
|
| 47 |
-
value: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), serial=()",
|
| 48 |
-
},
|
| 49 |
-
{
|
| 50 |
-
key: "Strict-Transport-Security",
|
| 51 |
-
value: "max-age=63072000; includeSubDomains; preload",
|
| 52 |
-
},
|
| 53 |
-
];
|
| 54 |
-
|
| 55 |
-
function isNextIntlExtractorDynamicImportWarning(warning) {
|
| 56 |
-
const message = typeof warning === "string" ? warning : warning?.message || "";
|
| 57 |
-
const resource = warning?.module?.resource || warning?.file || "";
|
| 58 |
-
const target = "next-intl/dist/esm/production/extractor/format/index.js";
|
| 59 |
-
return (
|
| 60 |
-
resource.includes(target) &&
|
| 61 |
-
(message.includes("import(t)") || message.includes("dependency is an expression"))
|
| 62 |
-
);
|
| 63 |
-
}
|
| 64 |
-
|
| 65 |
-
// OMNIROUTE_BUILD_PROFILE=minimal physically removes four optional privileged
|
| 66 |
-
// modules (MITM cert install, Zed keychain import, Cloud Sync, 9router
|
| 67 |
-
// installer) from the built bundle by aliasing them to feature-disabled stubs.
|
| 68 |
-
// The resulting artifact is intended to be published as `omniroute-secure`
|
| 69 |
-
// for security-sensitive environments. See docs/security/SOCKET_DEV_FINDINGS.md.
|
| 70 |
-
const isMinimalBuild = process.env.OMNIROUTE_BUILD_PROFILE === "minimal";
|
| 71 |
-
|
| 72 |
-
const minimalBuildAliases = isMinimalBuild
|
| 73 |
-
? {
|
| 74 |
-
"@/mitm/cert/install": "./src/mitm/cert/install.stub.ts",
|
| 75 |
-
"@/lib/zed-oauth/keychain-reader": "./src/lib/zed-oauth/keychain-reader.stub.ts",
|
| 76 |
-
"@/lib/cloudSync": "./src/lib/cloudSync.stub.ts",
|
| 77 |
-
"@/lib/services/installers/ninerouter": "./src/lib/services/installers/ninerouter.stub.ts",
|
| 78 |
-
}
|
| 79 |
-
: {};
|
| 80 |
|
| 81 |
/** @type {import('next').NextConfig} */
|
| 82 |
const nextConfig = {
|
| 83 |
-
distDir,
|
| 84 |
-
// Turbopack config: redirect native modules to stubs at build time
|
| 85 |
-
turbopack: {
|
| 86 |
-
root: projectRoot,
|
| 87 |
-
resolveAlias: {
|
| 88 |
-
// Point mitm/manager to a stub during build (native child_process/fs can't be bundled)
|
| 89 |
-
"@/mitm/manager": "./src/mitm/manager.stub.ts",
|
| 90 |
-
...minimalBuildAliases,
|
| 91 |
-
},
|
| 92 |
-
},
|
| 93 |
output: "standalone",
|
| 94 |
-
//
|
| 95 |
-
//
|
| 96 |
-
//
|
| 97 |
-
//
|
| 98 |
-
//
|
| 99 |
-
//
|
| 100 |
-
//
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
serverActions: {
|
| 105 |
-
bodySizeLimit: process.env.OMNIROUTE_SERVER_ACTIONS_BODY_LIMIT || "50mb",
|
| 106 |
-
},
|
| 107 |
-
// Next.js proxy (middleware) has a default 10MB body clone limit. File
|
| 108 |
-
// uploads (OpenAI-compatible /v1/files) routinely exceed this. Match the
|
| 109 |
-
// 512 MB server-side cap; tune via env if needed.
|
| 110 |
-
proxyClientMaxBodySize: process.env.NEXT_PROXY_BODY_LIMIT || "512mb",
|
| 111 |
-
},
|
| 112 |
-
outputFileTracingRoot: projectRoot,
|
| 113 |
-
outputFileTracingIncludes: {
|
| 114 |
-
// Migration SQL and compression rule/filter JSON files are read via fs at
|
| 115 |
-
// runtime and are NOT always auto-traced by webpack/turbopack.
|
| 116 |
-
"/*": [
|
| 117 |
-
"./src/lib/db/migrations/**/*",
|
| 118 |
-
"./src/mitm/server.cjs",
|
| 119 |
-
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
|
| 120 |
-
"./open-sse/services/compression/rules/**/*.json",
|
| 121 |
-
"./open-sse/lib/sha3_wasm_bg.wasm",
|
| 122 |
-
"./open-sse/lib/deepseek-pow-solver.cjs",
|
| 123 |
-
],
|
| 124 |
},
|
|
|
|
| 125 |
outputFileTracingExcludes: {
|
| 126 |
-
//
|
| 127 |
-
// when broad fs/path tracing pulls the whole repository into the NFT graph.
|
| 128 |
-
"/*": [
|
| 129 |
-
"./.git/**/*",
|
| 130 |
-
"./_tasks/**/*",
|
| 131 |
-
"./_references/**/*",
|
| 132 |
-
"./_ideia/**/*",
|
| 133 |
-
"./_mono_repo/**/*",
|
| 134 |
-
"./coverage/**/*",
|
| 135 |
-
"./test-results/**/*",
|
| 136 |
-
"./playwright-report/**/*",
|
| 137 |
-
"./app.__qa_backup/**/*",
|
| 138 |
-
"./tests/**/*",
|
| 139 |
-
"./logs/**/*",
|
| 140 |
-
],
|
| 141 |
},
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
"pino-pretty",
|
| 145 |
-
"thread-stream",
|
| 146 |
-
"pino-abstract-transport",
|
| 147 |
-
"better-sqlite3",
|
| 148 |
-
// sqlite-vec ships a native vec0.so loaded at runtime via createRequire().
|
| 149 |
-
// Turbopack otherwise tries to bundle the .so and fails with "Unknown module
|
| 150 |
-
// type"; externalizing it keeps the require at runtime (like better-sqlite3).
|
| 151 |
-
// See issue #3066.
|
| 152 |
-
"sqlite-vec",
|
| 153 |
-
"node-machine-id",
|
| 154 |
-
"keytar",
|
| 155 |
-
"wreq-js",
|
| 156 |
-
"zod",
|
| 157 |
-
"tls-client-node",
|
| 158 |
-
"koffi",
|
| 159 |
-
"tough-cookie",
|
| 160 |
-
"@ngrok/ngrok",
|
| 161 |
-
"@huggingface/transformers",
|
| 162 |
-
"child_process",
|
| 163 |
-
"fs",
|
| 164 |
-
"path",
|
| 165 |
-
"os",
|
| 166 |
-
"crypto",
|
| 167 |
-
"net",
|
| 168 |
-
"tls",
|
| 169 |
-
"http",
|
| 170 |
-
"https",
|
| 171 |
-
"stream",
|
| 172 |
-
"buffer",
|
| 173 |
-
"util",
|
| 174 |
-
"process",
|
| 175 |
-
],
|
| 176 |
-
transpilePackages: ["@omniroute/open-sse", "@lobehub/icons", "fumadocs-ui", "fumadocs-core"],
|
| 177 |
-
allowedDevOrigins: ["localhost", "127.0.0.1", "192.168.0.250"],
|
| 178 |
-
typescript: {
|
| 179 |
-
// TODO: Re-enable after fixing all sub-component useTranslations scope issues
|
| 180 |
-
ignoreBuildErrors: true,
|
| 181 |
},
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
test: /[\\/]node_modules[\\/]@lobehub[\\/]icons[\\/]/,
|
| 200 |
-
name: "vendor-lobe-icons",
|
| 201 |
-
chunks: "all",
|
| 202 |
-
priority: 20,
|
| 203 |
-
},
|
| 204 |
-
monaco: {
|
| 205 |
-
test: /[\\/]node_modules[\\/]monaco-editor[\\/]/,
|
| 206 |
-
name: "vendor-monaco",
|
| 207 |
-
chunks: "all",
|
| 208 |
-
priority: 20,
|
| 209 |
-
},
|
| 210 |
-
xyflow: {
|
| 211 |
-
test: /[\\/]node_modules[\\/]@xyflow[\\/]/,
|
| 212 |
-
name: "vendor-xyflow",
|
| 213 |
-
chunks: "all",
|
| 214 |
-
priority: 20,
|
| 215 |
-
},
|
| 216 |
-
mermaid: {
|
| 217 |
-
test: /[\\/]node_modules[\\/]mermaid[\\/]/,
|
| 218 |
-
name: "vendor-mermaid",
|
| 219 |
-
chunks: "all",
|
| 220 |
-
priority: 20,
|
| 221 |
-
},
|
| 222 |
-
},
|
| 223 |
-
};
|
| 224 |
-
|
| 225 |
-
if (isMinimalBuild) {
|
| 226 |
-
// Mirror the turbopack.resolveAlias entries for webpack-built artifacts.
|
| 227 |
-
// NormalModuleReplacementPlugin swaps the real module for a stub before
|
| 228 |
-
// webpack resolves it, so the privileged source files are never compiled
|
| 229 |
-
// into the standalone output.
|
| 230 |
-
const replacements = [
|
| 231 |
-
[/^@\/mitm\/cert\/install$/, "./src/mitm/cert/install.stub.ts"],
|
| 232 |
-
[/^@\/lib\/zed-oauth\/keychain-reader$/, "./src/lib/zed-oauth/keychain-reader.stub.ts"],
|
| 233 |
-
[/^@\/lib\/cloudSync$/, "./src/lib/cloudSync.stub.ts"],
|
| 234 |
-
[
|
| 235 |
-
/^@\/lib\/services\/installers\/ninerouter$/,
|
| 236 |
-
"./src/lib/services/installers/ninerouter.stub.ts",
|
| 237 |
-
],
|
| 238 |
-
];
|
| 239 |
-
for (const [pattern, stubPath] of replacements) {
|
| 240 |
-
config.plugins.push(
|
| 241 |
-
new webpack.NormalModuleReplacementPlugin(pattern, (resource) => {
|
| 242 |
-
resource.request = stubPath;
|
| 243 |
-
})
|
| 244 |
-
);
|
| 245 |
-
}
|
| 246 |
}
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
return config;
|
| 249 |
},
|
| 250 |
-
images: {
|
| 251 |
-
unoptimized: true,
|
| 252 |
-
},
|
| 253 |
-
|
| 254 |
-
async headers() {
|
| 255 |
-
return [
|
| 256 |
-
{
|
| 257 |
-
source: "/:path*",
|
| 258 |
-
headers: securityHeaders,
|
| 259 |
-
},
|
| 260 |
-
// G-10: allow OmniRoute's own dashboard to embed the 9Router UI via our reverse proxy.
|
| 261 |
-
// `frame-ancestors 'self'` overrides the global `frame-ancestors 'none'` only for this
|
| 262 |
-
// path. The route is already LOCAL_ONLY (routeGuard.ts) so remote origins cannot reach it.
|
| 263 |
-
{
|
| 264 |
-
source: "/dashboard/providers/services/:name/embed/:path*",
|
| 265 |
-
headers: [{ key: "Content-Security-Policy", value: "frame-ancestors 'self'" }],
|
| 266 |
-
},
|
| 267 |
-
];
|
| 268 |
-
},
|
| 269 |
-
|
| 270 |
-
async redirects() {
|
| 271 |
-
return [
|
| 272 |
-
// Dashboard routes
|
| 273 |
-
{
|
| 274 |
-
source: "/dashboard/skills",
|
| 275 |
-
destination: "/dashboard/omni-skills",
|
| 276 |
-
permanent: true,
|
| 277 |
-
},
|
| 278 |
-
// Architecture
|
| 279 |
-
{
|
| 280 |
-
source: "/docs/architecture",
|
| 281 |
-
destination: "/docs/architecture/architecture",
|
| 282 |
-
permanent: true,
|
| 283 |
-
},
|
| 284 |
-
{
|
| 285 |
-
source: "/docs/authz-guide",
|
| 286 |
-
destination: "/docs/architecture/authz-guide",
|
| 287 |
-
permanent: true,
|
| 288 |
-
},
|
| 289 |
-
{
|
| 290 |
-
source: "/docs/codebase-documentation",
|
| 291 |
-
destination: "/docs/architecture/codebase-documentation",
|
| 292 |
-
permanent: true,
|
| 293 |
-
},
|
| 294 |
-
{
|
| 295 |
-
source: "/docs/repository-map",
|
| 296 |
-
destination: "/docs/architecture/repository-map",
|
| 297 |
-
permanent: true,
|
| 298 |
-
},
|
| 299 |
-
{
|
| 300 |
-
source: "/docs/resilience-guide",
|
| 301 |
-
destination: "/docs/architecture/resilience-guide",
|
| 302 |
-
permanent: true,
|
| 303 |
-
},
|
| 304 |
-
// Guides
|
| 305 |
-
{ source: "/docs/docker-guide", destination: "/docs/guides/docker-guide", permanent: true },
|
| 306 |
-
{
|
| 307 |
-
source: "/docs/electron-guide",
|
| 308 |
-
destination: "/docs/guides/electron-guide",
|
| 309 |
-
permanent: true,
|
| 310 |
-
},
|
| 311 |
-
{ source: "/docs/features", destination: "/docs/guides/features", permanent: true },
|
| 312 |
-
{ source: "/docs/i18n", destination: "/docs/guides/i18n", permanent: true },
|
| 313 |
-
{ source: "/docs/kiro-setup", destination: "/docs/guides/kiro-setup", permanent: true },
|
| 314 |
-
{ source: "/docs/pwa-guide", destination: "/docs/guides/pwa-guide", permanent: true },
|
| 315 |
-
{ source: "/docs/setup-guide", destination: "/docs/guides/setup-guide", permanent: true },
|
| 316 |
-
{ source: "/docs/termux-guide", destination: "/docs/guides/termux-guide", permanent: true },
|
| 317 |
-
{
|
| 318 |
-
source: "/docs/troubleshooting",
|
| 319 |
-
destination: "/docs/guides/troubleshooting",
|
| 320 |
-
permanent: true,
|
| 321 |
-
},
|
| 322 |
-
{ source: "/docs/uninstall", destination: "/docs/guides/uninstall", permanent: true },
|
| 323 |
-
{ source: "/docs/user-guide", destination: "/docs/guides/user-guide", permanent: true },
|
| 324 |
-
// Reference
|
| 325 |
-
{
|
| 326 |
-
source: "/docs/api-reference",
|
| 327 |
-
destination: "/docs/reference/api-reference",
|
| 328 |
-
permanent: true,
|
| 329 |
-
},
|
| 330 |
-
{ source: "/docs/cli-tools", destination: "/docs/reference/cli-tools", permanent: true },
|
| 331 |
-
{ source: "/docs/environment", destination: "/docs/reference/environment", permanent: true },
|
| 332 |
-
{ source: "/docs/free-tiers", destination: "/docs/reference/free-tiers", permanent: true },
|
| 333 |
-
{
|
| 334 |
-
source: "/docs/provider-reference",
|
| 335 |
-
destination: "/docs/reference/provider-reference",
|
| 336 |
-
permanent: true,
|
| 337 |
-
},
|
| 338 |
-
// Frameworks
|
| 339 |
-
{ source: "/docs/a2a-server", destination: "/docs/frameworks/a2a-server", permanent: true },
|
| 340 |
-
{
|
| 341 |
-
source: "/docs/agent-protocols-guide",
|
| 342 |
-
destination: "/docs/frameworks/agent-protocols-guide",
|
| 343 |
-
permanent: true,
|
| 344 |
-
},
|
| 345 |
-
{ source: "/docs/cloud-agent", destination: "/docs/frameworks/cloud-agent", permanent: true },
|
| 346 |
-
{ source: "/docs/evals", destination: "/docs/frameworks/evals", permanent: true },
|
| 347 |
-
{
|
| 348 |
-
source: "/docs/gamification",
|
| 349 |
-
destination: "/docs/frameworks/gamification",
|
| 350 |
-
permanent: true,
|
| 351 |
-
},
|
| 352 |
-
{ source: "/docs/mcp-server", destination: "/docs/frameworks/mcp-server", permanent: true },
|
| 353 |
-
{ source: "/docs/memory", destination: "/docs/frameworks/memory", permanent: true },
|
| 354 |
-
{ source: "/docs/opencode", destination: "/docs/frameworks/opencode", permanent: true },
|
| 355 |
-
{ source: "/docs/skills", destination: "/docs/frameworks/skills", permanent: true },
|
| 356 |
-
{ source: "/docs/webhooks", destination: "/docs/frameworks/webhooks", permanent: true },
|
| 357 |
-
// Routing
|
| 358 |
-
{ source: "/docs/auto-combo", destination: "/docs/routing/auto-combo", permanent: true },
|
| 359 |
-
{
|
| 360 |
-
source: "/docs/reasoning-replay",
|
| 361 |
-
destination: "/docs/routing/reasoning-replay",
|
| 362 |
-
permanent: true,
|
| 363 |
-
},
|
| 364 |
-
// Security
|
| 365 |
-
{ source: "/docs/cli-token", destination: "/docs/security/cli-token", permanent: true },
|
| 366 |
-
{
|
| 367 |
-
source: "/docs/cli-token-auth",
|
| 368 |
-
destination: "/docs/security/cli-token-auth",
|
| 369 |
-
permanent: true,
|
| 370 |
-
},
|
| 371 |
-
{ source: "/docs/compliance", destination: "/docs/security/compliance", permanent: true },
|
| 372 |
-
{
|
| 373 |
-
source: "/docs/error-sanitization",
|
| 374 |
-
destination: "/docs/security/error-sanitization",
|
| 375 |
-
permanent: true,
|
| 376 |
-
},
|
| 377 |
-
{ source: "/docs/guardrails", destination: "/docs/security/guardrails", permanent: true },
|
| 378 |
-
{ source: "/docs/public-creds", destination: "/docs/security/public-creds", permanent: true },
|
| 379 |
-
{
|
| 380 |
-
source: "/docs/route-guard-tiers",
|
| 381 |
-
destination: "/docs/security/route-guard-tiers",
|
| 382 |
-
permanent: true,
|
| 383 |
-
},
|
| 384 |
-
{
|
| 385 |
-
source: "/docs/stealth-guide",
|
| 386 |
-
destination: "/docs/security/stealth-guide",
|
| 387 |
-
permanent: true,
|
| 388 |
-
},
|
| 389 |
-
// Compression
|
| 390 |
-
{
|
| 391 |
-
source: "/docs/compression-engines",
|
| 392 |
-
destination: "/docs/compression/compression-engines",
|
| 393 |
-
permanent: true,
|
| 394 |
-
},
|
| 395 |
-
{
|
| 396 |
-
source: "/docs/compression-guide",
|
| 397 |
-
destination: "/docs/compression/compression-guide",
|
| 398 |
-
permanent: true,
|
| 399 |
-
},
|
| 400 |
-
{
|
| 401 |
-
source: "/docs/compression-language-packs",
|
| 402 |
-
destination: "/docs/compression/compression-language-packs",
|
| 403 |
-
permanent: true,
|
| 404 |
-
},
|
| 405 |
-
{
|
| 406 |
-
source: "/docs/compression-rules-format",
|
| 407 |
-
destination: "/docs/compression/compression-rules-format",
|
| 408 |
-
permanent: true,
|
| 409 |
-
},
|
| 410 |
-
{
|
| 411 |
-
source: "/docs/rtk-compression",
|
| 412 |
-
destination: "/docs/compression/rtk-compression",
|
| 413 |
-
permanent: true,
|
| 414 |
-
},
|
| 415 |
-
// Ops
|
| 416 |
-
{ source: "/docs/coverage-plan", destination: "/docs/ops/coverage-plan", permanent: true },
|
| 417 |
-
{
|
| 418 |
-
source: "/docs/e2e-dashboard-shakedown-v3.8.0",
|
| 419 |
-
destination: "/docs/ops/e2e-dashboard-shakedown-v3.8.0",
|
| 420 |
-
permanent: true,
|
| 421 |
-
},
|
| 422 |
-
{
|
| 423 |
-
source: "/docs/fly-io-deployment-guide",
|
| 424 |
-
destination: "/docs/ops/fly-io-deployment-guide",
|
| 425 |
-
permanent: true,
|
| 426 |
-
},
|
| 427 |
-
{ source: "/docs/proxy-guide", destination: "/docs/ops/proxy-guide", permanent: true },
|
| 428 |
-
{
|
| 429 |
-
source: "/docs/release-checklist",
|
| 430 |
-
destination: "/docs/ops/release-checklist",
|
| 431 |
-
permanent: true,
|
| 432 |
-
},
|
| 433 |
-
{ source: "/docs/sqlite-runtime", destination: "/docs/ops/sqlite-runtime", permanent: true },
|
| 434 |
-
{ source: "/docs/tunnels-guide", destination: "/docs/ops/tunnels-guide", permanent: true },
|
| 435 |
-
{
|
| 436 |
-
source: "/docs/vm-deployment-guide",
|
| 437 |
-
destination: "/docs/ops/vm-deployment-guide",
|
| 438 |
-
permanent: true,
|
| 439 |
-
},
|
| 440 |
-
// CLI Pages — Plano 14 (F9)
|
| 441 |
-
{ source: "/dashboard/cli-tools", destination: "/dashboard/cli-code", permanent: true },
|
| 442 |
-
{
|
| 443 |
-
source: "/dashboard/cli-tools/:path*",
|
| 444 |
-
destination: "/dashboard/cli-code/:path*",
|
| 445 |
-
permanent: true,
|
| 446 |
-
},
|
| 447 |
-
{ source: "/dashboard/agents", destination: "/dashboard/acp-agents", permanent: true },
|
| 448 |
-
{
|
| 449 |
-
source: "/dashboard/agents/:path*",
|
| 450 |
-
destination: "/dashboard/acp-agents/:path*",
|
| 451 |
-
permanent: true,
|
| 452 |
-
},
|
| 453 |
-
];
|
| 454 |
-
},
|
| 455 |
-
|
| 456 |
async rewrites() {
|
| 457 |
return [
|
| 458 |
-
{
|
| 459 |
-
source: "/chat/completions",
|
| 460 |
-
destination: "/api/v1/chat/completions",
|
| 461 |
-
},
|
| 462 |
-
{
|
| 463 |
-
source: "/responses",
|
| 464 |
-
destination: "/api/v1/responses",
|
| 465 |
-
},
|
| 466 |
-
{
|
| 467 |
-
source: "/responses/:path*",
|
| 468 |
-
destination: "/api/v1/responses/:path*",
|
| 469 |
-
},
|
| 470 |
-
{
|
| 471 |
-
source: "/models",
|
| 472 |
-
destination: "/api/v1/models",
|
| 473 |
-
},
|
| 474 |
{
|
| 475 |
source: "/v1/v1/:path*",
|
| 476 |
-
destination: "/api/v1/:path*"
|
| 477 |
},
|
| 478 |
{
|
| 479 |
source: "/v1/v1",
|
| 480 |
-
destination: "/api/v1"
|
| 481 |
},
|
| 482 |
{
|
| 483 |
source: "/codex/:path*",
|
| 484 |
-
destination: "/api/v1/responses"
|
| 485 |
-
},
|
| 486 |
-
{
|
| 487 |
-
source: "/v1/:path*",
|
| 488 |
-
destination: "/api/v1/:path*",
|
| 489 |
},
|
| 490 |
{
|
| 491 |
-
source: "/
|
| 492 |
-
destination: "/api/v1"
|
| 493 |
},
|
| 494 |
{
|
| 495 |
source: "/v1beta/:path*",
|
| 496 |
-
destination: "/api/v1beta/:path*"
|
| 497 |
},
|
| 498 |
{
|
| 499 |
source: "/v1beta",
|
| 500 |
-
destination: "/api/v1beta"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 501 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
| 502 |
];
|
| 503 |
-
}
|
| 504 |
};
|
| 505 |
|
| 506 |
-
|
| 507 |
-
|
| 508 |
-
export default withMDX(withNextIntl(nextConfig));
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
import { fileURLToPath } from "node:url";
|
| 2 |
+
import { dirname, join } from "node:path";
|
| 3 |
|
|
|
|
|
|
|
| 4 |
const projectRoot = dirname(fileURLToPath(import.meta.url));
|
| 5 |
+
// CLI bundling needs workspace root so tracing includes hoisted node_modules (slim ~50MB).
|
| 6 |
+
// Docker / default uses projectRoot so server.js lands at /app/server.js (not nested).
|
| 7 |
+
const tracingRoot = process.env.NEXT_TRACING_ROOT_MODE === "workspace"
|
| 8 |
+
? join(projectRoot, "..")
|
| 9 |
+
: projectRoot;
|
| 10 |
+
const proxyClientMaxBodySize = process.env.NINEROUTER_PROXY_CLIENT_MAX_BODY_SIZE || "128mb";
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
/** @type {import('next').NextConfig} */
|
| 13 |
const nextConfig = {
|
| 14 |
+
distDir: process.env.NEXT_DIST_DIR || ".next",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
output: "standalone",
|
| 16 |
+
// `open` must stay external. It derives its own directory from `import.meta.url`, and
|
| 17 |
+
// webpack replaces that with the absolute path of the BUILD machine as a string literal.
|
| 18 |
+
// A release built on macOS therefore ships `file:///Users/.../open/index.js`, which
|
| 19 |
+
// `fileURLToPath` rejects on Windows ("File URL path must be absolute" — no drive
|
| 20 |
+
// letter). That throw happens at module scope, so every consumer of `open` dies on
|
| 21 |
+
// import — including xAI/Grok token refresh, which loads the OAuth service that imports
|
| 22 |
+
// it. Keeping it external preserves the real `import.meta.url` at runtime.
|
| 23 |
+
serverExternalPackages: ["better-sqlite3", "sql.js", "node:sqlite", "bun:sqlite", "open"],
|
| 24 |
+
turbopack: {
|
| 25 |
+
root: tracingRoot
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
},
|
| 27 |
+
outputFileTracingRoot: tracingRoot,
|
| 28 |
outputFileTracingExcludes: {
|
| 29 |
+
"*": ["./gitbook/**/*"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
},
|
| 31 |
+
images: {
|
| 32 |
+
unoptimized: true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
},
|
| 34 |
+
env: {},
|
| 35 |
+
experimental: {
|
| 36 |
+
// #1529/#1572: LLM clients can send long context or base64 image payloads through /v1 rewrites.
|
| 37 |
+
proxyClientMaxBodySize,
|
| 38 |
+
// Cache fetch responses across HMR refreshes for faster dev reloads.
|
| 39 |
+
serverComponentsHmrCache: true,
|
| 40 |
+
// Tree-shake heavy barrel imports to cut compile + bundle size
|
| 41 |
+
optimizePackageImports: ["@xyflow/react", "@dnd-kit/core", "@dnd-kit/sortable", "material-symbols", "marked"],
|
| 42 |
+
},
|
| 43 |
+
webpack: (config, { isServer }) => {
|
| 44 |
+
// Ignore fs/path modules in browser bundle
|
| 45 |
+
if (!isServer) {
|
| 46 |
+
config.resolve.fallback = {
|
| 47 |
+
...config.resolve.fallback,
|
| 48 |
+
fs: false,
|
| 49 |
+
path: false,
|
| 50 |
+
};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
}
|
| 52 |
+
// Exclude non-source dirs from watcher to reduce inotify load
|
| 53 |
+
config.watchOptions = {
|
| 54 |
+
...config.watchOptions,
|
| 55 |
+
aggregateTimeout: 300,
|
| 56 |
+
ignored: /[\\/](node_modules|\.git|logs|\.next|\.next-cli-build|gitbook|cli|open-sse\.old|tests|docs)[\\/]/,
|
| 57 |
+
};
|
| 58 |
return config;
|
| 59 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
async rewrites() {
|
| 61 |
return [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
{
|
| 63 |
source: "/v1/v1/:path*",
|
| 64 |
+
destination: "/api/v1/:path*"
|
| 65 |
},
|
| 66 |
{
|
| 67 |
source: "/v1/v1",
|
| 68 |
+
destination: "/api/v1"
|
| 69 |
},
|
| 70 |
{
|
| 71 |
source: "/codex/:path*",
|
| 72 |
+
destination: "/api/v1/responses"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
},
|
| 74 |
{
|
| 75 |
+
source: "/responses",
|
| 76 |
+
destination: "/api/v1/responses"
|
| 77 |
},
|
| 78 |
{
|
| 79 |
source: "/v1beta/:path*",
|
| 80 |
+
destination: "/api/v1beta/:path*"
|
| 81 |
},
|
| 82 |
{
|
| 83 |
source: "/v1beta",
|
| 84 |
+
destination: "/api/v1beta"
|
| 85 |
+
},
|
| 86 |
+
{
|
| 87 |
+
source: "/v1/:path*",
|
| 88 |
+
destination: "/api/v1/:path*"
|
| 89 |
},
|
| 90 |
+
{
|
| 91 |
+
source: "/v1",
|
| 92 |
+
destination: "/api/v1"
|
| 93 |
+
}
|
| 94 |
];
|
| 95 |
+
}
|
| 96 |
};
|
| 97 |
|
| 98 |
+
export default nextConfig;
|
|
|
|
|
|
package.json
CHANGED
|
@@ -1,276 +1,67 @@
|
|
| 1 |
{
|
| 2 |
-
"name": "
|
| 3 |
-
"version": "
|
| 4 |
-
"description": "
|
| 5 |
-
"
|
| 6 |
-
"bin": {
|
| 7 |
-
"omniroute": "bin/omniroute.mjs",
|
| 8 |
-
"omniroute-reset-password": "bin/reset-password.mjs"
|
| 9 |
-
},
|
| 10 |
-
"files": [
|
| 11 |
-
"bin/",
|
| 12 |
-
"dist/",
|
| 13 |
-
"src/lib/cli-helper/",
|
| 14 |
-
"@omniroute/",
|
| 15 |
-
"open-sse/mcp-server/index.ts",
|
| 16 |
-
"open-sse/mcp-server/server.ts",
|
| 17 |
-
"open-sse/mcp-server/httpTransport.ts",
|
| 18 |
-
"open-sse/mcp-server/audit.ts",
|
| 19 |
-
"open-sse/mcp-server/runtimeHeartbeat.ts",
|
| 20 |
-
"open-sse/mcp-server/scopeEnforcement.ts",
|
| 21 |
-
"open-sse/mcp-server/schemas/",
|
| 22 |
-
"open-sse/mcp-server/tools/",
|
| 23 |
-
"open-sse/mcp-server/README.md",
|
| 24 |
-
"open-sse/utils/setupPolyfill.ts",
|
| 25 |
-
"src/shared/contracts/",
|
| 26 |
-
"src/shared/utils/nodeRuntimeSupport.ts",
|
| 27 |
-
".env.example",
|
| 28 |
-
"scripts/build/postinstall.mjs",
|
| 29 |
-
"bin/cli/runtime/",
|
| 30 |
-
"scripts/postinstall.mjs",
|
| 31 |
-
"scripts/build/postinstallSupport.mjs",
|
| 32 |
-
"scripts/build/sync-env.mjs",
|
| 33 |
-
"scripts/dev/responses-ws-proxy.mjs",
|
| 34 |
-
"scripts/check/check-supported-node-runtime.ts",
|
| 35 |
-
"scripts/dev/sync-env.mjs",
|
| 36 |
-
"scripts/build/native-binary-compat.mjs",
|
| 37 |
-
"scripts/build/build-next-isolated.mjs",
|
| 38 |
-
"README.md",
|
| 39 |
-
"LICENSE"
|
| 40 |
-
],
|
| 41 |
-
"workspaces": [
|
| 42 |
-
"open-sse"
|
| 43 |
-
],
|
| 44 |
-
"engines": {
|
| 45 |
-
"node": ">=22.0.0 <23 || >=24.0.0 <27"
|
| 46 |
-
},
|
| 47 |
-
"keywords": [
|
| 48 |
-
"ai",
|
| 49 |
-
"router",
|
| 50 |
-
"proxy",
|
| 51 |
-
"openai",
|
| 52 |
-
"claude",
|
| 53 |
-
"anthropic",
|
| 54 |
-
"gemini",
|
| 55 |
-
"fallback",
|
| 56 |
-
"cursor",
|
| 57 |
-
"cline",
|
| 58 |
-
"codex",
|
| 59 |
-
"llm",
|
| 60 |
-
"auto-fallback"
|
| 61 |
-
],
|
| 62 |
-
"license": "MIT",
|
| 63 |
-
"author": "diegosouzapw",
|
| 64 |
-
"repository": {
|
| 65 |
-
"type": "git",
|
| 66 |
-
"url": "https://github.com/diegosouzapw/OmniRoute"
|
| 67 |
-
},
|
| 68 |
-
"homepage": "https://omniroute.online",
|
| 69 |
"scripts": {
|
| 70 |
-
"dev": "
|
| 71 |
-
"
|
| 72 |
-
"
|
| 73 |
-
"
|
| 74 |
-
"
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"
|
| 78 |
-
"
|
| 79 |
-
"
|
| 80 |
-
"
|
| 81 |
-
"
|
| 82 |
-
"
|
| 83 |
-
"electron:build:linux": "npm run build && cd electron && npm run build:linux",
|
| 84 |
-
"electron:smoke:packaged": "node scripts/dev/smoke-electron-packaged.mjs",
|
| 85 |
-
"test": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-concurrency=20 tests/unit/*.test.ts",
|
| 86 |
-
"test:unit": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=20 tests/unit/*.test.ts",
|
| 87 |
-
"test:unit:fast": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-isolation=none tests/unit/*.test.ts",
|
| 88 |
-
"test:unit:shard": "concurrently --kill-others-on-fail -n s1,s2 \"npm:test:unit:shard:1\" \"npm:test:unit:shard:2\"",
|
| 89 |
-
"test:unit:shard:1": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=10 --test-shard=1/2 tests/unit/*.test.ts",
|
| 90 |
-
"test:unit:shard:2": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=10 --test-shard=2/2 tests/unit/*.test.ts",
|
| 91 |
-
"test:plan3": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/plan3-p0.test.ts",
|
| 92 |
-
"test:fixes": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/fixes-p1.test.ts",
|
| 93 |
-
"test:security": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test tests/unit/security-fase01.test.ts",
|
| 94 |
-
"check:cycles": "node scripts/check/check-cycles.mjs",
|
| 95 |
-
"check:route-validation:t06": "node scripts/check/check-route-validation.mjs",
|
| 96 |
-
"check:any-budget:t11": "node scripts/check/check-t11-any-budget.mjs",
|
| 97 |
-
"check:docs-sync": "node scripts/check/check-docs-sync.mjs",
|
| 98 |
-
"check:env-doc-sync": "node scripts/check/check-env-doc-sync.mjs",
|
| 99 |
-
"check:docs-counts": "node scripts/check/check-docs-counts-sync.mjs",
|
| 100 |
-
"check:deprecated-versions": "node scripts/check/check-deprecated-versions.mjs",
|
| 101 |
-
"check:doc-links": "node scripts/check/check-doc-links.mjs",
|
| 102 |
-
"check:docs-all": "npm run check:docs-sync && npm run check:docs-counts && npm run check:env-doc-sync && npm run check:deprecated-versions && npm run check:doc-links",
|
| 103 |
-
"docs:render-diagrams": "node scripts/docs/render-diagrams.mjs",
|
| 104 |
-
"i18n:run": "node scripts/i18n/run-translation.mjs",
|
| 105 |
-
"i18n:run:dry": "node scripts/i18n/run-translation.mjs --dry-run",
|
| 106 |
-
"i18n:check": "node scripts/i18n/check-translation-drift.mjs",
|
| 107 |
-
"i18n:sync-ui": "node scripts/i18n/sync-ui-keys.mjs",
|
| 108 |
-
"i18n:sync-ui:dry": "node scripts/i18n/sync-ui-keys.mjs --dry-run",
|
| 109 |
-
"i18n:check-ui-coverage": "node scripts/i18n/check-ui-keys-coverage.mjs",
|
| 110 |
-
"check:node-runtime": "node --import tsx scripts/check/check-supported-node-runtime.ts",
|
| 111 |
-
"check:pack-artifact": "node --import tsx scripts/build/validate-pack-artifact.ts",
|
| 112 |
-
"audit:deps": "npm audit --audit-level=moderate && npm run audit:electron",
|
| 113 |
-
"audit:electron": "npm --prefix electron audit --audit-level=moderate",
|
| 114 |
-
"typecheck:core": "tsc --pretty false -p tsconfig.typecheck-core.json",
|
| 115 |
-
"typecheck:noimplicit:core": "tsc --pretty false -p tsconfig.typecheck-noimplicit-core.json",
|
| 116 |
-
"backfill-aggregation": "node --import tsx src/scripts/backfillAggregation.ts",
|
| 117 |
-
"env:sync": "node scripts/dev/sync-env.mjs",
|
| 118 |
-
"test:integration": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=1 tests/integration/*.test.ts",
|
| 119 |
-
"test:e2e": "node scripts/dev/run-playwright-tests.mjs test tests/e2e/*.spec.ts",
|
| 120 |
-
"test:protocols:e2e": "node scripts/dev/run-protocol-clients-tests.mjs",
|
| 121 |
-
"test:vitest": "vitest run --config vitest.mcp.config.ts",
|
| 122 |
-
"test:vitest:ui": "vitest run --config vitest.config.ts tests/unit/ui",
|
| 123 |
-
"test:ecosystem": "node scripts/dev/run-ecosystem-tests.mjs",
|
| 124 |
-
"test:system": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true node --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=1 tests/e2e/system-failover.test.ts",
|
| 125 |
-
"test:coverage": "cross-env DISABLE_SQLITE_AUTO_BACKUP=true c8 --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov --check-coverage --statements 60 --lines 60 --functions 60 --branches 60 node --max-old-space-size=8192 --import tsx --import ./open-sse/utils/setupPolyfill.ts --test --test-force-exit --test-concurrency=8 tests/unit/*.test.ts",
|
| 126 |
-
"test:coverage:legacy": "c8 --output-dir coverage --exclude=open-sse --check-coverage --lines 50 --functions 50 --branches 50 node --import tsx --test tests/unit/*.test.ts",
|
| 127 |
-
"coverage:report": "c8 report --output-dir coverage --exclude=tests/** --exclude=**/*.test.* --reporter=text --reporter=text-summary --reporter=html --reporter=json-summary --reporter=lcov",
|
| 128 |
-
"coverage:summary": "node scripts/check/test-report-summary.mjs --input coverage/coverage-summary.json --output coverage/coverage-report.md",
|
| 129 |
-
"check:pr-test-policy": "node scripts/check/check-pr-test-policy.mjs",
|
| 130 |
-
"coverage:report:legacy": "c8 report --output-dir coverage --exclude=open-sse --reporter=text --reporter=text-summary",
|
| 131 |
-
"test:all": "npm run test:unit && npm run test:vitest && npm run test:vitest:ui && npm run test:ecosystem && npm run test:e2e",
|
| 132 |
-
"check": "npm run lint && npm run test",
|
| 133 |
-
"prepublishOnly": "npm run build:cli-api && npm run build:cli && npm run check:pack-artifact",
|
| 134 |
-
"postinstall": "node scripts/build/postinstall.mjs",
|
| 135 |
-
"uninstall": "node scripts/build/uninstall.mjs",
|
| 136 |
-
"uninstall:full": "node scripts/build/uninstall.mjs --full",
|
| 137 |
-
"prepare": "husky",
|
| 138 |
-
"system-info": "node scripts/dev/system-info.mjs",
|
| 139 |
-
"build:cli-api": "node --import tsx/esm scripts/cli/generate-api-commands.mjs"
|
| 140 |
},
|
| 141 |
"dependencies": {
|
| 142 |
-
"@aws-sdk/client-bedrock-runtime": "^3.1045.0",
|
| 143 |
"@dnd-kit/core": "^6.3.1",
|
|
|
|
| 144 |
"@dnd-kit/sortable": "^10.0.0",
|
| 145 |
"@dnd-kit/utilities": "^3.2.2",
|
| 146 |
-
"@huggingface/transformers": "^4.2.0",
|
| 147 |
-
"@libsql/client": "^0.14.0",
|
| 148 |
-
"@lobehub/icons": "^5.8.0",
|
| 149 |
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
| 150 |
"@monaco-editor/react": "^4.7.0",
|
| 151 |
-
"@
|
| 152 |
-
"@
|
| 153 |
-
"@
|
| 154 |
-
"@xyflow/react": "^12.10.2",
|
| 155 |
-
"axios": "^1.16.1",
|
| 156 |
"bcryptjs": "^3.0.3",
|
| 157 |
-
"
|
| 158 |
-
"
|
| 159 |
-
"csv-stringify": "^6.7.0",
|
| 160 |
"express": "^5.2.1",
|
| 161 |
-
"
|
| 162 |
-
"
|
| 163 |
-
"
|
| 164 |
-
"
|
| 165 |
-
"fumadocs-ui": "^16.9.0",
|
| 166 |
-
"gray-matter": "^4.0.3",
|
| 167 |
-
"http-proxy-middleware": "^4.0.0",
|
| 168 |
-
"https-proxy-agent": "^9.0.0",
|
| 169 |
-
"ink": "^7.0.3",
|
| 170 |
-
"ink-spinner": "^5.0.0",
|
| 171 |
-
"ink-text-input": "^6.0.0",
|
| 172 |
-
"ioredis": "^5.10.1",
|
| 173 |
-
"jose": "^6.2.3",
|
| 174 |
-
"js-yaml": "^4.1.1",
|
| 175 |
-
"jsonc-parser": "^3.3.1",
|
| 176 |
-
"lowdb": "^7.0.1",
|
| 177 |
-
"lucide-react": "^1.16.0",
|
| 178 |
-
"marked": "^18.0.4",
|
| 179 |
-
"marked-terminal": "^7.3.0",
|
| 180 |
-
"mermaid": "^11.15.0",
|
| 181 |
"monaco-editor": "^0.55.1",
|
| 182 |
-
"next": "^16.
|
| 183 |
-
"
|
| 184 |
-
"next-themes": "^0.4.6",
|
| 185 |
"node-machine-id": "^1.1.12",
|
| 186 |
"open": "^11.0.0",
|
| 187 |
-
"ora": "^9.
|
| 188 |
-
"
|
| 189 |
-
"
|
| 190 |
-
"
|
| 191 |
-
"
|
| 192 |
-
"
|
| 193 |
-
"react": "19.2.7",
|
| 194 |
-
"react-dom": "19.2.7",
|
| 195 |
-
"react-is": "^19.2.6",
|
| 196 |
-
"react-markdown": "^10.1.0",
|
| 197 |
-
"react-reconciler": "^0.33.0",
|
| 198 |
-
"recharts": "^3.8.1",
|
| 199 |
"selfsigned": "^5.5.0",
|
|
|
|
| 200 |
"sql.js": "^1.14.1",
|
| 201 |
-
"
|
| 202 |
-
"
|
| 203 |
-
"
|
| 204 |
-
"update-notifier": "^7.3.1",
|
| 205 |
-
"uuid": "^14.0.0",
|
| 206 |
-
"ws": "^8.18.0",
|
| 207 |
-
"xxhash-wasm": "^1.1.0",
|
| 208 |
-
"yazl": "^3.3.1",
|
| 209 |
-
"zod": "^4.4.3",
|
| 210 |
-
"zustand": "^5.0.13"
|
| 211 |
},
|
| 212 |
"optionalDependencies": {
|
| 213 |
-
"better-sqlite3": "^12.
|
| 214 |
-
"keytar": "^7.9.0",
|
| 215 |
-
"tls-client-node": "^0.1.13",
|
| 216 |
-
"wreq-js": "^2.3.1"
|
| 217 |
},
|
|
|
|
| 218 |
"devDependencies": {
|
| 219 |
-
"@
|
| 220 |
-
"
|
| 221 |
-
"
|
| 222 |
-
"
|
| 223 |
-
"
|
| 224 |
-
"@types/better-sqlite3": "^7.6.13",
|
| 225 |
-
"@types/bun": "latest",
|
| 226 |
-
"@types/keytar": "^4.4.2",
|
| 227 |
-
"@types/node": "^25.9.1",
|
| 228 |
-
"@types/react": "^19.2.15",
|
| 229 |
-
"@types/react-dom": "^19.2.3",
|
| 230 |
-
"@types/ws": "^8.18.0",
|
| 231 |
-
"@vitejs/plugin-react": "^6.0.2",
|
| 232 |
-
"c8": "^11.0.0",
|
| 233 |
-
"concurrently": "^9.2.1",
|
| 234 |
-
"cross-env": "^10.1.0",
|
| 235 |
-
"eslint": "^9.39.4",
|
| 236 |
-
"eslint-config-next": "16.2.7",
|
| 237 |
-
"glob": "^13.0.6",
|
| 238 |
-
"husky": "^9.1.7",
|
| 239 |
-
"jsdom": "^29.1.1",
|
| 240 |
-
"lint-staged": "^17.0.5",
|
| 241 |
-
"node-loader": "^2.1.0",
|
| 242 |
-
"prettier": "^3.8.3",
|
| 243 |
-
"tailwindcss": "^4.3.0",
|
| 244 |
-
"typescript": "^6.0.3",
|
| 245 |
-
"typescript-eslint": "^8.59.4",
|
| 246 |
-
"vitest": "^4.1.7",
|
| 247 |
-
"wait-on": "^9.0.10",
|
| 248 |
-
"wtfnode": "^0.10.1"
|
| 249 |
-
},
|
| 250 |
-
"lint-staged": {
|
| 251 |
-
"*.{js,jsx,ts,tsx,mjs}": [
|
| 252 |
-
"prettier --write",
|
| 253 |
-
"eslint --fix --no-error-on-unmatched-pattern"
|
| 254 |
-
],
|
| 255 |
-
"*.{json,md,yml,yaml,css}": [
|
| 256 |
-
"prettier --write"
|
| 257 |
-
]
|
| 258 |
-
},
|
| 259 |
-
"pnpm": {
|
| 260 |
-
"onlyBuiltDependencies": [
|
| 261 |
-
"@parcel/watcher",
|
| 262 |
-
"@swc/core",
|
| 263 |
-
"better-sqlite3",
|
| 264 |
-
"esbuild",
|
| 265 |
-
"omniroute",
|
| 266 |
-
"sharp"
|
| 267 |
-
]
|
| 268 |
-
},
|
| 269 |
-
"overrides": {
|
| 270 |
-
"dompurify": "^3.4.3",
|
| 271 |
-
"postcss": "^8.5.14",
|
| 272 |
-
"ip-address": "10.2.0",
|
| 273 |
-
"qs": "^6.15.2",
|
| 274 |
-
"uuid": "^14.0.0"
|
| 275 |
}
|
| 276 |
}
|
|
|
|
| 1 |
{
|
| 2 |
+
"name": "9router-app",
|
| 3 |
+
"version": "0.5.55",
|
| 4 |
+
"description": "9Router web dashboard",
|
| 5 |
+
"private": true,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
"scripts": {
|
| 7 |
+
"dev": "next dev --port 20127",
|
| 8 |
+
"dev:webpack": "next dev --webpack --port 20127",
|
| 9 |
+
"build": "next build --webpack",
|
| 10 |
+
"postbuild": "node scripts/copy-standalone-assets.mjs",
|
| 11 |
+
"postbuild:bun": "node scripts/copy-standalone-assets.mjs",
|
| 12 |
+
"start": "node custom-server.js --port 20127",
|
| 13 |
+
"start:hf": "node hf-server.js",
|
| 14 |
+
"build:hf": "next build --webpack",
|
| 15 |
+
"dev:bun": "bun --bun next dev --webpack --port 20127",
|
| 16 |
+
"build:bun": "bun --bun next build --webpack",
|
| 17 |
+
"start:bun": "bun ./.next/standalone/custom-server.js",
|
| 18 |
+
"cli:pack": "npm --prefix cli run pack:cli",
|
| 19 |
+
"cli:publish": "npm --prefix cli run publish:cli"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
},
|
| 21 |
"dependencies": {
|
|
|
|
| 22 |
"@dnd-kit/core": "^6.3.1",
|
| 23 |
+
"@dnd-kit/modifiers": "^9.0.0",
|
| 24 |
"@dnd-kit/sortable": "^10.0.0",
|
| 25 |
"@dnd-kit/utilities": "^3.2.2",
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
"@monaco-editor/react": "^4.7.0",
|
| 27 |
+
"@next/third-parties": "^16.2.9",
|
| 28 |
+
"@node-saml/node-saml": "^5.1.0",
|
| 29 |
+
"@xyflow/react": "^12.10.1",
|
|
|
|
|
|
|
| 30 |
"bcryptjs": "^3.0.3",
|
| 31 |
+
"chalk": "^5.6.2",
|
| 32 |
+
"confbox": "^0.2.4",
|
|
|
|
| 33 |
"express": "^5.2.1",
|
| 34 |
+
"http-proxy-middleware": "^3.0.5",
|
| 35 |
+
"jose": "^6.1.3",
|
| 36 |
+
"marked": "^18.0.1",
|
| 37 |
+
"material-symbols": "^0.44.6",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
"monaco-editor": "^0.55.1",
|
| 39 |
+
"next": "^16.1.6",
|
| 40 |
+
"node-forge": "^1.3.3",
|
|
|
|
| 41 |
"node-machine-id": "^1.1.12",
|
| 42 |
"open": "^11.0.0",
|
| 43 |
+
"ora": "^9.1.0",
|
| 44 |
+
"prop-types": "^15.8.1",
|
| 45 |
+
"react": "19.2.4",
|
| 46 |
+
"react-dom": "19.2.4",
|
| 47 |
+
"react-is": "^16.13.1",
|
| 48 |
+
"recharts": "^3.7.0",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
"selfsigned": "^5.5.0",
|
| 50 |
+
"socks-proxy-agent": "^8.0.5",
|
| 51 |
"sql.js": "^1.14.1",
|
| 52 |
+
"undici": "^7.19.2",
|
| 53 |
+
"uuid": "^13.0.0",
|
| 54 |
+
"zustand": "^5.0.10"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
},
|
| 56 |
"optionalDependencies": {
|
| 57 |
+
"better-sqlite3": "^12.6.2"
|
|
|
|
|
|
|
|
|
|
| 58 |
},
|
| 59 |
+
"comment_better_sqlite3": "kept in optionalDependencies so npm install doesn't fail on systems without build tools — sql.js is used as fallback at runtime",
|
| 60 |
"devDependencies": {
|
| 61 |
+
"@tailwindcss/postcss": "^4.1.18",
|
| 62 |
+
"eslint": "^9",
|
| 63 |
+
"eslint-config-next": "16.1.6",
|
| 64 |
+
"postcss": "^8.5.6",
|
| 65 |
+
"tailwindcss": "^4"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
}
|
| 67 |
}
|
postcss.config.mjs
CHANGED
|
@@ -1,7 +1,12 @@
|
|
| 1 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
plugins: {
|
| 3 |
-
"@tailwindcss/postcss": {
|
|
|
|
|
|
|
| 4 |
},
|
| 5 |
};
|
| 6 |
-
|
| 7 |
-
export default postcssConfig;
|
|
|
|
| 1 |
+
import path from "node:path";
|
| 2 |
+
import { fileURLToPath } from "node:url";
|
| 3 |
+
|
| 4 |
+
const projectRoot = path.dirname(fileURLToPath(import.meta.url));
|
| 5 |
+
|
| 6 |
+
export default {
|
| 7 |
plugins: {
|
| 8 |
+
"@tailwindcss/postcss": {
|
| 9 |
+
base: projectRoot,
|
| 10 |
+
},
|
| 11 |
},
|
| 12 |
};
|
|
|
|
|
|
start.sh
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
docker stop 9router
|
| 2 |
+
docker rm 9router
|
| 3 |
+
docker build -t 9router .
|
| 4 |
+
docker run -d --name 9router -p 20128:20128 --env-file .env -v 9router-data:/app/data 9router
|