diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..5b921cd9 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,32 @@ +# VCS +.git +**/.git + +# Editor +.vscode +**/.vscode + +# Dependencies and build output +node_modules +.next +out +build +dist +coverage + +# Runtime data and logs +data +logs + +# Local env files (inject at runtime via --env-file or -e) +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Debug logs +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..238a5b21 --- /dev/null +++ b/.env.example @@ -0,0 +1,31 @@ +# 9Router environment contract +# This file reflects actual runtime usage in the current codebase. + +# Required +JWT_SECRET=change-me-to-a-long-random-secret +INITIAL_PASSWORD=change-me +DATA_DIR=/var/lib/9router + +# Recommended runtime variables +PORT=20128 +NODE_ENV=production + +# Recommended security and ops variables +API_KEY_SECRET=endpoint-proxy-api-key-secret +MACHINE_ID_SALT=endpoint-proxy-salt +ENABLE_REQUEST_LOGS=false + +# Cloud sync variables +# Must point to this running instance so internal sync jobs can call /api/sync/cloud. +NEXT_PUBLIC_BASE_URL=http://localhost:20128 +NEXT_PUBLIC_CLOUD_URL=https://9router.com + +# Optional outbound proxy variables for upstream provider calls +# Lowercase variants are also supported: http_proxy, https_proxy, all_proxy, no_proxy +# HTTP_PROXY=http://127.0.0.1:7890 +# HTTPS_PROXY=http://127.0.0.1:7890 +# ALL_PROXY=socks5://127.0.0.1:7890 +# NO_PROXY=localhost,127.0.0.1 + +# Currently unused by application runtime (kept as reference) +# INSTANCE_NAME=9router diff --git a/.gitignore b/.gitignore index 856e9967..450bc617 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +!.env.example # vercel .vercel @@ -47,6 +48,7 @@ logs/* source/* .cursor/* docs/* +!docs/ARCHITECTURE.md test/* bin/* open-sse/test/* diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..136366be --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +FROM node:20-alpine AS builder +WORKDIR /app + +COPY package*.json ./ +RUN if [ -f package-lock.json ]; then npm ci --no-audit --no-fund; else npm install --no-audit --no-fund; fi + +COPY . ./ +RUN npm run build + +FROM node:20-alpine AS runner +WORKDIR /app + +LABEL org.opencontainers.image.title="9router" + +ENV NODE_ENV=production +ENV PORT=20128 +ENV HOSTNAME=0.0.0.0 + +# Runtime writable location for localDb when DATA_DIR is configured to /app/data +RUN mkdir -p /app/data + +COPY --from=builder /app/public ./public +COPY --from=builder /app/.next/static ./.next/static +COPY --from=builder /app/.next/standalone ./ + +EXPOSE 20128 + +CMD ["node", "server.js"] diff --git a/README.md b/README.md index 3955e956..fa3ff0fc 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,27 @@ Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Settings: **That's it!** Start coding with FREE AI models. +**Alternative: run from source (this repository):** + +This repository package is private (`9router-app`), so source/Docker execution is the expected local development path. + +```bash +cp .env.example .env +npm install +PORT=20128 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run dev +``` + +Production mode: + +```bash +npm run build +PORT=20128 HOSTNAME=0.0.0.0 NEXT_PUBLIC_BASE_URL=http://localhost:20128 npm run start +``` + +Default URLs: +- Dashboard: `http://localhost:20128/dashboard` +- OpenAI-compatible API: `http://localhost:20128/v1` + --- ## 💡 Key Features @@ -539,7 +560,7 @@ Model: cc/claude-opus-4-6 ```bash # Clone and install git clone https://github.com/decolua/9router.git -cd 9router/app +cd 9router npm install npm run build @@ -547,7 +568,13 @@ npm run build export JWT_SECRET="your-secure-secret-change-this" export INITIAL_PASSWORD="your-password" export DATA_DIR="/var/lib/9router" +export PORT="20128" +export HOSTNAME="0.0.0.0" export NODE_ENV="production" +export NEXT_PUBLIC_BASE_URL="http://localhost:20128" +export NEXT_PUBLIC_CLOUD_URL="https://9router.com" +export API_KEY_SECRET="endpoint-proxy-api-key-secret" +export MACHINE_ID_SALT="endpoint-proxy-salt" # Start npm run start @@ -562,23 +589,72 @@ pm2 startup ### Docker ```bash +# Build image (from repository root) docker build -t 9router . + +# Run container (command used in current setup) docker run -d \ + --name 9router \ -p 20128:20128 \ - -e JWT_SECRET="your-secure-secret" \ - -e INITIAL_PASSWORD="your-password" \ + --env-file /root/dev/9router/.env \ -v 9router-data:/app/data \ + -v 9router-usage:/root/.9router \ 9router ``` +Portable command (if you are already at repository root): + +```bash +docker run -d \ + --name 9router \ + -p 20128:20128 \ + --env-file ./.env \ + -v 9router-data:/app/data \ + -v 9router-usage:/root/.9router \ + 9router +``` + +Container defaults: +- `PORT=20128` +- `HOSTNAME=0.0.0.0` + +Useful commands: + +```bash +docker logs -f 9router +docker restart 9router +docker stop 9router && docker rm 9router +``` + ### Environment Variables | Variable | Default | Description | |----------|---------|-------------| -| `JWT_SECRET` | Auto-generated | **MUST change in production!** | -| `DATA_DIR` | `~/.9router` | Database storage path | -| `INITIAL_PASSWORD` | `123456` | Dashboard login password | -| `NODE_ENV` | `development` | Set to `production` for deploy | +| `JWT_SECRET` | `9router-default-secret-change-me` | JWT signing secret for dashboard auth cookie (**change in production**) | +| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists | +| `DATA_DIR` | `~/.9router` | Main app database location (`db.json`) | +| `PORT` | framework default | Service port (`20128` in examples) | +| `HOSTNAME` | framework default | Bind host (Docker defaults to `0.0.0.0`) | +| `NODE_ENV` | runtime default | Set `production` for deploy | +| `NEXT_PUBLIC_BASE_URL` | `http://localhost:3000` | Internal base URL used by cloud sync jobs | +| `NEXT_PUBLIC_CLOUD_URL` | `https://9router.com` | Cloud sync endpoint base URL | +| `API_KEY_SECRET` | `endpoint-proxy-api-key-secret` | HMAC secret for generated API keys | +| `MACHINE_ID_SALT` | `endpoint-proxy-salt` | Salt for stable machine ID hashing | +| `ENABLE_REQUEST_LOGS` | `false` | Enables request/response logs under `logs/` | +| `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` | empty | Optional outbound proxy for upstream provider calls | + +Notes: +- Lowercase proxy variables are also supported: `http_proxy`, `https_proxy`, `all_proxy`, `no_proxy`. +- `.env` is not baked into Docker image (`.dockerignore`); inject runtime config with `--env-file` or `-e`. +- On Windows, `APPDATA` can be used for local storage path resolution. +- `INSTANCE_NAME` appears in older docs/env templates, but is currently not used at runtime. + +### Runtime Files and Storage + +- Main app state: `${DATA_DIR}/db.json` (providers, combos, aliases, keys, settings), managed by `src/lib/localDb.js`. +- Usage history and logs: `~/.9router/usage.json` and `~/.9router/log.txt`, managed by `src/lib/usageDb.js`. +- Optional request/translator logs: `/logs/...` when `ENABLE_REQUEST_LOGS=true`. +- Usage storage currently follows `~/.9router` path logic and is independent from `DATA_DIR`. @@ -648,12 +724,26 @@ docker run -d \ - Switch primary model to GLM/MiniMax - Use free tier (Gemini CLI, iFlow) for non-critical tasks +**Dashboard opens on wrong port** +- Set `PORT=20128` and `NEXT_PUBLIC_BASE_URL=http://localhost:20128` + +**Cloud sync errors** +- Verify `NEXT_PUBLIC_BASE_URL` points to your running instance +- Verify `NEXT_PUBLIC_CLOUD_URL` points to your expected cloud endpoint + +**First login not working** +- Check `INITIAL_PASSWORD` in `.env` +- If unset, fallback password is `123456` + +**No request logs under `logs/`** +- Set `ENABLE_REQUEST_LOGS=true` + --- ## 🛠️ Tech Stack - **Runtime**: Node.js 20+ -- **Framework**: Next.js 15 +- **Framework**: Next.js 16 - **UI**: React 19 + Tailwind CSS 4 - **Database**: LowDB (JSON file-based) - **Streaming**: Server-Sent Events (SSE) @@ -688,6 +778,47 @@ Authorization: Bearer your-api-key → Returns all models + combos in OpenAI format ``` +### Compatibility Endpoints + +- `POST /v1/chat/completions` +- `POST /v1/messages` +- `POST /v1/responses` +- `GET /v1/models` +- `POST /v1/messages/count_tokens` +- `GET /v1beta/models` +- `POST /v1beta/models/{...path}` (Gemini-style `generateContent`) +- `POST /v1/api/chat` (Ollama-style transform path) + +### Dashboard and Management API + +- Auth/settings: `/api/auth/login`, `/api/auth/logout`, `/api/settings`, `/api/settings/require-login` +- Provider management: `/api/providers`, `/api/providers/[id]`, `/api/providers/[id]/test`, `/api/providers/[id]/models`, `/api/providers/validate`, `/api/provider-nodes*` +- OAuth flows: `/api/oauth/[provider]/[action]` (+ provider-specific imports like Cursor/Kiro) +- Routing config: `/api/models/alias`, `/api/combos*`, `/api/keys*`, `/api/pricing` +- Usage/logs: `/api/usage/history`, `/api/usage/logs`, `/api/usage/request-logs`, `/api/usage/[connectionId]` +- Cloud sync: `/api/sync/cloud`, `/api/sync/initialize`, `/api/cloud/*` +- CLI helpers: `/api/cli-tools/claude-settings`, `/api/cli-tools/codex-settings`, `/api/cli-tools/droid-settings`, `/api/cli-tools/openclaw-settings` + +### Authentication Behavior + +- Dashboard routes (`/dashboard/*`) use `auth_token` cookie protection. +- Login uses saved password hash when present; otherwise it falls back to `INITIAL_PASSWORD`. +- `requireLogin` can be toggled via `/api/settings/require-login`. + +### Request Processing (High Level) + +1. Client sends request to `/v1/*`. +2. Route handler calls `handleChat` (`src/sse/handlers/chat.js`). +3. Model is resolved (direct provider/model or alias/combo resolution). +4. Credentials are selected from local DB with account availability filtering. +5. `handleChatCore` (`open-sse/handlers/chatCore.js`) detects format and translates request. +6. Provider executor sends upstream request. +7. Stream is translated back to client format when needed. +8. Usage/logging is recorded (`src/lib/usageDb.js`). +9. Fallback applies on provider/account/model errors according to combo rules. + +Full architecture reference: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) + --- ## 📧 Support diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md new file mode 100644 index 00000000..1a31ddaf --- /dev/null +++ b/docs/ARCHITECTURE.md @@ -0,0 +1,556 @@ +# 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 + } + + 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: `/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://:20128/v1` when `PORT=20128`