| # 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` |
|
|