automindy commited on
Commit
bcd2dbf
·
verified ·
1 Parent(s): f96921d

Upload 22 files

Browse files
Files changed (22) hide show
  1. .dockerignore +0 -93
  2. .env.example +23 -1443
  3. .gitignore +57 -170
  4. .npmignore +12 -84
  5. CHANGELOG.md +0 -0
  6. CLAUDE.md +76 -438
  7. DOCKER.md +132 -0
  8. Dockerfile +35 -150
  9. LICENSE +1 -1
  10. README.md +1526 -10
  11. README.zh-CN.md +1315 -0
  12. README_HUGGINGFACE.md +25 -0
  13. captain-definition +4 -0
  14. custom-server.js +139 -0
  15. docker-compose.yml +23 -171
  16. eslint.config.mjs +11 -97
  17. hf-server.js +126 -0
  18. jsconfig.json +12 -0
  19. next.config.mjs +62 -472
  20. package.json +46 -255
  21. postcss.config.mjs +9 -4
  22. 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
- # │ OmniRoute .env Contract │
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
- # 1. REQUIRED SECRETS — Must be set before first run!
11
- # ═══════════════════════════════════════════════════════════════════════════════
12
- # These secrets are critical for security. Generate strong, unique values.
13
 
14
- # JWT signing key for dashboard session tokens.
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
- # Container runtime controls startup script behavior (permissions, advice).
161
- # Values: docker | podman | Default: docker
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
- # Salt for deriving CLI machine-ID auth tokens (HMAC-SHA256).
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
- # Allow revealing full API key values in the Dashboard UI.
192
- # Used by: Dashboard providers page controls show/hide of key values.
193
- # Default: false | Security risk if enabled on shared instances.
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
- # Cloud relay URL — premium feature for remote config sync.
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
- # Browser-facing OmniRoute origin for generated assets in API responses.
329
- # Used by: chatgpt-web image generation cache URLs (/v1/chatgpt-web/image/<id>).
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
- # TLS fingerprint spoofing (opt-in) mimics Chrome 124 TLS handshake via wreq-js.
386
- # Reduces risk of JA3/JA4 fingerprint-based blocking by providers (e.g., Google).
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
- # Inspector
1447
- INSPECTOR_BUFFER_SIZE=1000
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
- node_modules/
47
- *.map
48
- .DS_Store
49
-
50
- # Obsidian sync plugin — committed for community distribution
51
- !obsidian-plugin/
52
- obsidian-plugin/node_modules/
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
54
- # Serena AI assistant config (local-only tool, not project code)
55
- .serena/
 
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
- # data and logs
78
  data/
79
- .data/
80
  logs/*
81
- test_output.log
82
-
83
- # analysis directories (generated, not tracked)
84
- .analysis/
85
- antigravity-manager-analysis/
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
- # Security Analysis (standalone project with own git)
104
- security-analysis/
105
-
106
- # Deploy workflow (contains sensitive VPS credentials)
107
- clipr/
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
- # i18n audit artifact (generated by scripts/i18n/audit-dashboard-pages.mjs)
173
- scripts/i18n/_audit.json
174
- scripts/i18n/_pending-keys.json
175
 
176
- # Private workflow / skill / command implementations
177
- # These contain proprietary multi-phase logic and should not be committed
178
- .agents/workflows/implement-features-ag.md
179
- .agents/workflows/port-upstream-features-ag.md
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
- # Fumadocs generated source
189
- .source/
190
-
191
- # AI agent local settings and configs
192
- .agents/
193
- .antigravitycli/
194
  .claude/
195
-
196
- # PR Reviews and local feedback files
197
- pr_reviews*.json
198
-
199
- #hidden local data directories (never commit)
200
- .local-data/
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
- # VS Code extension test runtime (large binary, not needed in npm package)
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
- tests/
16
- cloud/
17
- images/
18
- logs/
19
  scripts/
 
 
 
 
20
 
21
- # Config/dev files
22
  *.md
23
  !README.md
24
  .gitignore
25
- .git/
26
- .github/
27
- .husky/
28
- .vscode/
29
- .agents/
30
  .env*
31
- app/.env
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
- # Root-level underscore-prefixed directories (private/draft never publish)
74
- /_*/
75
- app/_*/
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
- ## Quick Start
6
 
