OneAPI Pro · An enterprise-grade AI API Gateway, built with Go
Deep refactor and extension of one-api — many thanks to the original author.
👉 Try the Live Demo: http://demo.one-api.pro
·
👉 Demo Account: root / 123456
English · 简体中文 · 繁體中文 · 日本語 · Русский · 한국어 · العربية · Deutsch
- 🚀 Quick Start
- 🔧 Tech Stack
- ✨ Feature Highlights
- 🔥 One Api Pro vs. one-api
- 📸 Screenshots
- ⚙️ Configuration
- 📖 API Documentation
- 📦 Deployment
- 🗺️ Roadmap
- 📄 License
Either download the prebuilt binary from the GitHub Releases page, or build from source:
git clone https://github.com/modelbus/one-api-pro.git
cd one-api-procd web
sh build.sh # builds every theme listed in web/THEMES (default: default-pro)
cd ..The backend must be built after the frontend, so the latest UI assets are embedded.
go build -ldflags "-s -w" -o one-api-proUse the release.sh script in the repo root to download dependencies, build the frontend and cross-compile all platforms in one step:
./release.sh # version from the VERSION file
./release.sh v0.1.0 # specify a version
./release.sh v0.1.0 --skip-frontend # skip the frontend build (reuse web/build)Prerequisites:
go,node,npm. The version is read from the rootVERSIONfile (with or without thevprefix).
The outputs are statically linked bare executables (no extraction needed, run directly) written to dist/:
dist/one-api-pro-linux-amd64
dist/one-api-pro-linux-arm64
dist/one-api-pro-windows-amd64.exe
dist/one-api-pro-darwin-amd64
dist/one-api-pro-darwin-arm64
The
linux-*binaries are statically linked and work on both CentOS and Ubuntu. GitHub Releases are built and published automatically by.github/workflows/release.ymlwhen av*tag is pushed, mirroring the localrelease.shoutput.
./one-api-pro --port 3000 --log-dir ./logsVisit http://localhost:3000 and log in with the default account root / 123456.
For production deployments, see 📦 Deployment. For API references, see 📖 API Documentation.
One Api Pro is built with the help of these outstanding open-source projects — many thanks to their authors.
| Library | Purpose |
|---|---|
| Gin | HTTP web framework |
| GORM | ORM, supports SQLite / MySQL / PostgreSQL |
| go-redis/redis | Redis client |
| golang-jwt/jwt | JWT authentication |
| AWS SDK for Go v2 | AWS Bedrock integration |
| Google API Go Client | Google Gemini / PaLM2 integration |
| pkoukk/tiktoken-go | Token counting |
| gorilla/websocket | WebSocket support (e.g. iFlytek) |
| joho/godotenv | .env configuration file parsing |
| Library | Purpose |
|---|---|
| Vue 3 | Frontend framework (Composition API) |
| Vite | Build tool |
| Arco Design Vue | UI component library |
| Pinia | State management |
| Vue Router 4 | Routing |
| Axios | HTTP client |
| ECharts | Data visualization |
| vue-i18n | Internationalization |
One Api Pro is an enterprise-grade AI API Gateway written in Go + Vue 3, deep refactored on top of one-api with architecture-level changes and enterprise-grade enhancements, while keeping every feature of the original.
A brand-new Vue 3 + Arco Design admin console delivers a visual dashboard with core metrics, usage trends and per-model distribution at a glance.
| Key-metric cards | Usage trend chart |
|---|---|
![]() |
![]() |
Multi-dimensional token controls: per-model allowlists, sub-net IP restrictions, quota caps, expiration, unlimited quotas. Permissions can be scoped all the way down to a single model.
| Token management |
|---|
![]() |
A complete plan & subscription system: token- or request-based billing, period-based rate limits (hourly / weekly / monthly), per-model controls, plus recommended plans and pricing.
| Plan management | Subscription management |
|---|---|
![]() |
![]() |
Every subscription checkout leaves a full order audit trail (order number, user, plan snapshot JSON, amount, payment channel, status, paid-at timestamp, provider trade number) supporting two order types — subscription and top-up. Native integrations for WeChat Pay Native (PC QR) and Alipay Face-to-Face (TradePrecreate) ship out of the box, with bank / offline / free reserved for admin-side channels. Upgrade pricing is calculated automatically from remaining days, and the upgrade-mode toggle (stack vs price_diff) is hot-switchable from Settings → Plan Operations. The order center table now includes an Order type column that distinguishes a subscription vs top-up row; the async payment notification handler processNotify dispatches on order.Type to either activate the plan or credit the top-up quota.
The console's My balance card now supports online quota top-up: it reuses the same WeChat / Alipay payment channels and opens a TopupModal for the user to pick an amount (preset chips + custom amount), with quota credited asynchronously after the channel confirms. Configurable under Settings → Top-up:
- Master switch —
topup.enabled; when off, the dashboard top-up entry is disabled. - Preset amounts —
topup.presets; admins predefine{amount, bonus_quota}rows that render as chips. - Custom amount —
topup.allow_custom; when on, an extra Custom chip appears at the end of the list, and the input box is revealed only after the user clicks it. - Exchange rate —
topup.exchange_rate, default1(1 CNY = 1 quota); applies only to custom amounts; preset amounts use their ownbonus_quota.
Order type = 2 (OrderTypeTopup) is written to the same orders table with prefix TP. Activation is idempotent (already-paid orders return without re-crediting) and runs IncreaseUserQuota + RecordTopupLog on success.
Each node runs independent MySQL + Redis. Application-layer events keep data in sync across nodes — no shared database required, naturally supporting low-latency access from any region.
| Cluster node management |
|---|
![]() |
- 30+ model providers — OpenAI / Anthropic / Gemini / DeepSeek / Qwen / Wenxin / iFlytek / Zhipu, all unified behind an OpenAI-compatible interface.
- Precise cost accounting — token- or request-based billing, independent Prompt / Completion / Cached pricing, stacked group discounts, period usage tracking.
- Channel load balancing — weighted random distribution, automatic failover, cool-down / disable policies, channel concurrency and RPM rate limits.
- Multi-level permissions — Guest / User / Admin / Root, with upstream API authorization loophole fixed and granular admin action controls.
- Enterprise-grade security — full-link HTTPS, token authentication, sub-net IP restrictions, real-time auditing logs.
| Dimension | one-api | one-api-pro |
|---|---|---|
| Project name | one-api | one-api-pro |
| Adaptor architecture | Centralized constants: channeltype/define.go (56-line iota) + parallel arrays in url.go + 2-tier switch in helper.go; adding a provider requires edits across 4 framework files |
Self-registration (registry + register.go); adding a provider only needs a new package + one register call, zero framework changes |
| Permission granularity | Admin / user boundaries are blurry — anyone can change system settings through the API | Tiered permission system with the API authorization loophole fixed and granular admin controls |
| Subscription mode | No plan / subscription system | Full plan & subscription + period rate limits + per-model controls |
| Decentralized cluster | No independent cluster support; multi-host deployments share MySQL | Decentralized active-active cluster — each node runs its own MySQL + Redis, kept in sync through application-layer events with no shared database |
| Directory layout | relay/adaptor/ flattens 40 directories; base protocols and providers are mixed together; relay/model/ collides with root model/ |
adaptor/openai/, adaptor/anthropic/ as base protocols; adaptor/provider/ unifies 37 providers; relay/schema/ removes the naming collision |
| Admin console | 3 React themes (default / berry / air), basic management features |
Brand-new Vue 3 + Arco Design console with a visual dashboard |
| Maintenance status | Upstream project officially stopped in 2024 | Actively maintained, optimized for enterprise-grade scenarios |
One Api Pro is ready to use out of the box.
You can further customize it through environment variables or command-line arguments, then log in as root and continue configuration inside the admin console.
Tip: if you are unsure about the meaning of a setting, delete its value temporarily to see the in-app hint.
One Api Pro reads environment variables from a
.envfile. See.env.examplefor the full list — copy it to.envbefore use. You can also point to any path with--env(see below).
REDIS_CONN_STRING— enables Redis as a caching layer.- Example:
REDIS_CONN_STRING=redis://default:redispw@localhost:49153 - If your database latency is already low, Redis is not strictly required — enabling it may even introduce short-term staleness.
- For Redis Sentinel / cluster mode, set this to a comma-separated node list, e.g.
localhost:49153,localhost:49154,localhost:49155, and additionally set:REDIS_PASSWORD— password for Sentinel / cluster mode.REDIS_MASTER_NAME— master name for Sentinel mode.
- Example:
SESSION_SECRET— pins the session key, keeping existing cookies valid across restarts.- Example:
SESSION_SECRET=random_string
- Example:
SQL_DSN— uses an external database instead of SQLite. MySQL or PostgreSQL.- Examples:
- MySQL:
SQL_DSN=root:123456@tcp(localhost:3306)/oneapi - PostgreSQL:
SQL_DSN=postgres://postgres:123456@localhost:5432/oneapi(in-progress, feedback welcome)
- MySQL:
- The
oneapidatabase must exist beforehand; tables are auto-created on first run. - Cloud databases that require identity verification may need
?tls=skip-verifyin the DSN. - Tunable pool parameters (defaults shown):
SQL_MAX_IDLE_CONNS— max idle connections, default100.SQL_MAX_OPEN_CONNS— max open connections, default1000. Reduce if you seeError 1040: Too many connections.SQL_CONN_MAX_LIFETIME— connection lifetime in minutes, default60.
- Examples:
LOG_SQL_DSN— uses a dedicated database for thelogstable.FRONTEND_BASE_URL— when set on a slave node, redirects page requests to this address.- Example:
FRONTEND_BASE_URL=https://openai.justsong.cn
- Example:
MEMORY_CACHE_ENABLED— enables in-memory caching. Causes minor staleness on quota updates. Defaultfalse.- Example:
MEMORY_CACHE_ENABLED=true
- Example:
SYNC_FREQUENCY— when caching is enabled, how often (in seconds) configurations are synced from the database. Default600.- Example:
SYNC_FREQUENCY=60
- Example:
NODE_TYPE—masterorslave. Defaultmaster.- Example:
NODE_TYPE=slave
- Example:
CHANNEL_UPDATE_FREQUENCY— interval in minutes for periodic channel-balance refresh. No refresh if unset.- Example:
CHANNEL_UPDATE_FREQUENCY=1440
- Example:
CHANNEL_TEST_FREQUENCY— interval in minutes for periodic channel health checks. No check if unset.- Example:
CHANNEL_TEST_FREQUENCY=1440
- Example:
POLLING_INTERVAL— interval (seconds) between batch channel-balance / health requests. Default0(no spacing).- Example:
POLLING_INTERVAL=5
- Example:
BATCH_UPDATE_ENABLED— enables batched DB updates for user-quota changes. Causes minor staleness. Defaultfalse.- Example:
BATCH_UPDATE_ENABLED=true - Helpful when you hit too many DB connections.
- Example:
BATCH_UPDATE_INTERVAL— batch update aggregation window in seconds, default5.- Example:
BATCH_UPDATE_INTERVAL=5
- Example:
- Rate limits:
GLOBAL_API_RATE_LIMIT— global API rate limit (excluding relay requests), per IP, per 3 minutes. Default180.GLOBAL_WEB_RATE_LIMIT— global Web rate limit, per IP, per 3 minutes. Default60.
- Tokenizer caches:
TIKTOKEN_CACHE_DIR— caches tiktoken encodings (e.g.gpt-3.5-turbo,gpt-4,gpt-4o) locally. On startup the encodings are downloaded on demand; if the network is restricted or offline, the download times out (about 30 seconds) and the system automatically falls back to approximate token counting (about0.38 × character count), so the service still starts normally. For precise billing, pre-download the encodings into this directory on a networked machine, then move them into the offline environment.DATA_GYM_CACHE_DIR— same purpose, lower priority thanTIKTOKEN_CACHE_DIR.
RELAY_TIMEOUT— relay request timeout in seconds. No timeout by default.RELAY_PROXY— proxy URL for upstream API requests.USER_CONTENT_REQUEST_TIMEOUT— user content download timeout in seconds.USER_CONTENT_REQUEST_PROXY— proxy for user-uploaded content (e.g. images).SQLITE_BUSY_TIMEOUT— SQLite lock-wait timeout in milliseconds. Default3000.GEMINI_SAFETY_SETTING— Gemini safety setting. DefaultBLOCK_NONE.GEMINI_VERSION— Gemini API version used by One Api Pro. Defaultv1.THEME— admin theme. Defaultdefault-pro(Vue 3). Other options:default/berry/air(legacy React themes). See web/README.md.ENABLE_METRIC— auto-disable channels with low success rate. Defaultfalse.METRIC_QUEUE_SIZE— sliding window size for success-rate statistics. Default10.METRIC_SUCCESS_RATE_THRESHOLD— success-rate threshold. Default0.8.INITIAL_ROOT_TOKEN— if set, a root token with this value is auto-created on first startup.INITIAL_ROOT_ACCESS_TOKEN— if set, a root access token with this value is auto-created on first startup.ENFORCE_INCLUDE_USAGE— forceusagein stream responses. Defaultfalse.TEST_PROMPT— prompt used by channel/model self-tests. DefaultPrint your model name exactly and do not output without any other text..
When none of the following are set, One Api Pro runs in single-node mode with no side effects.
CLUSTER_ENABLED— enable cluster mode. Default off.- Example:
CLUSTER_ENABLED=true
- Example:
CLUSTER_NODE_ID— node ID (1–49). Must match MySQL'sauto_increment_offset, unique across nodes.- Example:
CLUSTER_NODE_ID=1
- Example:
CLUSTER_NODE_NAME— display name. Defaultnode-{NODE_ID}.- Example:
CLUSTER_NODE_NAME=node-cn
- Example:
CLUSTER_NODE_ADDRESS— public address other nodes use to reach this one (must include protocol).- Example:
CLUSTER_NODE_ADDRESS=https://cn.example.com
- Example:
CLUSTER_SECRET— initial per-node secret. Used as seed on first start, then stored in the DB; admins can rotate it later.- Example:
CLUSTER_SECRET=MyClusterSecret123abc
- Example:
CLUSTER_SEEDS— comma-separated seed nodes used for first-time discovery; one reachable node is enough. The very first node can leave this empty (or use its own address).- Example:
CLUSTER_SEEDS=https://cn.example.com - Multiple:
CLUSTER_SEEDS=https://cn.example.com,https://us.example.com
- Example:
CLUSTER_PUSH_INTERVAL— sync event push interval in seconds. Default3.CLUSTER_DISCOVERY_INTERVAL— node discovery interval in seconds. Default30.CLUSTER_DEAD_PING_INTERVAL— ping interval for unreachable nodes in seconds. Default120.CLUSTER_MAX_PING_FAILURES— consecutive failures before a node is marked failed. Default3.CLUSTER_SYNC_LOGS— sync thelogstable across nodes. Defaulttrue.- Example:
CLUSTER_SYNC_LOGS=false
- Example:
CLUSTER_BATCH_SIZE— max events per push. Default50.
--port <port_number>— listen port. Default3000.- Example:
--port 3000
- Example:
--log-dir <log_dir>— log directory. Default./logsin the working directory.- Example:
--log-dir ./logs
- Example:
--env <env_file_path>— configuration file path; supports relative and absolute paths. Auto-loads./.envwhen unset.- Example:
--env ./config.env - Example:
--env /etc/one-api-pro/production.env - Multiple-instance example:
./one-api-pro --env ./instances/instance1.env --port 3001 & ./one-api-pro --env ./instances/instance2.env --port 3002 &
- Precedence: CLI args > system env vars >
--envfile > defaults
- Example:
--version— print version and exit.- Example:
./one-api-pro --version - Version source (highest priority first):
- The
VERSIONfile in the current working directory or next to the executable (auto-detects thevprefix, e.g.0.0.2orv0.0.2); - The version injected at build time via
-ldflags "-X .../common.Version=..."(bothrelease.shand CI do this automatically); - The hard-coded default in
common/constants.go.
- The
- Therefore you only need to maintain the root
VERSIONfile to keep--version, the startup log, the/api/statusendpoint and the dashboard badge consistent.
- Example:
--help— print command-line help.- Example:
./one-api-pro --help
- Example:
The full API reference lives at docs/API.md, covering:
- Authentication — Cookie Session / Access Token / API Key (Bearer Token)
- Admin endpoints — full CRUD for model pricing, group discounts, channels, tokens, users, logs, redemption codes, plans, subscriptions, etc.
- OpenAI-compatible endpoints —
/v1/models,/v1/chat/completions,/v1/embeddings, image, audio, moderation, etc. - Cluster management API — node discovery, heartbeats, data sync endpoints for decentralized clusters
👉 View the full API documentation →
Choose one of the following:
Option 1: Download a prebuilt binary (recommended)
Download the bare executable for your platform (Linux / macOS / Windows) from GitHub Releases — no extraction needed, run it directly.
Option 2: One-click packaging with release.sh
git clone https://github.com/modelbus/one-api-pro.git
cd one-api-pro
./release.sh # multi-platform packaging, outputs to dist/Option 3: Build from source
git clone https://github.com/modelbus/one-api-pro.git
cd one-api-pro
# Build the Vue 3 admin console (per web/THEMES)
cd web
sh build.sh
# Build the backend (must run AFTER the frontend build, so the latest assets are embedded)
cd ..
go build -ldflags "-s -w" -o one-api-prochmod u+x one-api-pro
./one-api-pro --port 3000 --log-dir ./logsOpen http://localhost:3000/ and log in. The default account is root / 123456.
- Set the same
SESSION_SECRETon every host. - Set
SQL_DSNso all hosts share the same MySQL database (SQLite won't work). - Set
NODE_TYPE=slaveon every secondary host (default ismaster). - Set
SYNC_FREQUENCYso every host periodically syncs configuration from the database. With a remote database this is recommended regardless of master/slave, and Redis should be enabled. - Secondary hosts may set
FRONTEND_BASE_URLto redirect page requests to the master. - Install separate Redis instances on every secondary host and set
REDIS_CONN_STRINGaccordingly. This can hit the DB zero times while the cache is warm, reducing latency (see env vars for Sentinel / cluster support). - If the master also has high DB latency, enable Redis +
SYNC_FREQUENCYon the master as well.
See Environment Variables for the full set of options.
The cluster mode lets multiple nodes each run their own One Api Pro + MySQL, kept in sync through application-layer events. No shared database is required.
Use cases: global multi-region deployment, low-latency local access, HA / DR, multi-node load balancing.
┌─────────────┐
│ Nginx/LB │ (single entry, ip_hash load balancing)
└──────┬──────┘
│
┌──────────────┼──────────────┐
│ │ │
┌──────┴──────┐ ┌────┴───────┐ ┌───┴────────┐
│ Node A │ │ Node B │ │ Node C │
│ (one-api-pro) │ │ (one-api-pro) │ │ (one-api-pro) │
│ + MySQL │ │ + MySQL │ │ + MySQL │
│ + Redis │ │ + Redis │ │ + Redis │
└──────┬──────┘ └─────┬──────┘ └────┬────────┘
│ │ │
└────── HTTP push of sync events ──────┘
- Decentralized — every node is equal; any data change is actively pushed to all alive nodes.
- Zero-invasion — GORM callbacks capture data changes; no business-code modifications.
- Async push — sync runs in a background goroutine, never blocking the main flow.
- Conflict resolution — comparison is based on
updated_at; only newer data is written. - Rate-limit sync — channel concurrency & RPM counters sync across nodes via DB tables.
- Single-node compatible — without the cluster env vars, the system runs in plain single-node mode.
| Table | Synced? | Notes |
|---|---|---|
| users | ✅ | user accounts |
| tokens | ✅ | API tokens |
| channels | ✅ | provider channels |
| abilities | ✅ | channel abilities |
| options | ✅ | system settings |
| redemptions | ✅ | redemption codes |
| plans | ✅ | subscription plans |
| user_plans | ✅ | user subscriptions |
| plan_usages | ✅ | plan usage |
| channel_counters | ✅ | channel rate-limit counters |
| cluster_nodes | 🔄 Discovery | maintained by the discovery mechanism, not data sync |
| logs | controlled by CLUSTER_SYNC_LOGS |
1. MySQL — every node must use an independent MySQL instance.
Each node needs its own MySQL instance — you cannot deploy multiple nodes by simply creating multiple databases in a single MySQL instance, because auto_increment_offset is an instance-level variable.
# Node 1 my.cnf
[mysqld]
server-id = 1
auto_increment_increment = 50
auto_increment_offset = 1
log_bin = mysql-bin
binlog_format = ROW
# Node 2 my.cnf
[mysqld]
server-id = 2
auto_increment_increment = 50
auto_increment_offset = 2
log_bin = mysql-bin
binlog_format = ROW
# Node 3 my.cnf
[mysqld]
server-id = 3
auto_increment_increment = 50
auto_increment_offset = 3
log_bin = mysql-bin
binlog_format = ROW
auto_increment_incrementis set to 50 — supports up to 50 nodes. Each node'soffsetmust match itsCLUSTER_NODE_IDand be unique.
Important:
auto_increment_incrementandauto_increment_offsetare MySQL instance-level variables. They apply to every database inside an instance; they cannot be set per database or per table (MySQL's table option only supports anAUTO_INCREMENTstarting value, not step size). So every node must run its own MySQL instance — you cannot deploy multiple nodes by creating separate databases in the same MySQL. To run multiple instances on one machine, start multiplemysqldprocesses on different ports or use multiple Docker containers.
About
server-idand binlog:server-idmust be unique across all MySQL instances in the cluster.log_binandbinlog_format=ROWare strongly recommended — they enable future master-slave replication and point-in-time recovery. The cluster data sync itself doesn't depend on binlog (it goes through GORM callbacks), but binlog adds an extra layer of reliability.
2. Redis — every node must use an independent Redis instance.
Each node needs its own Redis instance (different port or different machine). Redis is used only for local cache and rate limiting here; cluster traffic does not flow through Redis.
3. Bootstrap a new node
A newly added node must first obtain a data snapshot from an existing node:
# Option 1: dump + import
mysqldump -h existing-node -u root -p oneapi > backup.sql
mysql -u root -p oneapi < backup.sql
# Option 2: snapshot API (after the existing node is up)
curl -H "X-Cluster-Secret: your-secret" \
"https://existing-node/api/cluster/snapshot?tables=users,tokens,channels,abilities,options,redemptions,plans,user_plans,plan_usages" \
-o snapshot.json4. Environment configuration (full example)
Below is a complete .env for a 3-node cluster. Every node uses its own MySQL & Redis instances, with distinct ports and paths.
Node 1 — China (/opt/one-api-pro/node1/.env):
# ========================
# Basics
# ========================
PORT=3000
SYSTEM_NAME=One Api Pro Cluster
# ========================
# Database (independent MySQL)
# ========================
SQL_DSN=root:password@tcp(127.0.0.1:3306)/oneapi_node1?charset=utf8mb4&parseTime=True&loc=Local
# ========================
# Redis (independent)
# ========================
REDIS_CONN_STRING=redis://127.0.0.1:6379/0
# ========================
# Cluster
# ========================
CLUSTER_ENABLED=true
CLUSTER_NODE_ID=1
CLUSTER_NODE_NAME=node-cn
CLUSTER_NODE_ADDRESS=https://cn.example.com
CLUSTER_SECRET=your-strong-shared-secret-key-change-me
# Seed nodes (only needed for first-time discovery)
# First node: leave empty or use its own address
# Later nodes: any alive node is enough
CLUSTER_SEEDS=https://cn.example.com,https://us.example.com,https://eu.example.com
# ========================
# Cluster tuning (optional)
# ========================
CLUSTER_DISCOVERY_INTERVAL=30
CLUSTER_DEAD_PING_INTERVAL=120
CLUSTER_MAX_PING_FAILURES=3
CLUSTER_PUSH_INTERVAL=3
CLUSTER_SYNC_LOGS=true
CLUSTER_BATCH_SIZE=50Node 2 — US (/opt/one-api-pro/node2/.env):
PORT=3001
SYSTEM_NAME=One Api Pro Cluster
SQL_DSN=root:password@tcp(127.0.0.1:3306)/oneapi_node2?charset=utf8mb4&parseTime=True&loc=Local
REDIS_CONN_STRING=redis://127.0.0.1:6380/0
CLUSTER_ENABLED=true
CLUSTER_NODE_ID=2
CLUSTER_NODE_NAME=node-us
CLUSTER_NODE_ADDRESS=https://us.example.com
CLUSTER_SECRET=your-strong-shared-secret-key-change-me # must match node 1
# One reachable node is enough
CLUSTER_SEEDS=https://cn.example.comNode 3 — Europe (/opt/one-api-pro/node3/.env):
PORT=3002
SYSTEM_NAME=One Api Pro Cluster
SQL_DSN=root:password@tcp(127.0.0.1:3306)/oneapi_node3?charset=utf8mb4&parseTime=True&loc=Local
REDIS_CONN_STRING=redis://127.0.0.1:6381/0
CLUSTER_ENABLED=true
CLUSTER_NODE_ID=3
CLUSTER_NODE_NAME=node-eu
CLUSTER_NODE_ADDRESS=https://eu.example.com
CLUSTER_SECRET=your-strong-shared-secret-key-change-me # must match all nodes
CLUSTER_SEEDS=https://cn.example.comConfiguration cheat-sheet:
| Variable | Node 1 | Node 2 | Node 3 | Notes |
|---|---|---|---|---|
PORT |
3000 | 3001 | 3002 | distinct on same host |
SQL_DSN |
...oneapi_node1 | ...oneapi_node2 | ...oneapi_node3 | independent MySQL |
REDIS_CONN_STRING |
:6379/0 | :6380/0 | :6381/0 | independent Redis |
CLUSTER_NODE_ID |
1 | 2 | 3 | matches auto_increment_offset |
CLUSTER_NODE_NAME |
node-cn | node-us | node-eu | display name |
CLUSTER_NODE_ADDRESS |
https://cn.example.com | https://us.example.com | https://eu.example.com | public URL |
CLUSTER_SECRET |
same | same | same | identical across all nodes |
CLUSTER_SEEDS |
own address or empty | any alive node | any alive node | bootstraps discovery |
5. Start commands
Each node loads its own config via --env:
# Node 1
./one-api-pro --env /opt/one-api-pro/node1/.env --port 3000
# Node 2
./one-api-pro --env /opt/one-api-pro/node2/.env --port 3001
# Node 3
./one-api-pro --env /opt/one-api-pro/node3/.env --port 30026. Startup order
- Start the first node (Node A); leave
CLUSTER_SEEDSempty or use its own address. - Wait for Node A to fully start (~5–10 s; look for
cluster module initializedin the logs). - Start the remaining nodes, pointing
CLUSTER_SEEDSat any alive node. - New nodes ping the seed, then transitively learn all the rest.
- Once every node is up, view the cluster under Settings → Node Management in the admin console.
7. Optional Nginx load-balancing example
upstream one_api_cluster {
ip_hash; # same client always hits the same node — preserves session & cache
server cn.example.com:3000;
server us.example.com:3000;
server eu.example.com:3000;
}
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://one_api_cluster;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 300s;
}
}
ip_hashis critical — it pins a client to a single node so plan rate-limits and Redis cache stay consistent.
8. Verify cluster status
# List nodes (call any node)
curl -H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
https://cn.example.com/api/cluster_node/
# Returns every node with status, last_heartbeat, ping_failures, …Or open the admin console → Settings → Node Management to inspect nodes, status and last heartbeats.
💡 Cluster API details: docs/API.md Appendix E — Cluster Management API
- Each node must have its own MySQL & Redis instances.
CLUSTER_SECRETmust be identical across all nodes. Use a strong value and keep it safe.CLUSTER_NODE_IDmust be unique across nodes and match MySQL'sauto_increment_offset.CLUSTER_NODE_ADDRESSmust be reachable by other nodes (include protocol prefix, e.g.https://).- New nodes must be bootstrapped manually (pull a snapshot from a live node).
- Logs are large by nature — disable with
CLUSTER_SYNC_LOGS=falseif needed. - MySQL's
auto_increment_increment&auto_increment_offsetmust matchCLUSTER_NODE_ID. - Discovery uses a bi-directional ping; failed nodes are not deleted, only marked
status=2. They auto-resurrect when the network is back. CLUSTER_SEEDSis only the bootstrap. Once peers are discovered, SEEDS is no longer consulted.- Changes that happen while a node is offline are not back-filled automatically. The node must pull a snapshot after coming back.
Each node writes a self-record (node_id == CLUSTER_NODE_ID) into its own cluster_nodes table on startup. This is intentional:
- Admin visibility — Settings → Node Management should display local info (address, status, heartbeat) for troubleshooting.
- Transitive discovery — when Node B receives a ping from Node A, A returns its full node list (including itself). B merges it. So C can learn about A through B.
- Liveness signal — local
last_heartbeatis refreshed every 30 s bydiscoverOnce, reflecting this node's own health.
The design prevents loops with five layers of guards:
| Guard | Effect |
|---|---|
① GetAllRemoteNodes SQL filter |
WHERE node_id != ? excludes self during discovery |
② GetAliveNodesForSync SQL filter |
WHERE node_id != ? excludes self during push |
③ handlePing rejects self-ping |
req.NodeId == NodeID is refused explicitly |
④ mergeDiscoveredNodes skips self |
local merge skips self |
⑤ ApplyEvents skips self events |
receiver ignores events produced by itself |
Data flow is one-way: local → remote, remote → local — no loops.
The admin console displays a blue "self" badge next to the local node and disables Delete / Manual Ping on it (they have no meaning for the local node).
Each node carries its own secret, replacing the previous global-shared-secret design. Reasons:
- Security — one node's secret leaking does not affect others.
- Flexibility — every node can rotate its own secret independently.
- Auto-discovery — peers share their secret on every ping.
Secret lifecycle:
- First start: uses
CLUSTER_SECRETenv var as the initial value, written tocluster_nodes.secret_key. - Subsequent starts: read from
cluster_nodes.secret_key. - Admins can update any other node's secret from the Node Management page.
X-Cluster-Secretheader on ping = target node's secret (looked up locally).
Adding a new node:
- On Node A, add Node B's record using B's
CLUSTER_SECRET. - On Node B, add Node A's record using A's
CLUSTER_SECRET. - A → B ping uses B's secret; B verifies with its own secret ✓.
- B's response carries both A's and B's secrets — A updates its local copy.
Removing a node sets disabled = true instead of physically deleting the record:
- Prevents the deleted node from "regrowing" through ping-driven re-registration.
- Disabled nodes still respond to pings (so peers know they're online) but won't fetch this node's info.
- Hard delete requires manual SQL:
DELETE FROM cluster_nodes WHERE node_id = ?.
Cluster data sync relies entirely on GORM callbacks + HTTP active push:
- Every INSERT/UPDATE/DELETE on a business table → captured by the GORM callback → written to
sync_events→ Pusher goroutine pushes to every alive node. - Receivers write through
WithSkipHook(no loop-back). - Receivers also skip events whose
event.NodeId == local NodeID(belt-and-braces).
Design trade-off: this design does not implement cross-node active pull, for these reasons:
- Business-intrusive — pull would require knowing each table's business-unique field, polluting business code.
- Primary-key conflicts — auto-increment IDs differ across nodes (different
auto_increment_offset); using the source's ID would break the offset design. - Complexity — maintenance cost is high, reliability gain is limited.
- Push is enough — push covers ~95% of normal scenarios (alive nodes, normal traffic).
Known limits & operational needs:
- Changes made while a node is offline → lost permanently (push is realtime only).
- After a node comes back, missed data is not auto-recovered.
- New nodes only see changes from the moment they join — no history.
- Operator's recourse:
mysqldumpan existing node and restore.
Scenario cheat-sheet:
| Scenario | Pull needed? | How to handle |
|---|---|---|
| Node permanently online | ❌ | push is enough |
| Node reboots occasionally (minutes) | minor loss during downtime, usually acceptable | |
| Node under frequent maintenance | ❌ | push keeps going; resume is instant |
| New node joins cluster | ❌ | DBA runs mysqldump to seed |
| Long offline node recovers | ❌ | DBA runs mysqldump to repair |
If a freshly deployed cluster shows a blank page, see #97.
- Architectural refactor — adaptor self-registration; adding a provider needs zero framework changes.
- Vue 3 admin console — Arco Design + visual dashboard + 30+ provider icons.
- Subscription & plan system — token- or request-based billing, period rate limits, per-model controls.
- Decentralized active-active cluster — GORM-event-driven + HTTP active push; no shared DB.
- Precise cost accounting — independent Prompt / Completion / Cached pricing, stacked group discounts.
- Multi-level permissions — Guest / User / Admin / Root, upstream API authorization loophole fixed.
- OpenAI-compatible API —
models/chat/completions/embeddings/images/audio/moderations. - Subscription checkout & upgrade flow — native
POST /api/order/plancreates the subscription order, supportsstack(additive) andprice_diff(upgrade) modes, auto-computes the upgrade price from remaining days, and rejects same-tier / downgrade attempts. - Order audit & order center — new
orderstable (type/source/order_no/plan_info/amount/status/pay_status/pay_method/pay_time/pay_trade_no) persists every checkout / admin grant. Frontend/plansand/orderspages render the full lifecycle; the order center table includes an Order type column that distinguishes a subscription vs top-up row. - Real payment integration (gopay) — native WeChat Pay Native (PC QR) and Alipay Face-to-Face (TradePrecreate); async callbacks at
/api/payment/{wechat,alipay}/notifycomplete the verify → mark paid → activate loop. The notification handlerprocessNotifydispatches onorder.Typeto either activate the plan or credit the top-up quota. - Payment / plan-operations settings — two new sub-tabs under Operations Settings:
Plan(price_diff vs stack upgrade toggle) andPayment(independent enable/disable + cert upload + notify URL per channel: WeChat / Alipay / Bank). - Online quota top-up — users top up their account balance from the console's My balance card; reuses the same WeChat / Alipay payment channels. New
OrderTypeTopup=2shares theorderstable, order-number prefixTP, activation is idempotent;ActivateTopupByOrdercallsIncreaseUserQuotaand writesRecordTopupLog. - Top-up settings center — new Settings → Top-up tab replaces the previous
plan.allow_topupplaceholder; configures the master switch, preset amounts, allow-custom flag, and the exchange rate (default 1:1); reuses existing payment settings and thesystem_settingstable. - Unified order management (admin view) — the admin order center supports list / search / detail / update / delete, with multi-dimensional filters by status, type, source, user and plan; admins can manually mark an order as paid (offline / bank activates immediately) or refunded (status flag only).
- Channel diagnostics & intelligent routing (baseline) — automatic cooldown (
CooldownFilter), fallback degradation (FallbackFilter) and low-success-rate auto-disable (monitor) are live. - Internationalization (i18n) — full zh/en coverage for the admin console, landing page and legal pages; menus, page copy and Arco Design components switch in real time; the language choice is persisted and synced to
<html lang>. Message packs are split per page module and auto deep-merged.
- Channel diagnostics & intelligent routing (enhancement) — on top of the live cooldown / fallback / low-success-rate auto-disable, complete the standalone diagnostics dashboard, per-node ping and manual-review workflow.
- Richer usage analytics reports and exports.
- Refund loop — orders with
status=3currently only flip the status without reversing quota; to be completed once the refund feature ships.
- More languages — extend beyond Chinese / English to Traditional Chinese, Japanese, Korean, Russian, German, Arabic, etc. (aligned with the README translations)
- More payment channels — Apple Pay, UnionPay, Stripe, etc.
- Order refund capability — async refund API + automated refund ledger; roll back subscription / quota on plan or top-up refunds, with a visual refund ledger.
- Finance-system integration — sync top-ups, consumption and refunds with mainstream finance / reconciliation platforms.
- Token-low alerting — multi-channel notifications when account / token Token balance drops below threshold.
- Audit logs & reports — full operation audit trail and visual reports for compliance.
- AI-powered analytics — LLM-driven analysis of usage, costs and channel health, with recommendations.
- Plugin extension mechanism.
- Enterprise SSO / LDAP integration.
- Alerting & notification channels (DingTalk / Feishu / WeCom).
- More model providers.
💡 PRs and issues are welcome — see issues.
This project is released under the MIT License.





