mirror of
https://github.com/decolua/9router.git
synced 2026-05-08 12:01:28 +00:00
feat(docker): add Docker setup, environment examples, and architecture docs
This commit is contained in:
32
.dockerignore
Normal file
32
.dockerignore
Normal file
@@ -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*
|
||||||
31
.env.example
Normal file
31
.env.example
Normal file
@@ -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
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -33,6 +33,7 @@ yarn-error.log*
|
|||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
@@ -47,6 +48,7 @@ logs/*
|
|||||||
source/*
|
source/*
|
||||||
.cursor/*
|
.cursor/*
|
||||||
docs/*
|
docs/*
|
||||||
|
!docs/ARCHITECTURE.md
|
||||||
test/*
|
test/*
|
||||||
bin/*
|
bin/*
|
||||||
open-sse/test/*
|
open-sse/test/*
|
||||||
|
|||||||
28
Dockerfile
Normal file
28
Dockerfile
Normal file
@@ -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"]
|
||||||
147
README.md
147
README.md
@@ -91,6 +91,27 @@ Claude Code/Codex/Gemini CLI/OpenClaw/Cursor/Cline Settings:
|
|||||||
|
|
||||||
**That's it!** Start coding with FREE AI models.
|
**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
|
## 💡 Key Features
|
||||||
@@ -539,7 +560,7 @@ Model: cc/claude-opus-4-6
|
|||||||
```bash
|
```bash
|
||||||
# Clone and install
|
# Clone and install
|
||||||
git clone https://github.com/decolua/9router.git
|
git clone https://github.com/decolua/9router.git
|
||||||
cd 9router/app
|
cd 9router
|
||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
@@ -547,7 +568,13 @@ npm run build
|
|||||||
export JWT_SECRET="your-secure-secret-change-this"
|
export JWT_SECRET="your-secure-secret-change-this"
|
||||||
export INITIAL_PASSWORD="your-password"
|
export INITIAL_PASSWORD="your-password"
|
||||||
export DATA_DIR="/var/lib/9router"
|
export DATA_DIR="/var/lib/9router"
|
||||||
|
export PORT="20128"
|
||||||
|
export HOSTNAME="0.0.0.0"
|
||||||
export NODE_ENV="production"
|
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
|
# Start
|
||||||
npm run start
|
npm run start
|
||||||
@@ -562,23 +589,72 @@ pm2 startup
|
|||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Build image (from repository root)
|
||||||
docker build -t 9router .
|
docker build -t 9router .
|
||||||
|
|
||||||
|
# Run container (command used in current setup)
|
||||||
docker run -d \
|
docker run -d \
|
||||||
|
--name 9router \
|
||||||
-p 20128:20128 \
|
-p 20128:20128 \
|
||||||
-e JWT_SECRET="your-secure-secret" \
|
--env-file /root/dev/9router/.env \
|
||||||
-e INITIAL_PASSWORD="your-password" \
|
|
||||||
-v 9router-data:/app/data \
|
-v 9router-data:/app/data \
|
||||||
|
-v 9router-usage:/root/.9router \
|
||||||
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
|
### Environment Variables
|
||||||
|
|
||||||
| Variable | Default | Description |
|
| Variable | Default | Description |
|
||||||
|----------|---------|-------------|
|
|----------|---------|-------------|
|
||||||
| `JWT_SECRET` | Auto-generated | **MUST change in production!** |
|
| `JWT_SECRET` | `9router-default-secret-change-me` | JWT signing secret for dashboard auth cookie (**change in production**) |
|
||||||
| `DATA_DIR` | `~/.9router` | Database storage path |
|
| `INITIAL_PASSWORD` | `123456` | First login password when no saved hash exists |
|
||||||
| `INITIAL_PASSWORD` | `123456` | Dashboard login password |
|
| `DATA_DIR` | `~/.9router` | Main app database location (`db.json`) |
|
||||||
| `NODE_ENV` | `development` | Set to `production` for deploy |
|
| `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: `<repo>/logs/...` when `ENABLE_REQUEST_LOGS=true`.
|
||||||
|
- Usage storage currently follows `~/.9router` path logic and is independent from `DATA_DIR`.
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -648,12 +724,26 @@ docker run -d \
|
|||||||
- Switch primary model to GLM/MiniMax
|
- Switch primary model to GLM/MiniMax
|
||||||
- Use free tier (Gemini CLI, iFlow) for non-critical tasks
|
- 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
|
## 🛠️ Tech Stack
|
||||||
|
|
||||||
- **Runtime**: Node.js 20+
|
- **Runtime**: Node.js 20+
|
||||||
- **Framework**: Next.js 15
|
- **Framework**: Next.js 16
|
||||||
- **UI**: React 19 + Tailwind CSS 4
|
- **UI**: React 19 + Tailwind CSS 4
|
||||||
- **Database**: LowDB (JSON file-based)
|
- **Database**: LowDB (JSON file-based)
|
||||||
- **Streaming**: Server-Sent Events (SSE)
|
- **Streaming**: Server-Sent Events (SSE)
|
||||||
@@ -688,6 +778,47 @@ Authorization: Bearer your-api-key
|
|||||||
→ Returns all models + combos in OpenAI format
|
→ 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
|
## 📧 Support
|
||||||
|
|||||||
556
docs/ARCHITECTURE.md
Normal file
556
docs/ARCHITECTURE.md
Normal file
@@ -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: `<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`
|
||||||
Reference in New Issue
Block a user