7
- ```bash
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
- - `CLOSED`: normal traffic is allowed.
105
- - `OPEN`: provider is temporarily blocked; callers get a provider-circuit-open response
106
- or combo routing skips to another target.
107
- - `HALF_OPEN`: reset timeout has elapsed; allow a probe request. Success closes the
108
- breaker, failure opens it again.
109
 
110
- **Defaults** (`open-sse/config/constants.ts`):
111
 
112
- - OAuth providers: threshold `3`, reset timeout `60s`.
113
- - API-key providers: threshold `5`, reset timeout `30s`.
114
- - Local providers: threshold `2`, reset timeout `15s`.
115
 
116
- Only provider-level failure statuses should trip the provider breaker:
117
-
118
- ```ts
119
- (408, 500, 502, 503, 504);
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
- Cooldowns are also lazy: when `rateLimitedUntil` is in the past, the connection becomes
164
- eligible again. On successful use, `clearAccountError()` clears `testStatus`,
165
- `rateLimitedUntil`, error fields, and `backoffLevel`.
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
- The anti-thundering-herd guard prevents concurrent failures on the same connection from
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
- # Never commit directly to main
393
- git checkout -b feat/your-feature
394
- git commit -m "feat: describe your change"
395
- git push -u origin feat/your-feature
396
  ```
397
-
398
- **Branch prefixes**: `feat/`, `fix/`, `refactor/`, `docs/`, `test/`, `chore/`
399
-
400
- **Commit format** (Conventional Commits): `feat(db): add circuit breaker` — scopes: `db`, `sse`, `oauth`, `dashboard`, `api`, `cli`, `docker`, `ci`, `mcp`, `a2a`, `memory`, `skills`
401
-
402
- **Husky hooks**:
403
-
404
- - **pre-commit**: lint-staged + `check-docs-sync` + `check:any-budget:t11`
405
- - **pre-push**: `npm run test:unit`
406
-
407
- ---
408
-
409
- ## Environment
410
-
411
- - **Runtime**: Node.js ≥20.20.2 <21 || ≥22.22.2 <23 || ≥24 <25, ES Modules
412
- - **TypeScript**: 5.9+, target ES2022, module esnext, resolution bundler
413
- - **Path aliases**: `@/*` `src/`, `@omniroute/open-sse` → `open-sse/`, `@omniroute/open-sse/*` → `open-sse/*`
414
- - **Default port**: 20128 (API + dashboard on same port)
415
- - **Data directory**: `DATA_DIR` env var, defaults to `~/.omniroute/`
416
- - **Key env vars**: `PORT`, `JWT_SECRET`, `API_KEY_SECRET`, `INITIAL_PASSWORD`, `REQUIRE_API_KEY`, `APP_LOG_LEVEL`
417
- - Setup: `cp .env.example .env` then generate `JWT_SECRET` (`openssl rand -base64 48`) and `API_KEY_SECRET` (`openssl rand -hex 32`)
418
-
419
- ---
420
-
421
- ## Hard Rules
422
-
423
- 1. Never commit secrets or credentials
424
- 2. Never add logic to `localDb.ts`
425
- 3. Never use `eval()` / `new Function()` / implied eval
426
- 4. Never commit directly to `main`
427
- 5. Never write raw SQL in routes — use `src/lib/db/` modules
428
- 6. Never silently swallow errors in SSE streams
429
- 7. Always validate inputs with Zod schemas
430
- 8. Always include tests when changing production code
431
- 9. Coverage must stay ≥60% (statements, lines, functions, branches).
432
- 10. Never bypass Husky hooks (`--no-verify`, `--no-gpg-sign`) without explicit operator approval.
433
- 11. Never embed public upstream OAuth client_id/secret or Firebase Web keys as string literals always go through `resolvePublicCred()` (`open-sse/utils/publicCreds.ts`). See `docs/security/PUBLIC_CREDS.md`.
434
- 12. Never return raw `err.stack` / `err.message` in HTTP / SSE / executor responses always route through `buildErrorBody()` or `sanitizeErrorMessage()` (`open-sse/utils/error.ts`). See `docs/security/ERROR_SANITIZATION.md`.
435
- 13. Never string-interpolate external paths or runtime values into shell scripts passed to `exec()`/`spawn()` — pass via the `env` option instead. Reference: `src/mitm/cert/install.ts::updateNssDatabases`.
436
- 14. Never dismiss a CodeQL / Secret-Scanning alert without (a) first checking the pattern docs above to see if the helper applies, and (b) recording the technical justification in the dismissal comment. Precedent: `js/stack-trace-exposure` raised on callsites that already route through `sanitizeErrorMessage()` is a known CodeQL limitation (custom sanitizers not recognized) — dismiss as `false positive` referencing `docs/security/ERROR_SANITIZATION.md`.
437
- 15. Never expose routes that spawn child processes (`/api/mcp/`, `/api/cli-tools/runtime/`) without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. Loopback enforcement happens unconditionally before any auth check — leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
438
- 16. Never include `Co-Authored-By` trailers that credit an AI assistant, LLM, or automation account (e.g. names containing "Claude", "GPT", "Copilot", "Bot"; emails at `anthropic.com` / `openai.com` / bot-owned `noreply.github.com` addresses). Such trailers route attribution to the bot account on GitHub, hiding the real author (`diegosouzapw`) in PR history. Human collaborators — including upstream PR authors and issue reporters being ported into OmniRoute — MAY and SHOULD be credited with standard `Co-authored-by: Name <email>` trailers; the upstream-port workflows (`/port-upstream-features`, `/port-upstream-issues`) depend on this.
439
- 17. Never expose routes under `/api/services/` or `/dashboard/providers/services/*/embed/` without `isLocalOnlyPath()` classification in `src/server/authz/routeGuard.ts`. These routes can spawn child processes (`npm install`, `node`). Loopback enforcement happens unconditionally before any auth check a leaked JWT via tunnel cannot trigger process spawning. See `docs/security/ROUTE_GUARD_TIERS.md`.
440
- 18. Every bug fix must be validated before shipping: a failing-then-passing unit/integration test (TDD) OR a documented live test on the production VPS (192.168.0.15). A fix without either is not merged. See Testing → "Bug fix / issue triage protocol" for the full decision tree.
441
-
442
- ---
443
-
444
- ## PII & Stream Sanitization Learnings
445
-
446
- ### 1. Regex Security (ReDoS)
447
- All regex patterns matching variable-length strings (e.g. IPv6 address, credit cards) must use strictly bounded, non-overlapping sequences (e.g., limit occurrences with bounded ranges `{1,7}`) to prevent catastrophic backtracking when processing untrusted inputs.
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 untouchednever 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
- # ── Common base with runtime deps ──────────────────────────────────────────
2
- FROM node:24-trixie-slim AS base
 
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
- # Build tools for native module compilation
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 ci --no-audit --no-fund --legacy-peer-deps --ignore-scripts \
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
- RUN --mount=type=cache,target=/app/.build/next/cache \
46
- mkdir -p /app/data && npm run build
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 OMNIROUTE_MEMORY_MB=1024
61
- ENV NODE_OPTIONS="--max-old-space-size=${OMNIROUTE_MEMORY_MB}"
62
-
63
- # Data directory inside Docker — defaults to /data for compatibility with Hugging Face Space persistent storage
64
- ENV DATA_DIR=/data
65
- RUN mkdir -p /data && chown -R node:node /data
66
-
67
- # `npm run build` (build-next-isolated assembleStandalone) bundles ALL runtime
68
- # files into .build/next/standalone/ .next, node_modules, migrations, scripts,
69
- # docs, and the previously hand-COPY'd modules below (@swc/helpers, pino-*, split2,
70
- # migrations). assembleStandalone copies them straight from the builder's
71
- # node_modules, so they are present regardless of NFT/Turbopack trace behaviour.
72
- # The old per-module overrides were therefore pure duplication and were removed
73
- # (build-output-isolation cleanup). See scripts/build/assembleStandalone.mjs
74
- # (EXTRA_MODULE_ENTRIES) for the single source of truth.
75
- COPY --from=builder /app/.build/next/standalone ./
76
- # better-sqlite3 is the one exception still copied explicitly: assembleStandalone
77
- # only syncs its native build/ dir; the JS wrapper (lib/, package.json) is left to
78
- # Next.js tracing. bootstrap-env requires SQLite BEFORE the standalone server
79
- # starts, so guarantee the complete package independent of trace behaviour.
80
- COPY --from=builder /app/node_modules/better-sqlite3 ./node_modules/better-sqlite3
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
- # Drop to non-root before ENTRYPOINT/CMD so every derived stage (runner-cli,
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 diegosouzapw
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
- title: Hahma
3
- emoji: 📊
4
- colorFrom: gray
5
- colorTo: pink
6
- sdk: docker
7
- pinned: false
8
- ---
9
-
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router)
11
+ [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router)
12
+ [![Docker Pulls](https://img.shields.io/docker/pulls/decolua/9router.svg?logo=docker&label=Docker%20pulls)](https://hub.docker.com/r/decolua/9router)
13
+ [![GHCR](https://img.shields.io/badge/GHCR-decolua%2F9router-blue?logo=github)](https://github.com/decolua/9router/pkgs/container/9router)
14
+ [![License](https://img.shields.io/npm/l/9router.svg)](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
+ [![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors)
1492
+
1493
+ ---
1494
+
1495
+ ## 📊 Star Chart
1496
+
1497
+ [![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](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)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Rust token-saver. 9Router ports its compression pipeline to JS → **−20-40% input tokens** on every request.
1511
+ - **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) 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)** ![Stars](https://img.shields.io/github/stars/DietrichGebert/ponytail?style=flat&color=yellow) 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
+ [![npm](https://img.shields.io/npm/v/9router.svg)](https://www.npmjs.com/package/9router)
12
+ [![Downloads](https://img.shields.io/npm/dm/9router.svg)](https://www.npmjs.com/package/9router)
13
+ [![License](https://img.shields.io/npm/l/9router.svg)](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
+ [![Contributors](https://contrib.rocks/image?repo=decolua/9router&max=150&columns=15&anon=1&v=20260309)](https://github.com/decolua/9router/graphs/contributors)
1280
+
1281
+ ---
1282
+
1283
+ ## 📊 Star 图表
1284
+
1285
+ [![Star Chart](https://starchart.cc/decolua/9router.svg?variant=adaptive)](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)** ![Stars](https://img.shields.io/github/stars/rtk-ai/rtk?style=flat&color=yellow) — Rust token 节省器。9Router 将其压缩管道移植到 JS → 每次请求 **减少 20-40% 输入 tokens**。
1301
+ - **[Caveman](https://github.com/JuliusBrussee/caveman)** ![Stars](https://img.shields.io/github/stars/JuliusBrussee/caveman?style=flat&color=yellow) 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
- # ── Redis (Rate Limiter Backend) ──────────────────────────────────
45
- redis:
46
- image: docker.io/library/redis:7-alpine
47
- container_name: omniroute-redis
48
- restart: unless-stopped
49
  ports:
50
- - "${REDIS_PORT:-6379}:6379"
51
  volumes:
52
- - redis-data:/data
53
- command: redis-server --save 60 1 --loglevel warning
54
- healthcheck:
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
- - DATA_DIR=/app/data
123
- - PORT=${PORT:-20128}
124
- - DASHBOARD_PORT=${DASHBOARD_PORT:-20128}
125
- - API_PORT=${API_PORT:-20129}
126
- - API_HOST=${API_HOST:-0.0.0.0}
127
- - CLI_MODE=host
128
- - CLI_EXTRA_PATHS=/host-local/bin:/host-node/bin
129
- - CLI_CONFIG_HOME=/host-home
130
- - CLI_ALLOW_CONFIG_WRITES=true
131
- # Uncomment per-tool overrides as needed:
132
- # - CLI_CURSOR_BIN=agent
133
- # - CLI_CLINE_BIN=cline
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
- - "${CLIPROXYAPI_PORT:-8317}:${CLIPROXYAPI_PORT:-8317}"
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
- cliproxyapi-data:
176
- name: cliproxyapi-data
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
- /** @type {import("eslint").Linter.Config[]} */
5
- const eslintConfig = [
6
  ...nextVitals,
7
- // FASE-02: Security rules (strict everywhere)
8
- {
9
- rules: {
10
- "no-eval": "error",
11
- "no-implied-eval": "error",
12
- "no-new-func": "error",
13
- "no-restricted-imports": [
14
- "error",
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
- const scriptSrc =
10
- process.env.NODE_ENV === "development"
11
- ? "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"
12
- : "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:";
13
- const contentSecurityPolicy = [
14
- "default-src 'self'",
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
- // OmniRoute is a proxy for AI APIs request bodies routinely include
95
- // multi-MB payloads (vision models, image edits, base64-encoded files,
96
- // long chat histories with embedded images). Next.js's Server Action
97
- // handler intercepts POSTs with multipart/form-data or
98
- // x-www-form-urlencoded content-types and enforces a 1 MB cap that
99
- // surfaces as a 413 with a confusing "Server Actions" hint, even on
100
- // pure route handlers. 50 MB matches what most upstream LLM providers
101
- // accept for image-bearing requests; tune via env if a deployment needs
102
- // more.
103
- experimental: {
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
- // Planning/task docs are not runtime assets and can break standalone copies
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
- serverExternalPackages: [
143
- "pino",
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
- webpack(config, { webpack }) {
183
- config.ignoreWarnings = [
184
- ...(config.ignoreWarnings || []),
185
- isNextIntlExtractorDynamicImportWarning,
186
- ];
187
- config.optimization = config.optimization || {};
188
- config.optimization.splitChunks = {
189
- ...config.optimization.splitChunks,
190
- cacheGroups: {
191
- ...(config.optimization.splitChunks?.cacheGroups || {}),
192
- recharts: {
193
- test: /[\\/]node_modules[\\/]recharts[\\/]/,
194
- name: "vendor-recharts",
195
- chunks: "all",
196
- priority: 20,
197
- },
198
- lobeIcons: {
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: "/v1",
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
- const withMDX = createMDX();
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": "omniroute",
3
- "version": "3.8.13",
4
- "description": "Unified AI router with 160+ providers, RTK+Caveman compression, auto fallback, MCP/A2A, desktop, PWA, and OpenAI-compatible APIs.",
5
- "type": "module",
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": "node scripts/dev/run-next.mjs dev",
71
- "prebuild:docs": "node scripts/docs/gen-openapi-module.mjs",
72
- "gen:provider-reference": "node --import tsx scripts/docs/gen-provider-reference.ts",
73
- "build": "node scripts/build/build-next-isolated.mjs",
74
- "build:secure": "OMNIROUTE_BUILD_PROFILE=minimal node scripts/build/build-next-isolated.mjs",
75
- "build:cli": "node --import tsx scripts/build/prepublish.ts",
76
- "build:release": "rm -rf .build dist && OMNIROUTE_BUILD_SHA=$(git rev-parse --short HEAD) npm run build && npm run build:cli && node scripts/build/write-build-sha.mjs",
77
- "start": "node scripts/dev/run-next.mjs start",
78
- "lint": "eslint .",
79
- "electron:dev": "concurrently \"npm run dev\" \"wait-on http://localhost:20128 && cd electron && npm run dev\"",
80
- "electron:build": "npm run build && cd electron && npm run build",
81
- "electron:build:win": "npm run build && cd electron && npm run build:win",
82
- "electron:build:mac": "npm run build && cd electron && npm run build:mac",
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
- "@ngrok/ngrok": "^1.7.0",
152
- "@swc/helpers": "0.5.23",
153
- "@types/mdx": "^2.0.13",
154
- "@xyflow/react": "^12.10.2",
155
- "axios": "^1.16.1",
156
  "bcryptjs": "^3.0.3",
157
- "bottleneck": "^2.19.5",
158
- "commander": "^15.0.0",
159
- "csv-stringify": "^6.7.0",
160
  "express": "^5.2.1",
161
- "fetch-socks": "^1.3.3",
162
- "fflate": "^0.8.3",
163
- "fumadocs-core": "^16.9.0",
164
- "fumadocs-mdx": "^15.0.7",
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.2.6",
183
- "next-intl": "^4.12.0",
184
- "next-themes": "^0.4.6",
185
  "node-machine-id": "^1.1.12",
186
  "open": "^11.0.0",
187
- "ora": "^9.4.0",
188
- "parse5": "^8.0.1",
189
- "pino": "^10.3.1",
190
- "pino-abstract-transport": "^3.0.0",
191
- "pino-pretty": "^13.1.3",
192
- "playwright": "1.60.0",
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
- "sqlite-vec": "^0.1.9",
202
- "tsx": "^4.22.3",
203
- "undici": "^8.3.0",
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.10.0",
214
- "keytar": "^7.9.0",
215
- "tls-client-node": "^0.1.13",
216
- "wreq-js": "^2.3.1"
217
  },
 
218
  "devDependencies": {
219
- "@playwright/test": "^1.60.0",
220
- "@tailwindcss/postcss": "^4.3.0",
221
- "@testing-library/jest-dom": "^6.9.1",
222
- "@testing-library/react": "^16.3.2",
223
- "@types/bcryptjs": "^3.0.0",
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
- const postcssConfig = {
 
 
 
 
 
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