Spaces:
Sleeping
Sleeping
| # 9Router Architecture | |
| _Last updated: 2026-02-06_ | |
| ## Executive Summary | |
| 9Router is a local AI routing gateway and dashboard built on Next.js. | |
| It provides a single OpenAI-compatible endpoint (`/v1/*`) and routes traffic across multiple upstream providers with translation, fallback, token refresh, and usage tracking. | |
| Core capabilities: | |
| - OpenAI-compatible API surface for CLI/tools | |
| - Request/response translation across provider formats | |
| - Model combo fallback (multi-model sequence) | |
| - Account-level fallback (multi-account per provider) | |
| - OAuth + API-key provider connection management | |
| - Local persistence for providers, keys, aliases, combos, settings, pricing | |
| - Usage/cost tracking and request logging | |
| - Optional cloud sync for multi-device/state sync | |
| Primary runtime model: | |
| - Next.js app routes under `src/app/api/*` implement both dashboard APIs and compatibility APIs | |
| - A shared SSE/routing core in `src/sse/*` + `open-sse/*` handles provider execution, translation, streaming, fallback, and usage | |
| ## Scope and Boundaries | |
| ### In Scope | |
| - Local gateway runtime | |
| - Dashboard management APIs | |
| - Provider authentication and token refresh | |
| - Request translation and SSE streaming | |
| - Local state + usage persistence | |
| - Optional cloud sync orchestration | |
| ### Out of Scope | |
| - Cloud service implementation behind `NEXT_PUBLIC_CLOUD_URL` | |
| - Provider SLA/control plane outside local process | |
| - External CLI binaries themselves (Claude CLI, Codex CLI, etc.) | |
| ## High-Level System Context | |
| ```mermaid | |
| flowchart LR | |
| subgraph Clients[Developer Clients] | |
| C1[Claude Code] | |
| C2[Codex CLI] | |
| C3[OpenClaw / Droid / Cline / Continue / Roo] | |
| C4[Custom OpenAI-compatible clients] | |
| BROWSER[Browser Dashboard] | |
| end | |
| subgraph Router[9Router Local Process] | |
| API[V1 Compatibility API\n/v1/*] | |
| DASH[Dashboard + Management API\n/api/*] | |
| CORE[SSE + Translation Core\nopen-sse + src/sse] | |
| DB[(db.json)] | |
| UDB[(usage.json + log.txt)] | |
| end | |
| subgraph Upstreams[Upstream Providers] | |
| P1[OAuth Providers\nClaude/Codex/Gemini/Qwen/iFlow/GitHub/Kiro/Cursor/Antigravity] | |
| P2[API Key Providers\nOpenAI/Anthropic/OpenRouter/GLM/Kimi/MiniMax] | |
| P3[Compatible Nodes\nOpenAI-compatible / Anthropic-compatible] | |
| end | |
| subgraph Cloud[Optional Cloud Sync] | |
| CLOUD[Cloud Sync Endpoint\nNEXT_PUBLIC_CLOUD_URL] | |
| end | |
| C1 --> API | |
| C2 --> API | |
| C3 --> API | |
| C4 --> API | |
| BROWSER --> DASH | |
| API --> CORE | |
| DASH --> DB | |
| CORE --> DB | |
| CORE --> UDB | |
| CORE --> P1 | |
| CORE --> P2 | |
| CORE --> P3 | |
| DASH --> CLOUD | |
| ``` | |
| ## Core Runtime Components | |
| ## 1) API and Routing Layer (Next.js App Routes) | |
| Main directories: | |
| - `src/app/api/v1/*` and `src/app/api/v1beta/*` for compatibility APIs | |
| - `src/app/api/*` for management/configuration APIs | |
| - Next rewrites in `next.config.mjs` map `/v1/*` to `/api/v1/*` | |
| Important compatibility routes: | |
| - `src/app/api/v1/chat/completions/route.js` | |
| - `src/app/api/v1/messages/route.js` | |
| - `src/app/api/v1/responses/route.js` | |
| - `src/app/api/v1/models/route.js` | |
| - `src/app/api/v1/messages/count_tokens/route.js` | |
| - `src/app/api/v1beta/models/route.js` | |
| - `src/app/api/v1beta/models/[...path]/route.js` | |
| Management domains: | |
| - Auth/settings: `src/app/api/auth/*`, `src/app/api/settings/*` | |
| - Providers/connections: `src/app/api/providers*` | |
| - Provider nodes: `src/app/api/provider-nodes*` | |
| - OAuth: `src/app/api/oauth/*` | |
| - Keys/aliases/combos/pricing: `src/app/api/keys*`, `src/app/api/models/alias`, `src/app/api/combos*`, `src/app/api/pricing` | |
| - Usage: `src/app/api/usage/*` | |
| - Sync/cloud: `src/app/api/sync/*`, `src/app/api/cloud/*` | |
| - CLI tooling helpers: `src/app/api/cli-tools/*` | |
| ## 2) SSE + Translation Core | |
| Main flow modules: | |
| - Entry: `src/sse/handlers/chat.js` | |
| - Core orchestration: `open-sse/handlers/chatCore.js` | |
| - Provider execution adapters: `open-sse/executors/*` | |
| - Format detection/provider config: `open-sse/services/provider.js` | |
| - Model parse/resolve: `src/sse/services/model.js`, `open-sse/services/model.js` | |
| - Account fallback logic: `open-sse/services/accountFallback.js` | |
| - Translation registry: `open-sse/translator/index.js` | |
| - Stream transformations: `open-sse/utils/stream.js`, `open-sse/utils/streamHandler.js` | |
| - Usage extraction/normalization: `open-sse/utils/usageTracking.js` | |
| ## 3) Persistence Layer | |
| Primary state DB: | |
| - `src/lib/localDb.js` | |
| - file: `${DATA_DIR}/db.json` (or `~/.9router/db.json` when `DATA_DIR` is unset) | |
| - entities: providerConnections, providerNodes, modelAliases, combos, apiKeys, settings, pricing | |
| Usage DB: | |
| - `src/lib/usageDb.js` | |
| - files: `~/.9router/usage.json`, `~/.9router/log.txt` | |
| - note: currently independent from `DATA_DIR` | |
| ## 4) Auth + Security Surfaces | |
| - Dashboard cookie auth: `src/proxy.js`, `src/app/api/auth/login/route.js` | |
| - API key generation/verification: `src/shared/utils/apiKey.js` | |
| - Provider secrets persisted in `providerConnections` entries | |
| - Optional proxy support for upstream calls via env proxy variables (`open-sse/utils/proxyFetch.js`) | |
| ## 5) Cloud Sync | |
| - Scheduler init: `src/lib/initCloudSync.js`, `src/shared/services/initializeCloudSync.js` | |
| - Periodic task: `src/shared/services/cloudSyncScheduler.js` | |
| - Control route: `src/app/api/sync/cloud/route.js` | |
| ## Request Lifecycle (`/v1/chat/completions`) | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| participant Client as CLI/SDK Client | |
| participant Route as /api/v1/chat/completions | |
| participant Chat as src/sse/handlers/chat | |
| participant Core as open-sse/handlers/chatCore | |
| participant Model as Model Resolver | |
| participant Auth as Credential Selector | |
| participant Exec as Provider Executor | |
| participant Prov as Upstream Provider | |
| participant Stream as Stream Translator | |
| participant Usage as usageDb | |
| Client->>Route: POST /v1/chat/completions | |
| Route->>Chat: handleChat(request) | |
| Chat->>Model: parse/resolve model or combo | |
| alt Combo model | |
| Chat->>Chat: iterate combo models (handleComboChat) | |
| end | |
| Chat->>Auth: getProviderCredentials(provider) | |
| Auth-->>Chat: active account + tokens/api key | |
| Chat->>Core: handleChatCore(body, modelInfo, credentials) | |
| Core->>Core: detect source format | |
| Core->>Core: translate request to target format | |
| Core->>Exec: execute(provider, transformedBody) | |
| Exec->>Prov: upstream API call | |
| Prov-->>Exec: SSE/JSON response | |
| Exec-->>Core: response + metadata | |
| alt 401/403 | |
| Core->>Exec: refreshCredentials() | |
| Exec-->>Core: updated tokens | |
| Core->>Exec: retry request | |
| end | |
| Core->>Stream: translate/normalize stream to client format | |
| Stream-->>Client: SSE chunks / JSON response | |
| Stream->>Usage: extract usage + persist history/log | |
| ``` | |
| ## Combo + Account Fallback Flow | |
| ```mermaid | |
| flowchart TD | |
| A[Incoming model string] --> B{Is combo name?} | |
| B -- Yes --> C[Load combo models sequence] | |
| B -- No --> D[Single model path] | |
| C --> E[Try model N] | |
| E --> F[Resolve provider/model] | |
| D --> F | |
| F --> G[Select account credentials] | |
| G --> H{Credentials available?} | |
| H -- No --> I[Return provider unavailable] | |
| H -- Yes --> J[Execute request] | |
| J --> K{Success?} | |
| K -- Yes --> L[Return response] | |
| K -- No --> M{Fallback-eligible error?} | |
| M -- No --> N[Return error] | |
| M -- Yes --> O[Mark account unavailable cooldown] | |
| O --> P{Another account for provider?} | |
| P -- Yes --> G | |
| P -- No --> Q{In combo with next model?} | |
| Q -- Yes --> E | |
| Q -- No --> R[Return all unavailable] | |
| ``` | |
| Fallback decisions are driven by `open-sse/services/accountFallback.js` using status codes and error-message heuristics. | |
| ## OAuth Onboarding and Token Refresh Lifecycle | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| participant UI as Dashboard UI | |
| participant OAuth as /api/oauth/[provider]/[action] | |
| participant ProvAuth as Provider Auth Server | |
| participant DB as localDb | |
| participant Test as /api/providers/[id]/test | |
| participant Exec as Provider Executor | |
| UI->>OAuth: GET authorize or device-code | |
| OAuth->>ProvAuth: create auth/device flow | |
| ProvAuth-->>OAuth: auth URL or device code payload | |
| OAuth-->>UI: flow data | |
| UI->>OAuth: POST exchange or poll | |
| OAuth->>ProvAuth: token exchange/poll | |
| ProvAuth-->>OAuth: access/refresh tokens | |
| OAuth->>DB: createProviderConnection(oauth data) | |
| OAuth-->>UI: success + connection id | |
| UI->>Test: POST /api/providers/[id]/test | |
| Test->>Exec: validate credentials / optional refresh | |
| Exec-->>Test: valid or refreshed token info | |
| Test->>DB: update status/tokens/errors | |
| Test-->>UI: validation result | |
| ``` | |
| Refresh during live traffic is executed inside `open-sse/handlers/chatCore.js` via executor `refreshCredentials()`. | |
| ## Cloud Sync Lifecycle (Enable / Sync / Disable) | |
| ```mermaid | |
| sequenceDiagram | |
| autonumber | |
| participant UI as Endpoint Page UI | |
| participant Sync as /api/sync/cloud | |
| participant DB as localDb | |
| participant Cloud as External Cloud Sync | |
| participant Claude as ~/.claude/settings.json | |
| UI->>Sync: POST action=enable | |
| Sync->>DB: set cloudEnabled=true | |
| Sync->>DB: ensure API key exists | |
| Sync->>Cloud: POST /sync/{machineId} (providers/aliases/combos/keys) | |
| Cloud-->>Sync: sync result | |
| Sync->>Cloud: GET /{machineId}/v1/verify | |
| Sync-->>UI: enabled + verification status | |
| UI->>Sync: POST action=sync | |
| Sync->>Cloud: POST /sync/{machineId} | |
| Cloud-->>Sync: remote data | |
| Sync->>DB: update newer local tokens/status | |
| Sync-->>UI: synced | |
| UI->>Sync: POST action=disable | |
| Sync->>DB: set cloudEnabled=false | |
| Sync->>Cloud: DELETE /sync/{machineId} | |
| Sync->>Claude: switch ANTHROPIC_BASE_URL back to local (if needed) | |
| Sync-->>UI: disabled | |
| ``` | |
| Periodic sync is triggered by `CloudSyncScheduler` when cloud is enabled. | |
| ## Data Model and Storage Map | |
| ```mermaid | |
| erDiagram | |
| SETTINGS ||--o{ PROVIDER_CONNECTION : controls | |
| PROVIDER_NODE ||--o{ PROVIDER_CONNECTION : backs_compatible_provider | |
| PROVIDER_CONNECTION ||--o{ USAGE_ENTRY : emits_usage | |
| SETTINGS { | |
| boolean cloudEnabled | |
| number stickyRoundRobinLimit | |
| boolean requireLogin | |
| string password_hash | |
| } | |
| PROVIDER_CONNECTION { | |
| string id | |
| string provider | |
| string authType | |
| string name | |
| number priority | |
| boolean isActive | |
| string apiKey | |
| string accessToken | |
| string refreshToken | |
| string expiresAt | |
| string testStatus | |
| string lastError | |
| string rateLimitedUntil | |
| json providerSpecificData | |
| } | |
| PROVIDER_NODE { | |
| string id | |
| string type | |
| string name | |
| string prefix | |
| string apiType | |
| string baseUrl | |
| } | |
| MODEL_ALIAS { | |
| string alias | |
| string targetModel | |
| } | |
| COMBO { | |
| string id | |
| string name | |
| string[] models | |
| } | |
| API_KEY { | |
| string id | |
| string name | |
| string key | |
| string machineId | |
| boolean isActive | |
| } | |
| USAGE_ENTRY { | |
| string provider | |
| string model | |
| number prompt_tokens | |
| number completion_tokens | |
| string connectionId | |
| string timestamp | |
| } | |
| ``` | |
| Physical storage files: | |
| - main state: `${DATA_DIR}/db.json` (or `~/.9router/db.json`) | |
| - usage stats: `~/.9router/usage.json` | |
| - request log lines: `~/.9router/log.txt` | |
| - optional translator/request debug sessions: `<repo>/logs/...` | |
| ## Deployment Topology | |
| ```mermaid | |
| flowchart LR | |
| subgraph LocalHost[Developer Host] | |
| CLI[CLI Tools] | |
| Browser[Dashboard Browser] | |
| end | |
| subgraph ContainerOrProcess[9Router Runtime] | |
| Next[Next.js Server\nPORT=20128] | |
| Core[SSE Core + Executors] | |
| MainDB[(db.json)] | |
| UsageDB[(usage.json/log.txt)] | |
| end | |
| subgraph External[External Services] | |
| Providers[AI Providers] | |
| SyncCloud[Cloud Sync Service] | |
| end | |
| CLI --> Next | |
| Browser --> Next | |
| Next --> Core | |
| Next --> MainDB | |
| Core --> MainDB | |
| Core --> UsageDB | |
| Core --> Providers | |
| Next --> SyncCloud | |
| ``` | |
| ## Module Mapping (Decision-Critical) | |
| ### Route and API Modules | |
| - `src/app/api/v1/*`, `src/app/api/v1beta/*`: compatibility APIs | |
| - `src/app/api/providers*`: provider CRUD, validation, testing | |
| - `src/app/api/provider-nodes*`: custom compatible node management | |
| - `src/app/api/oauth/*`: OAuth/device-code flows | |
| - `src/app/api/keys*`: local API key lifecycle | |
| - `src/app/api/models/alias`: alias management | |
| - `src/app/api/combos*`: fallback combo management | |
| - `src/app/api/pricing`: pricing overrides for cost calculation | |
| - `src/app/api/usage/*`: usage and logs APIs | |
| - `src/app/api/sync/*` + `src/app/api/cloud/*`: cloud sync and cloud-facing helpers | |
| - `src/app/api/cli-tools/*`: local CLI config writers/checkers | |
| ### Routing and Execution Core | |
| - `src/sse/handlers/chat.js`: request parse, combo handling, account selection loop | |
| - `open-sse/handlers/chatCore.js`: translation, executor dispatch, retry/refresh handling, stream setup | |
| - `open-sse/executors/*`: provider-specific network and format behavior | |
| ### Translation Registry and Format Converters | |
| - `open-sse/translator/index.js`: translator registry and orchestration | |
| - Request translators: `open-sse/translator/request/*` | |
| - Response translators: `open-sse/translator/response/*` | |
| - Format constants: `open-sse/translator/formats.js` | |
| ### Persistence | |
| - `src/lib/localDb.js`: persistent config/state | |
| - `src/lib/usageDb.js`: usage history and rolling request logs | |
| ## Provider Executor Coverage | |
| Specialized executors: | |
| - `antigravity` | |
| - `gemini-cli` | |
| - `github` | |
| - `kiro` | |
| - `codex` | |
| - `cursor` | |
| Default executor path: | |
| - all other providers (including compatible node providers) use `open-sse/executors/default.js` | |
| ## Format Translation Coverage | |
| Detected source formats include: | |
| - `openai` | |
| - `openai-responses` | |
| - `claude` | |
| - `gemini` | |
| Target formats include: | |
| - OpenAI chat/Responses | |
| - Claude | |
| - Gemini/Gemini-CLI/Antigravity envelope | |
| - Kiro | |
| - Cursor | |
| Translations are selected dynamically based on source payload shape and provider target format. | |
| ## Failure Modes and Resilience | |
| ## 1) Account/Provider Availability | |
| - provider account cooldown on transient/rate/auth errors | |
| - account fallback before failing request | |
| - combo model fallback when current model/provider path is exhausted | |
| ## 2) Token Expiry | |
| - pre-check and refresh with retry for refreshable providers | |
| - 401/403 retry after refresh attempt in core path | |
| ## 3) Stream Safety | |
| - disconnect-aware stream controller | |
| - translation stream with end-of-stream flush and `[DONE]` handling | |
| - usage estimation fallback when provider usage metadata is missing | |
| ## 4) Cloud Sync Degradation | |
| - sync errors are surfaced but local runtime continues | |
| - scheduler has retry-capable logic, but periodic execution currently calls single-attempt sync by default | |
| ## 5) Data Integrity | |
| - DB shape migration/repair for missing keys | |
| - corrupt JSON reset safeguards for localDb and usageDb | |
| ## Observability and Operational Signals | |
| Runtime visibility sources: | |
| - console logs from `src/sse/utils/logger.js` | |
| - per-request usage aggregates in `usage.json` | |
| - textual request status log in `log.txt` | |
| - optional deep request/translation logs under `logs/` when `ENABLE_REQUEST_LOGS=true` | |
| - dashboard usage endpoints (`/api/usage/*`) for UI consumption | |
| ## Security-Sensitive Boundaries | |
| - JWT secret (`JWT_SECRET`) secures dashboard session cookie verification/signing | |
| - Initial password fallback (`INITIAL_PASSWORD`, default `123456`) must be overridden in real deployments | |
| - API key HMAC secret (`API_KEY_SECRET`) secures generated local API key format | |
| - Provider secrets (API keys/tokens) are persisted in local DB and should be protected at filesystem level | |
| - Cloud sync endpoints rely on API key auth + machine id semantics | |
| ## Environment and Runtime Matrix | |
| Environment variables actively used by code: | |
| - App/auth: `JWT_SECRET`, `INITIAL_PASSWORD` | |
| - Storage: `DATA_DIR` | |
| - Security hashing: `API_KEY_SECRET`, `MACHINE_ID_SALT` | |
| - Logging: `ENABLE_REQUEST_LOGS` | |
| - Sync/cloud URLing: `NEXT_PUBLIC_BASE_URL`, `NEXT_PUBLIC_CLOUD_URL` | |
| - Outbound proxy: `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` and lowercase variants | |
| - Platform/runtime helpers (not app-specific config): `APPDATA`, `NODE_ENV`, `PORT`, `HOSTNAME` | |
| ## Known Architectural Notes | |
| 1. `usageDb` currently stores under `~/.9router` and does not follow `DATA_DIR`. | |
| 2. `/api/v1/route.js` returns a static model list and is not the main models source used by `/v1/models`. | |
| 3. Request logger writes full headers/body when enabled; treat log directory as sensitive. | |
| 4. Cloud behavior depends on correct `NEXT_PUBLIC_BASE_URL` and cloud endpoint reachability. | |
| ## Operational Verification Checklist | |
| - Build from source: `cd /root/dev/9router && npm run build` | |
| - Build Docker image: `cd /root/dev/9router && docker build -t 9router .` | |
| - Start service and verify: | |
| - `GET /api/settings` | |
| - `GET /api/v1/models` | |
| - CLI target base URL should be `http://<host>:20128/v1` when `PORT=20128` | |