Origin Story: This project is a ground-up restructuring of freellmapi β a brilliant gateway that aggregated free-tier overseas models (GPT-4, Claude, Gemini) behind a single OpenAI-compatible endpoint. We took its core idea (Express proxy + key pool + SQLite + AES encryption) and flipped the thesis: instead of chasing free overseas quota, we optimize for Chinese domestic models with intent Γ difficulty routing β while keeping the best of overseas (Claude) accessible through the same unified endpoint. Same DNA, evolved philosophy.
freellmapi llm-keypool Target Overseas free tiers (GPT-4, Claude, Gemini) Domestic + overseas (DeepSeek, GLM, Qwen, Doubao, MiniMax, Kimi, Claude) Routing Round-robin / first-available Intent Γ Difficulty matrix (8 intents Γ 3 levels) Selection Cheapest free model that works Best-fit model for the task (codeβcoder, reasonβR1, chatβflash) Key mgmt Free key pooling Encrypted pool + free-tier-first + 429 auto-fallback Dashboard React SPA React + Vite SPA (4-page admin: Dashboard / Keys / Models / Playground) Philosophy Maximize free overseas quota Match the right model to the right job (domestic-first, global-ready)
The Chinese LLM landscape is fundamentally different from the overseas one:
- Fragmentation β 12+ providers, 38+ models, each with different APIs, rate limits, and strengths. There's no single "best" model β DeepSeek R1 dominates reasoning, GLM-4-Flash is the fastest free chat, Qwen-Long handles 10M context, Kimi excels at search-augmented tasks, Claude leads in complex reasoning and code, GPT-4o and Gemini 2.5 Pro push the frontier on multimodal tasks, and Agnes AI offers free 1M-context access.
- Free tiers everywhere β Unlike the overseas market where free quota is scarce, Chinese providers offer generous free tiers. The challenge isn't finding free access β it's picking the right model for the right job.
- Intent matters β Sending a coding task to a general chat model wastes tokens and gets worse results. Sending a simple "hello" to DeepSeek R1 wastes its reasoning capability and costs more time. You need routing, not just proxying.
You send: POST /v1/chat/completions {"model": "auto", "messages": [...]}
We think: intent=code, difficulty=hard β what's the best coder for hard tasks?
We route: deepseek-coder via DeepSeek provider (with 429 fallback to qwen-coder)
You get: OpenAI-compatible response + routing metadata headers
| Feature | What it means for you | |
|---|---|---|
| π§ | Intent Γ Difficulty Routing | 8 intents Γ 3 difficulty levels β 24-cell matrix auto-picks the right model across 12 providers |
| π | Key Pool with AES-256-GCM | Add multiple keys per provider; encrypted at rest, free-tier-first selection |
| π‘οΈ | Automatic Fallback | Provider A returns 429? Cooldown that key, switch to Provider B β transparently |
| π‘ | Streaming Passthrough | SSE streams flow through untouched β no buffering, no content-type mangling |
| π | Drop-in OpenAI Compatible | Change base_url, keep everything else β works with LangChain, Cursor, Continue, Claude Code, etc. |
| π | Domestic-First + Global | DeepSeek, GLM, Qwen, Doubao, MiniMax, Kimi, Agnes (free tiers) + OpenAI, Anthropic, Gemini, OpenRouter (overseas) |
| π€ | Multi-Protocol Support | Anthropic Claude (Messages API), Google Gemini (Generative AI API), OpenAI-compatible β all transparent to clients |
| π | React Admin Dashboard | 4-page SPA: real-time monitoring, key management, model routing matrix, interactive playground |
| πͺΆ | Zero Dependencies Bloat | Express + better-sqlite3 + dotenv. That's it. No Redis, no Docker, no Kubernetes |
git clone https://github.com/xiaopengs/llm-keypool.git
cd llm-keypool
npm install && npm run build
# Build the admin dashboard
cd client && npm install && npm run build && cd ..
# Generate encryption key (required for API key storage)
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
# Configure
cp .env.example .env
# Edit .env: paste the key as ENCRYPTION_KEY, set PROXY_TOKEN
# Launch
npm start
# β [keypool] v2.0.0 listening on :3131
# β [keypool] Dashboard: http://localhost:3131/dashboard# Add your first API key (free tier β most Chinese providers offer one)
curl -X POST http://localhost:3131/api/keys \
-H "Authorization: Bearer YOUR_PROXY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"deepseek","api_key":"sk-xxx","tier":"free"}'
# Send a request β watch it auto-route
curl http://localhost:3131/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"auto","messages":[{"role":"user","content":"Write a Python quicksort"}]}'
# Response headers tell you where it went:
# X-Routed-Provider: deepseek
# X-Routed-Model: deepseek-coder
# X-Intent-Detected: code
# X-Difficulty: medium
# Use Claude (Anthropic) β just specify the model
curl http://localhost:3131/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"claude-sonnet-4","messages":[{"role":"user","content":"Explain quantum computing"}]}'
# Open the React admin dashboard to manage and monitor everything
open http://localhost:3131/dashboardThat's it. One key, one endpoint, zero configuration beyond
.env. The routing matrix has sensible defaults β you only customize when you want to.
llm-keypool speaks pure OpenAI API protocol. Any tool that lets you configure a custom base_url or api_base works out of the box.
Claude Code supports custom OpenAI-compatible endpoints via environment variables:
# Set in your shell or .bashrc/.zshrc
export OPENAI_API_KEY="anything" # keypool handles upstream auth
export OPENAI_BASE_URL="http://localhost:3131/v1"
# Now Claude Code routes through keypool
claude "Write a TypeScript HTTP server"
# β Routes to code:medium β qwen-coder (or your configured model)Or configure per-project in .claude/settings.json:
{
"env": {
"OPENAI_API_KEY": "anything",
"OPENAI_BASE_URL": "http://localhost:3131/v1"
}
}Settings β Models β OpenAI API Base URL:
API Base URL: http://localhost:3131/v1
API Key: anything
Model: auto
Or use intent-prefixed models for specific tasks:
| Model in Cursor | Routes to |
|---|---|
auto |
Automatic intent + difficulty routing |
code:deepseek-coder |
Force coding β DeepSeek Coder |
reasoning:deepseek-r1 |
Force reasoning β DeepSeek R1 |
deepseek-v3 |
Direct model β no routing |
Edit ~/.continue/config.json:
{
"models": [
{
"title": "llm-keypool (auto)",
"provider": "openai",
"model": "auto",
"apiBase": "http://localhost:3131/v1",
"apiKey": "anything"
},
{
"title": "llm-keypool (code)",
"provider": "openai",
"model": "code:deepseek-coder",
"apiBase": "http://localhost:3131/v1",
"apiKey": "anything"
}
]
}In extension settings:
API Provider: OpenAI Compatible
Base URL: http://localhost:3131/v1
API Key: anything
Model ID: auto
aider --openai-api-base http://localhost:3131/v1 \
--model openai/auto \
--openai-api-key anythingfrom openai import OpenAI
client = OpenAI(
base_url="http://localhost:3131/v1",
api_key="anything"
)
# Auto-route
response = client.chat.completions.create(
model="auto",
messages=[{"role": "user", "content": "Explain transformers"}]
)
# Force reasoning
response = client.chat.completions.create(
model="reasoning:deepseek-r1",
messages=[{"role": "user", "content": "Prove the halting problem"}]
)Add to your .bashrc / .zshrc:
# Route all OpenAI calls through keypool
export OPENAI_API_KEY="anything"
export OPENAI_API_BASE="http://localhost:3131/v1"
# Or for specific tools
export DEEPSEEK_API_KEY="anything"
export DEEPSEEK_API_BASE="http://localhost:3131/v1"Tip: The
modelfield is your routing control plane."auto"for hands-off,"intent:alias"for explicit control,"alias"for direct selection. See Routing for details.
This is the core innovation over freellmapi's round-robin approach. Every request goes through a 3-stage pipeline:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Request Pipeline β
β β
β 1. INTENT 2. DIFFICULTY 3. RESOLVE β
β βββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β Explicit: β β msg length β β 8 Γ 3 β β
β β prefix: βββββΆβ tool count βββββΆβ MATRIX β β
β β header: β β conv depth β β lookup β β
β β Implicit: β β max_tokens β β β β β
β β keywords β β system promptβ β βΌ β β
β β patterns β β β β β model_alias β β
β βββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β β β β
β chat/code/ easy/medium/ deepseek-v3/ β
β reasoning/... hard glm-4-flash/... β
β β β β
β score: 0-6 key pick + β
β rate limit + β
β fallback β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Signal | Example | Confidence |
|---|---|---|
| Model prefix | "model": "code:deepseek-v3" |
1.0 (explicit) |
| X-Intent header | X-Intent: reasoning |
1.0 (explicit) |
| Content keywords | "ε代η ", "debug", "implement" | 0.3β1.0 (implicit) |
| Tool names | code_interpreter, file_edit |
0.3 (implicit) |
| Fallback | none matched | 0.5 β defaults to chat |
8 intents: chat Β· code Β· reasoning Β· creative Β· search Β· vision Β· translation Β· summarization
The gateway evaluates request complexity on a 0β6 scale across multiple signals:
| Signal | Easy (+0) | Medium (+1) | Hard (+2) |
|---|---|---|---|
| Message length | <1K chars | 1Kβ4K | >4K |
| Tool count | 0 | 1β3 | >3 |
| Conversation depth | β€5 turns | 5β10 | >10 |
| System prompt | absent | present | complex |
| max_tokens hint | β€200 | 200β1K | >1K |
Score β₯3 β hard, β₯1 β medium, else β easy
This is what makes keypool different: freellmapi treated every request the same. We ask "what are you trying to do?" and "how hard is it?" β then route accordingly.
| Intent | Easy | Medium | Hard |
|---|---|---|---|
| chat | glm-4-flash | deepseek-v3 | qwen-max |
| code | glm-4-flash | qwen-coder | deepseek-coder |
| reasoning | deepseek-v3 | deepseek-r1 | minimax-m1 |
| creative | doubao-lite | minimax-creative | glm-z1 |
| search | glm-4-flash | kimi | qwen-max |
| vision | glm-4v | qwen-vl | kimi-vl |
| translation | qwen-turbo | deepseek-v3 | glm-4 |
| summarization | glm-4-flash | qwen-long | glm-4-long |
Override any cell via
PUT /api/routingβ no code changes, no restarts.
| Provider | Base URL | Auth | Notes |
|---|---|---|---|
| DeepSeek | api.deepseek.com/v1 |
Bearer token | Most popular free tier; R1 is best-in-class reasoning |
| ζΊθ°± GLM | open.bigmodel.cn/api/paas/v4 |
Bearer token | GLM-4-Flash: 100 RPM free; GLM-4-Long: 1M context |
| ιδΉ Qwen | dashscope.aliyuncs.com/compatible-mode/v1 |
Bearer token | Qwen-Long: 10M context; best multi-variant lineup |
| θ±ε Doubao | ark.cn-beijing.volces.com/api/v3 |
Bearer token | Volcengine/ByteDance; uses endpoint_id mapping |
| MiniMax | api.minimax.chat/v1 |
Bearer token | M1: 1M context reasoning; abab6.5s for creative |
| Kimi/Moonshot | api.moonshot.cn/v1 |
Bearer token | Best search-augmented chat; VL for multimodal |
| OpenAI | api.openai.com/v1 |
Bearer token | GPT-4o, GPT-4o Mini, o1, o3-mini; official API |
| Anthropic | api.anthropic.com/v1 |
x-api-key header | Claude Sonnet 4, Opus 4, Haiku 3.5; protocol adaptation |
| Google Gemini | generativelanguage.googleapis.com/v1beta |
API key (query param) | Gemini 2.5 Pro/Flash, 2.0 Flash; Generative AI API |
| OpenRouter | openrouter.ai/api/v1 |
Bearer token | Unified access to 200+ models via OpenAI-compatible API |
| Agnes AI | apihub.agnes-ai.com/v1 |
Bearer token | ζ ιζε θ΄Ήηε ¨ζ¨‘ζ AIοΌAgnes-2.0-Flash: 1M δΈδΈζ |
| Custom | User-configured | Bearer token | Any OpenAI-compatible endpoint (vLLM, Ollama, LiteLLM, etc.) |
Most Chinese providers offer free tiers with no credit card required:
| Provider | Sign-up URL | Free Tier |
|---|---|---|
| DeepSeek | platform.deepseek.com | 50 RPM, 50K TPM |
| ζΊθ°± GLM | open.bigmodel.cn | GLM-4-Flash: 100 RPM free |
| ιδΉ Qwen | dashscope.console.aliyun.com | Free quota on all models |
| θ±ε Doubao | console.volcengine.com/ark | Free tier available |
| MiniMax | platform.minimax.chat | Free quota |
| Kimi/Moonshot | platform.moonshot.cn | Free tier available |
| OpenAI | platform.openai.com | API usage-based pricing |
| Anthropic | console.anthropic.com | API usage-based pricing |
| Google Gemini | aistudio.google.com | Free tier available |
| OpenRouter | openrouter.ai | Unified billing for 200+ models |
| Agnes AI | agnes-ai.com | ζ ιζε θ΄ΉοΌζ ιδΏ‘η¨ε‘ |
| Custom | N/A | Depends on your endpoint |
Doubao uses Volcengine Ark's endpoint_id system instead of model names. The gateway handles this transparently:
# You use the stable alias
curl ... -d '{"model":"doubao-pro",...}'
# We translate to the endpoint_id under the hood
# β {"model": "ep-xxxx", ...} (configured in DB)Anthropic's Messages API differs significantly from OpenAI's Chat Completions API. The gateway handles all protocol differences transparently:
| Aspect | OpenAI Format | Anthropic Format | Gateway Action |
|---|---|---|---|
| Endpoint | /v1/chat/completions |
/v1/messages |
Auto-redirect |
| Auth | Authorization: Bearer |
x-api-key header |
Auto-convert |
| System msg | In messages array | Top-level system field |
Auto-extract |
| Max tokens | Optional | Required (default 4096) | Auto-inject |
| Response | choices[0].message.content |
content[0].text |
Auto-transform |
# You send OpenAI-compatible format
curl http://localhost:3131/v1/chat/completions \
-d '{"model":"claude-sonnet-4","messages":[{"role":"user","content":"Hello!"}]}'
# We convert to Anthropic format, call their API,
# then transform the response back to OpenAI formatStreaming: SSE streams pass through with X-Upstream-Format: anthropic header. Clients should handle Anthropic's SSE event format for streaming Claude requests.
DeepSeek (3 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
deepseek-v3 |
deepseek-chat | 64K | chat, code | medium |
deepseek-r1 |
deepseek-reasoner | 64K | reasoning | hard |
deepseek-coder |
deepseek-chat | 128K | code | medium |
ζΊθ°± GLM (5 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
glm-4 |
glm-4 | 128K | chat, reasoning | medium |
glm-4-flash |
glm-4-flash | 128K | chat, search, translation | easy |
glm-z1 |
glm-z1-air | 128K | reasoning | hard |
glm-4v |
glm-4v | 128K | vision | medium |
glm-4-long |
glm-4-long | 1M | chat, summarization | medium |
ιδΉ Qwen (6 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
qwen-max |
qwen-max | 32K | chat, reasoning | hard |
qwen-plus |
qwen-plus | 131K | chat, code | medium |
qwen-turbo |
qwen-turbo | 131K | chat, search, translation | easy |
qwen-coder |
qwen-coder-plus | 131K | code | medium |
qwen-vl |
qwen-vl-max | 32K | vision | medium |
qwen-long |
qwen-long | 10M | summarization | medium |
θ±ε Doubao (3 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
doubao-pro |
doubao-pro-32k | 32K | chat, creative | medium |
doubao-lite |
doubao-lite-32k | 32K | chat | easy |
doubao-pro-128k |
doubao-pro-128k | 128K | chat, summarization | medium |
MiniMax (3 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
minimax-m1 |
MiniMax-M1 | 1M | chat, reasoning | hard |
minimax-text |
abab6.5s-chat | 245K | chat | medium |
minimax-creative |
abab6.5s-chat | 245K | creative | medium |
Kimi / Moonshot (4 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
kimi |
moonshot-v1-auto | 128K | chat, search | medium |
kimi-8k |
moonshot-v1-8k | 8K | chat | easy |
kimi-32k |
moonshot-v1-32k | 32K | chat, summarization | medium |
kimi-vl |
kimi-vl-audio | 128K | vision | medium |
Anthropic Claude (3 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
claude-sonnet-4 |
claude-sonnet-4-20250514 | 200K | chat, code, reasoning, creative | hard |
claude-opus-4 |
claude-opus-4-20250514 | 200K | reasoning, code, creative | hard |
claude-haiku-3.5 |
claude-3-5-haiku-20241022 | 200K | chat, search, translation | easy |
OpenAI (4 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
gpt-4o |
gpt-4o | 128K | chat, code, reasoning, creative | hard |
gpt-4o-mini |
gpt-4o-mini | 128K | chat, search, translation | easy |
o1 |
o1 | 200K | reasoning | hard |
o3-mini |
o3-mini | 200K | reasoning, code | medium |
Google Gemini (3 models)
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
gemini-2.5-pro |
gemini-2.5-pro-preview-06-05 | 1M | reasoning, code, creative | hard |
gemini-2.5-flash |
gemini-2.5-flash-preview-05-20 | 1M | chat, code, search | medium |
gemini-2.0-flash |
gemini-2.0-flash | 1M | chat, search, translation | easy |
OpenRouter (4 models)
OpenRouter provides unified access to 200+ models. These are pre-configured aliases for popular models:
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
openrouter:claude-sonnet-4 |
anthropic/claude-sonnet-4 | 200K | chat, code, reasoning, creative | hard |
openrouter:gpt-4o |
openai/gpt-4o | 128K | chat, code, reasoning, creative | hard |
openrouter:gemini-2.5-pro |
google/gemini-2.5-pro-preview | 1M | reasoning, code, creative | hard |
openrouter:deepseek-r1 |
deepseek/deepseek-r1 | 64K | reasoning | hard |
Agnes AI (1 model)
Agnes AI provides free multimodal AI services. Currently only the text model is supported (image/video generation requires different API endpoints):
| Alias | API Model | Context | Intent | Difficulty |
|---|---|---|---|---|
agnes-2.0-flash |
agnes-2.0-flash | 1M | chat, code, reasoning, creative | medium |
Note: Agnes AI also offers image generation (Agnes-Image-2.1-Flash) and video generation (Agnes-2.0-Video) models, but these require different API endpoints and are not yet supported by llm-keypool.
Custom Endpoint (user-configured)
The custom provider supports any OpenAI-compatible API endpoint (vLLM, Ollama, LiteLLM, etc.). No models are pre-seeded β you define them via the admin API:
# Add a custom endpoint
curl -X POST http://localhost:3131/api/keys \
-H "Authorization: Bearer YOUR_PROXY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"provider":"custom","api_key":"your-key","label":"My vLLM","base_url":"http://localhost:8000/v1"}'llm-keypool ships with a React + Vite admin dashboard at http://localhost:3131/dashboard β a full-featured SPA for managing your gateway.
| Page | Route | Description |
|---|---|---|
| Dashboard | /dashboard/ |
Real-time monitoring: stats overview, timeline charts, live request table, provider distribution, intentΓdifficulty heatmap |
| Keys | /dashboard/keys |
API key management: add/remove/toggle keys per provider, free/paid tier, masked display |
| Models | /dashboard/models |
Model registry & routing matrix: 38 models across 12 providers, intentΓdifficulty assignments, provider badges |
| Playground | /dashboard/playground |
Interactive chat: test any model through the gateway with live streaming responses, routing metadata display |
| Layer | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build | Vite 5 + PostCSS + Tailwind CSS 3 |
| UI | shadcn/ui (Card, Button, Table, Badge, Switch, Select...) + Radix UI |
| Data | TanStack React Query 5 (server state management) |
| Charts | Recharts (timeline, heatmap) |
| Icons | Lucide React |
| Routing | React Router DOM 6 |
The dashboard requires the same PROXY_TOKEN as the admin API. Enter it in the auth gate on first visit β stored in localStorage for convenience.
The original single-file HTML dashboard is still available at http://localhost:3131/legacy-dashboard for backward compatibility. It provides the same real-time monitoring without requiring a build step.
# Build the dashboard for production
cd client && npm install && npm run build && cd ..
# Output: client/dist/ β served at /dashboard
# Development with hot reload
cd client && npm run dev
# Vite dev server at http://localhost:5173, proxies API to :3131POST /v1/chat/completions
Content-Type: application/json
Request body β 100% OpenAI-compatible, plus:
| Field | Type | Description |
|---|---|---|
model |
string | Model alias, "auto", or "intent:alias" (e.g. "code:deepseek-v3") |
messages |
array | Required. Same format as OpenAI |
stream |
boolean | true for SSE streaming |
temperature, top_p, max_tokens, tools, etc. |
β | All pass through |
Model field routing modes:
model value |
Behavior | Example |
|---|---|---|
"auto" |
Full auto: classify intent + assess difficulty β matrix lookup | Chat, code, reasoning β all automatic |
"intent:alias" |
Explicit intent, auto difficulty | "code:auto" β best code model for this difficulty |
"intent:specific" |
Explicit intent + explicit model | "code:deepseek-coder" β force DeepSeek Coder |
"model-alias" |
Direct model, no routing | "deepseek-r1" β go straight to R1 |
Response headers (routing metadata):
X-Routed-Provider: deepseek
X-Routed-Model: deepseek-coder
X-Route-Latency-Ms: 342
X-Intent-Detected: code
X-Intent-Confidence: 1.0
X-Intent-Source: explicit
X-Difficulty: medium
X-Difficulty-Score: 2.0
GET /v1/models
Returns OpenAI-format model list with extra fields (display_name, intent_tags, context_window, etc.).
POST /api/keys # Add key: {provider, api_key, label?, tier?}
GET /api/keys # List all keys (masked)
DELETE /api/keys/:id # Remove key
PATCH /api/keys/:id # Enable/disable: {enabled: true/false}
GET /api/routing # Full routing matrix (default + overrides)
PUT /api/routing # Upsert rule: {intent, difficulty, provider, model_alias, priority?}
DELETE /api/routing/:id # Delete a rule
POST /api/routing/reset # Reset to defaults
GET /api/status # Health + key status
GET /api/providers # Registered providers
POST /api/seed # Re-seed default models
GET /health # Lightweight health check (no auth)
GET /api/analytics/summary # Overview: total requests, today's requests, avg latency, error rate
GET /api/analytics/timeline # Time series: requests/latency/errors by minute (?minutes=60)
GET /api/analytics/recent # Recent requests: last N requests (?limit=50)
GET /api/analytics/providers # Provider stats: requests, latency, error rate, tokens
GET /api/analytics/models # Model stats: same as providers, grouped by model
GET /api/analytics/intents # Intent Γ Difficulty matrix heatmap data
All
/api/*routes requireAuthorization: Bearer <PROXY_TOKEN>
| Variable | Required | Default | Description |
|---|---|---|---|
ENCRYPTION_KEY |
β | β | 64 hex chars (32 bytes). AES-256-GCM key for API key storage |
PROXY_TOKEN |
β | β | Bearer token for admin API auth |
PORT |
3131 |
Server port | |
DB_PATH |
./data/keypool.db |
SQLite database path | |
LOG_LEVEL |
info |
debug Β· info Β· warn Β· error |
ENCRYPTION_KEY=<output of: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))">
PROXY_TOKEN=your-secret-token-here
PORT=3131llm-keypool/
βββ src/ # Backend (Express + TypeScript)
β βββ index.ts # Entry: Express server + static serving + SPA fallback
β βββ config.ts # Environment config
β βββ aes.ts # AES-256-GCM encrypt/decrypt
β βββ rate-limiter.ts # Sliding window RPM tracker
β βββ router.ts # Core: route β pick key β forward β fallback
β βββ router/
β β βββ intent.ts # 8-intent classifier (explicit > implicit > default)
β β βββ difficulty.ts # 3-level difficulty assessor (multi-signal)
β β βββ model-map.ts # 8Γ3 routing matrix + DB overrides
β βββ providers/
β β βββ types.ts # ProviderAdapter interface + error classifier
β β βββ openai-compat.ts # Shared adapter for OpenAI-compatible providers (DeepSeek, GLM, Qwen, MiniMax, Moonshot, Doubao, OpenAI, OpenRouter, Agnes, Custom)
β β βββ doubao.ts # Doubao adapter (endpoint_id mapping)
β β βββ anthropic.ts # Anthropic adapter (Messages API protocol adaptation)
β β βββ gemini.ts # Google Gemini adapter (Generative AI API protocol adaptation)
β β βββ index.ts # Provider registry
β βββ db/
β β βββ index.ts # SQLite WAL init + schema
β β βββ keys.ts # Key CRUD + encrypted storage + cooldown/health
β β βββ models.ts # Model CRUD + 38-model seed data
β β βββ request-log.ts # Async batch request logger
β βββ routes/
β βββ chat.ts # /v1/* endpoints (OpenAI-compatible)
β βββ admin.ts # /api/* endpoints (key/routing/status mgmt)
β βββ analytics.ts # /api/analytics/* endpoints (dashboard data)
βββ client/ # Frontend (React + Vite admin dashboard)
β βββ src/
β β βββ main.tsx # React entry point
β β βββ App.tsx # Router + auth gate + layout
β β βββ pages/
β β β βββ DashboardPage.tsx # Real-time monitoring & analytics
β β β βββ KeysPage.tsx # API key management (CRUD + toggle)
β β β βββ ModelsPage.tsx # Model registry (38 models across 12 providers) & routing matrix
β β β βββ PlaygroundPage.tsx# Interactive chat playground
β β βββ components/
β β β βββ auth-gate.tsx # Bearer token authentication gate
β β β βββ page-header.tsx # Shared page header with nav
β β β βββ ui/ # shadcn/ui components (12 components)
β β βββ lib/
β β βββ api.ts # API client (fetch wrappers for all endpoints)
β β βββ utils.ts # Tailwind merge utility
β βββ index.html # Vite HTML entry
β βββ vite.config.ts # Vite config + dev proxy to :3131
β βββ package.json # React 18 + TanStack Query + Recharts + shadcn/ui
β βββ dist/ # Build output β served at /dashboard
βββ dashboard/
β βββ index.html # Legacy single-file dashboard (β /legacy-dashboard)
βββ docs/
β βββ index.html # Project landing page
β βββ assets/ # Logo SVGs
βββ tests/ # Test suite (110 tests)
β βββ setup.ts # Test environment configuration
β βββ unit/ # Unit tests (86 tests)
β β βββ crypto.test.ts # AES-256-GCM encryption/decryption
β β βββ difficulty.test.ts # Difficulty assessment logic
β β βββ intent.test.ts # Intent classification
β β βββ model-map.test.ts # Routing matrix
β β βββ providers.test.ts # Provider adapters
β β βββ rate-limiter.test.ts # Rate limiting logic
β βββ integration/ # Integration tests (24 tests)
β βββ api.test.ts # HTTP endpoints + auth + admin + analytics
βββ data/ # SQLite DB (gitignored)
βββ .env # Config (gitignored)
βββ package.json # Backend dependencies
βββ vitest.config.ts # Vitest test configuration
βββ tsconfig.json
- First principles over frameworks β No monorepo, no class inheritance, no Redis. Express + SQLite + config objects.
- Configuration objects over inheritance β 10 providers share one
OpenAICompatProviderinstance; onlybaseUrldiffers. Anthropic and Gemini have dedicated adapters for protocol adaptation. - Explicit > Implicit β Model prefix (
code:xxx) andX-Intentheader always win over keyword guessing. - Fail soft β Confidence < 0.6? Fall back to
chat. 429? Cooldown + switch provider. DB not ready? Use defaults. - Domestic-first, global-ready β Default routing matrix uses Chinese providers, but OpenAI, Google Gemini, Anthropic Claude, and OpenRouter are fully supported for overseas model access.
- Frontend-backend separation β React SPA builds to
client/dist/, served by Express at/dashboard. Legacy single-file dashboard preserved at/legacy-dashboard. Zero coupling between build chains.
| Concept | freellmapi | llm-keypool |
|---|---|---|
| Express proxy pattern | β | β |
| SQLite + WAL mode | β | β |
| AES-256-GCM key encryption | β | β |
| Key pool with free-tier-first | β | β (simplified: no tier accounting) |
| Provider adapter pattern | β | β (config objects, not class inheritance) |
| Streaming SSE passthrough | β | β (fixed: no content-type override) |
| Admin dashboard | β | β (React + Vite SPA with 4 pages) |
| Aspect | freellmapi | llm-keypool |
|---|---|---|
| Target providers | GPT-4, Claude, Gemini (overseas) | DeepSeek, GLM, Qwen, Doubao, MiniMax, Kimi, OpenAI, Gemini, Anthropic, OpenRouter, Agnes, Custom (domestic + global) |
| Routing | Round-robin / first-available | Intent Γ Difficulty matrix |
| Architecture | Monorepo (React + Express) | Separate packages (Express backend + React SPA client) |
| Provider adapters | Class inheritance | Config objects (10 providers share one OpenAICompat instance + 2 protocol adapters) |
| Doubao | Not supported | Special adapter for Volcengine endpoint_id |
| Anthropic | Not adapted | Full protocol adaptation (Messages API β OpenAI format) |
| Google Gemini | Not adapted | Full protocol adaptation (Generative AI API β OpenAI format) |
| Rate limiting | Token accounting | Sliding window RPM (simpler, practical) |
| Auth model | Hardcoded | PROXY_TOKEN env (simple Bearer) |
| Dashboard | React SPA (embedded) | React + Vite SPA (4 pages: Dashboard, Keys, Models, Playground) |
| Phase | Status | Scope |
|---|---|---|
| Phase 1 Core | β Done | Express + SQLite WAL + AES encryption + 6 providers + streaming + 429 fallback |
| Phase 2 Routing | β Done | Intent/difficulty routing + 8Γ3 matrix + rate limiter + routing CRUD |
| Phase 3 Claude + Analytics | β Done | Anthropic/Claude provider + analytics API + legacy single-file dashboard |
| Phase 4 Admin Dashboard | β Done | React + Vite SPA: 4 pages (Dashboard, Keys, Models, Playground) + shadcn/ui + TanStack Query + Recharts |
| Phase 5 Global Providers | β Done | OpenAI (4 models) + Google Gemini (3 models) + OpenRouter (4 models) + Agnes AI (1 model) + Custom endpoint support (38 models, 12 providers) |
| Phase 6 Testing | β Done | 110 tests: 86 unit tests (Vitest) + 24 integration tests (supertest) + coverage reporting |
| Phase 7 Advanced | π Planned | Sticky sessions + CLI tool + token accounting + cost tracking |
The project includes comprehensive test coverage with 110 tests (86 unit + 24 integration):
# Run all tests
npm test
# Run with coverage
npm test -- --coverage
# Run specific test suite
npx vitest tests/unit/crypto.test.ts
npx vitest tests/integration/api.test.tsTest Structure:
- Unit tests (
tests/unit/): Crypto, difficulty assessment, intent classification, model mapping, provider adapters, rate limiter - Integration tests (
tests/integration/): HTTP endpoints, auth middleware, admin API, analytics API, chat completions validation
Tests use Vitest with supertest for HTTP testing. Environment variables are configured in tests/setup.ts.
- Fork β Branch β PR
npm install && npm run buildβ backend must compile cleannpm testβ all tests must passcd client && npm install && npm run buildβ frontend must compile clean- New providers: implement
ProviderAdapterinterface, register inproviders/index.ts - New intents: add to
router/intent.ts+ updatemodel-map.tsDEFAULT_MATRIX - New dashboard pages: add to
client/src/pages/+ register inApp.tsxrouter
MIT β use it however you want.
π Homepage Β· π¦ GitHub Β· π Full Docs Β· π¨π³ δΈζζζ‘£
Inspired by freellmapi Β· Built with π§ by xiaopengs