This guide explains how to deploy CommandCode Bridge as a durable OpenAI-compatible API service for CommandCode-backed DeepSeek models in a CommandCode CLI environment.
CommandCode CLI environment required: download/install the official CLI from commandcode.ai/install (official site: commandcode.ai), then authenticate the CLI or provide equivalent
COMMANDCODE_*credentials. This bridge uses the same CommandCode account/upstream API and is not a public standalone DeepSeek proxy.
Korean version: DEPLOYMENT.ko.md
For a personal workstation, homelab, or tailnet host, the recommended shape is:
OpenAI-compatible client
-> http://127.0.0.1:9992 or http://<tailscale-ip>:9992
-> commandcode-bridge systemd service
-> CommandCode /alpha/generate upstream
Security baseline:
- Keep
BRIDGE_API_KEYenabled for anything beyond local ad-hoc testing. - Bind to
127.0.0.1for local-only use, or0.0.0.0only behind a private network such as Tailscale/WireGuard/VPN or an authenticated reverse proxy. - Never commit real environment files or credential JSON files.
- Treat CommandCode CLI auth files and API keys as personal upstream credentials.
- Use the admin endpoint only with bridge authentication.
On this workstation, the public/tailnet endpoint is now fronted by a user-systemd router. The local bridge remains a separate user service and listens on an internal port used by the router.
Paths:
- Bridge user unit:
~/.config/systemd/user/commandcode-bridge.service - Router user unit:
~/.config/systemd/user/commandcode-router.service - Bridge runtime env file:
~/.config/commandcode-bridge/env - Router runtime env file:
~/.config/commandcode-bridge/router.env - Bridge executable:
~/.local/bin/commandcode-bridge - Router executable:
~/.local/bin/commandcode-router - External/Tailscale endpoint: router on
0.0.0.0:9992 - Local backend endpoint: bridge on
127.0.0.1:19992via router config - Tailscale URL base for clients:
http://100.122.162.75:9992
Check status:
systemctl --user status commandcode-bridge --no-pager
systemctl --user status commandcode-router --no-pager
systemctl --user is-enabled commandcode-bridge
systemctl --user is-enabled commandcode-router
loginctl show-user "$USER" -p LingerExpected:
- both services are
active (running) - both services are
enabled - linger is
Linger=yes
Operate:
systemctl --user restart commandcode-bridge
systemctl --user restart commandcode-router
journalctl --user -u commandcode-bridge -f
journalctl --user -u commandcode-router -fRouter health:
curl -sS http://127.0.0.1:9992/health | jqDirect local backend health, bypassing the router:
curl -sS http://127.0.0.1:19992/health | jqAuthenticated models check through the router:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
curl -sS http://127.0.0.1:9992/v1/models \
-H "Authorization: Bearer $BRIDGE_API_KEY" | jqRouter backend status:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
curl -sS http://127.0.0.1:9992/admin/router/backends \
-H "Authorization: Bearer $BRIDGE_API_KEY" | jqSmoke test through the externally preserved endpoint:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
BRIDGE_BASE_URL=http://127.0.0.1:9992 npm run smokeIf the upstream account is reachable but blocked by balance/credits, use routing-only smoke mode:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
BRIDGE_BASE_URL=http://127.0.0.1:9992 SMOKE_ACCEPT_UPSTREAM_ERRORS=1 npm run smokeSMOKE_ACCEPT_UPSTREAM_ERRORS=1 is not a generation canary. It only accepts explicit upstream/fail-closed errors such as commandcode_event_error, commandcode_empty_response, or commandcode_empty_visible_response.
Admin credential metrics through the router:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
curl -sS 'http://127.0.0.1:9992/admin/commandcode/credentials?refresh=true' -H "Authorization: Bearer $BRIDGE_API_KEY" | jqThe admin endpoint returns credential IDs, routing state, billing-derived metrics, and alert configuration. It must not return raw CommandCode API keys or the bridge key.
Run one commandcode-bridge per machine, then put one commandcode-router in front of them. The router preserves a single OpenAI-compatible endpoint for agents and chooses an eligible backend by least in-flight request count.
Minimal router env:
HOST=0.0.0.0
PORT=9992
COMMANDCODE_ROUTER_BACKENDS=local=http://127.0.0.1:19992,pc2=http://100.x.y.z:9992
COMMANDCODE_ROUTER_BACKEND_MAX_INFLIGHT=1
COMMANDCODE_ROUTER_BACKEND_TIMEOUT_MS=300000
COMMANDCODE_ROUTER_HEALTH_TIMEOUT_MS=3000
COMMANDCODE_ROUTER_COOLDOWN_MS=60000If all bridge backends use the same BRIDGE_API_KEY, the router can reuse it as both client auth and backend auth. If a backend uses a different key, provide JSON backend entries with per-backend apiKey values in a private env file; never commit those values.
A single streaming request cannot migrate between PCs after output starts. Failover and load distribution happen at independent request boundaries.
For Raspberry Pi or Linux hosts that should run the bridge as the current user, run this from the repository root:
./install.shThe installer asks whether to bind to 127.0.0.1 or 0.0.0.0 and which port to use. The safe default is 127.0.0.1:9992.
Prerequisites: Linux with user systemd, Node.js >= 22 for current CommandCode CLI installation, npm, and either CommandCode CLI auth at ~/.commandcode/auth.json or a COMMANDCODE_API_KEY. If this is a headless host that must start before login, enable linger once with sudo loginctl enable-linger "$USER". Use 0.0.0.0 only behind LAN/Tailscale/VPN/firewall controls and keep a strong BRIDGE_API_KEY.
Non-interactive install:
./install.sh --yes --host 127.0.0.1 --port 9992Remove the service and installed files while preserving credentials/env:
./uninstall.shRemove credentials/env too:
./uninstall.sh --purge-configUse this when you do not have root access, or when the bridge should run as the current user and read that user's ~/.commandcode/auth.json.
Prerequisites:
command -v node
command -v npm
command -v commandcode-bridge
systemctl --user is-system-runningEnable linger so the user service starts at boot even before login:
sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" -p LingerCreate the env directory and file:
mkdir -p ~/.config/commandcode-bridge
chmod 700 ~/.config/commandcode-bridge
nano ~/.config/commandcode-bridge/env
chmod 600 ~/.config/commandcode-bridge/envMinimal env file:
HOST=0.0.0.0
PORT=9992
NODE_ENV=production
BRIDGE_API_KEY=replace-with-long-random-client-key
COMMANDCODE_ROUTING_POLICY=depletion_aware
COMMANDCODE_EMPTY_VISIBLE_RESPONSE_POLICY=error_on_length
COMMANDCODE_BALANCE_ALERT_ENABLED=falseIf you rely on the normal CommandCode CLI auth file, keep it at:
~/.commandcode/auth.json
If you prefer explicit upstream credentials, add one of these to the env file:
# Single-key mode
COMMANDCODE_API_KEY=cmd_key_here
# Multi-key mode
COMMANDCODE_API_KEYS=primary=cmd_key_one,secondary=cmd_key_twoCreate the user unit manually, or copy the release template from release/systemd/commandcode-bridge.user.service.
Manual unit creation:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/commandcode-bridge.service <<'EOF'
[Unit]
Description=CommandCode Bridge - OpenAI-compatible API for CommandCode DeepSeek
After=default.target
[Service]
Type=simple
WorkingDirectory=%h
EnvironmentFile=%h/.config/commandcode-bridge/env
ExecStart=%h/.local/bin/commandcode-bridge
Restart=always
RestartSec=5
TimeoutStopSec=20
KillSignal=SIGTERM
NoNewPrivileges=true
PrivateTmp=true
RestrictSUIDSGID=true
LockPersonality=true
[Install]
WantedBy=default.target
EOFEnable and start:
systemctl --user daemon-reload
systemctl --user enable --now commandcode-bridge
systemctl --user status commandcode-bridge --no-pagerUse this for server-style installs where a dedicated service user owns /opt/commandcode-bridge.
The repository includes a system unit at:
release/systemd/commandcode-bridge.service
Install from a source checkout:
sudo useradd --system --home /opt/commandcode-bridge --shell /usr/sbin/nologin commandcode-bridge || true
sudo mkdir -p /opt/commandcode-bridge
sudo rsync -a --delete ./ /opt/commandcode-bridge/
cd /opt/commandcode-bridge
sudo npm ci
sudo npm run verify
sudo npm run build
sudo npm prune --omit=devCreate the environment file:
sudo cp release/env.production.example /etc/commandcode-bridge.env
sudo chmod 600 /etc/commandcode-bridge.env
sudoedit /etc/commandcode-bridge.envStart the unit:
sudo cp release/systemd/commandcode-bridge.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now commandcode-bridge
sudo systemctl status commandcode-bridge --no-pagerOperate:
sudo journalctl -u commandcode-bridge -f
sudo systemctl restart commandcode-bridge
sudo systemctl stop commandcode-bridgeUse Docker Compose when you want a container boundary and have the full source checkout available.
cd /opt/commandcode-bridge
cp release/env.production.example release/env.production
chmod 600 release/env.production
nano release/env.production
cd release
docker compose up -d --buildVerify:
export BRIDGE_API_KEY='<same value as release/env.production>'
./smoke-curl.sh http://127.0.0.1:9992Notes:
- The Compose service binds inside the container to
HOST=0.0.0.0. - The host port can still be published as
127.0.0.1:9992:9992for local-only exposure. - Do not commit
release/env.production.
If the live service uses the globally installed package, building the source checkout is not enough. Repack and reinstall the package globally, then restart the service.
cd /home/yelixir/workspace/commandcode-bridge
npm run verify
TGZ=$(npm pack --silent | tail -n1)
npm install -g "./$TGZ"
rm -f "$TGZ"
systemctl --user restart commandcode-bridge
systemctl --user status commandcode-bridge --no-pagerThen run smoke:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
npm run smoke| Variable | Default | Description |
|---|---|---|
HOST |
127.0.0.1 |
Bind address. Use 127.0.0.1 for local-only, 0.0.0.0 for Tailscale/VPN/reverse-proxy exposure. |
PORT |
9992 |
HTTP listen port. |
BRIDGE_API_KEY |
unset | Client-facing bearer key. Strongly recommended; required for admin endpoints. |
REQUEST_BODY_LIMIT_BYTES |
1048576 |
Fastify request body limit. Increase only for unusually large prompts/tool schemas. |
RATE_LIMIT_MAX |
60 |
Max requests per rate-limit window per client. |
RATE_LIMIT_WINDOW |
1 minute |
Rate-limit window string accepted by @fastify/rate-limit. |
LOG_LEVEL |
info |
Pino/Fastify log level. Common values: debug, info, warn, error, silent. |
CORS_ORIGIN |
unset | Enables CORS for a specific browser origin. Leave unset for non-browser clients. |
INCLUDE_REASONING |
false |
If true, reasoning deltas are appended to visible content. Keep false for normal OpenAI-compatible use. |
| Variable | Default | Description |
|---|---|---|
COMMANDCODE_API_KEY |
unset | Single upstream CommandCode API key. Also recognized: COMMAND_CODE_API_KEY, CMD_API_KEY. If unset, the bridge can read the normal CommandCode auth file. |
COMMANDCODE_API_KEYS |
unset | Comma-separated multi-key id=key list, for example primary=...,secondary=.... Takes precedence over single-key mode. |
COMMANDCODE_CREDENTIALS |
unset | JSON credential array/object or comma-separated multi-key list. Useful for structured deployment systems. |
COMMANDCODE_CREDENTIALS_FILE |
unset | Path to a JSON credentials file. Highest upstream credential precedence. Recommended for complex multi-key setups. |
COMMANDCODE_UPSTREAM_MODE |
auto |
auto probes the Provider API at startup and uses it when the plan allows (Provider $15/mo or higher); provider forces it; alpha forces the legacy /alpha/generate tunnel for every model. |
COMMANDCODE_ZDR |
false |
Sends x-cmd-zdr: 1 (zero data retention) on Provider API requests. |
COMMANDCODE_API_BASE |
https://api.commandcode.ai |
Upstream CommandCode API base URL. Change only for testing or if CommandCode changes endpoint base. |
COMMANDCODE_DEFAULT_MODEL |
deepseek/deepseek-v4-pro |
Upstream model used by model: "default". |
COMMANDCODE_ALLOWED_MODELS |
Pro + Flash | Comma-separated allowlist. Requests outside this list are rejected unless unknown models are allowed. |
COMMANDCODE_ALLOW_UNKNOWN_MODELS |
false |
Allows arbitrary model IDs to pass through. Not recommended for production. |
COMMANDCODE_CLI_VERSION |
1.14.0 |
Version header sent upstream to match the tested CommandCode CLI behavior. |
COMMANDCODE_TIMEOUT_MS |
600000 |
Upstream generation timeout (10 minutes). Transient failures are retried within this budget. |
COMMANDCODE_RETRY_MAX_ATTEMPTS |
5 |
Total request attempts per chat call, including the first. 429/5xx/timeouts retry with backoff; 401/402/403 skip that key. |
COMMANDCODE_RETRY_BACKOFF_MS |
250 |
Base exponential backoff between retries (doubles per attempt, capped at 2s). |
Credential file shape:
{
"credentials": [
{ "id": "primary", "apiKey": "cmd_key_one", "weight": 1 },
{
"id": "flash-only",
"apiKey": "cmd_key_two",
"weight": 1,
"allowedModels": ["deepseek/deepseek-v4-flash"]
}
]
}| Variable | Default | Description |
|---|---|---|
COMMANDCODE_ROUTING_POLICY |
depletion_aware |
depletion_aware routes by billing/expiry pressure; round_robin rotates eligible keys by weight. |
COMMANDCODE_BILLING_REFRESH_MS |
300000 |
Billing/usage cache TTL per credential. |
COMMANDCODE_BILLING_TIMEOUT_MS |
10000 |
Timeout for billing probes. On probe failure, routing falls back safely rather than hanging requests. |
COMMANDCODE_CREDENTIAL_COOLDOWN_MS |
60000 |
Cooldown after 429/5xx/timeouts. 402 uses at least this and the billing refresh window. |
Routing behavior:
depletion_awareprefers keys whose expiring/monthly credits need to be consumed before reset.- Every policy first prioritizes credentials with a positive monthly/free
expiringBalanceand no more than one day remaining. Purchased-only credits remain reserve capacity and never enter this urgent pool. - depleted or failing keys are cooled down and skipped when alternatives exist.
- application-level stream errors before visible output can fail over to another eligible credential.
- once visible output has been sent, the bridge surfaces the error rather than retrying and duplicating output.
GET /v1/models/:model returns one available model. Known capacity is exposed consistently as context_window, context_length, and max_context_length.
When upgrading a persisted 1.3.1 dashboard catalog, enabled state and custom models are preserved, built-in metadata is refreshed from 1.14.0, and retired built-in IDs are removed instead of being forwarded upstream. Admin writes and restarts require the current BRIDGE_API_KEY; only a keyless loopback runtime can bootstrap without one.
| Variable | Default | Description |
|---|---|---|
COMMANDCODE_EMPTY_VISIBLE_RESPONSE_POLICY |
error_on_length |
If upstream finishes with finish_reason: length before any visible content, return commandcode_empty_visible_response instead of blank success. Use allow only for legacy compatibility. |
This protects clients from treating hidden-token exhaustion as a valid empty answer.
Balance alerts are disabled by default.
| Variable | Default | Description |
|---|---|---|
COMMANDCODE_BALANCE_ALERT_ENABLED |
false |
Enables periodic alert checks. Default is intentionally off. |
COMMANDCODE_BALANCE_ALERT_MIN_CURRENT_BALANCE |
1 |
Alert when current total balance drops below this threshold. Set 0 to disable. |
COMMANDCODE_BALANCE_ALERT_MIN_EXPIRING_BALANCE |
0 |
Alert when monthly/free expiring balance drops below this threshold. Set 0 to disable. |
COMMANDCODE_BALANCE_ALERT_MAX_REQUIRED_DAILY_BURN |
0 |
Alert when required daily burn exceeds this threshold. Set 0 to disable. |
COMMANDCODE_BALANCE_ALERT_INTERVAL_MS |
billing refresh TTL | Periodic alert check interval. |
COMMANDCODE_BALANCE_ALERT_REPEAT_MS |
3600000 |
Duplicate-notification throttle per credential/alert type. |
COMMANDCODE_BALANCE_ALERT_WEBHOOK_URL |
unset | Optional JSON webhook target. Alerts are logged even without a webhook. |
COMMANDCODE_BALANCE_ALERT_WEBHOOK_BEARER |
unset | Optional bearer token for the alert webhook. |
Run this only after adding enough balance/top-up for real content generation.
-
Configure at least two credentials with distinct IDs using
COMMANDCODE_API_KEYSorCOMMANDCODE_CREDENTIALS_FILE. -
Restart the service.
-
Confirm admin diagnostics show all expected IDs:
set -a . "$HOME/.config/commandcode-bridge/env" set +a curl -sS 'http://127.0.0.1:9992/admin/commandcode/credentials?refresh=true' \ -H "Authorization: Bearer $BRIDGE_API_KEY" | jq
-
Confirm every canary key has positive usable balance.
-
Run
npm run smokewithoutSMOKE_ACCEPT_UPSTREAM_ERRORS. -
Send several low-token requests.
-
Check admin metrics again and confirm selection/routing movement according to
COMMANDCODE_ROUTING_POLICY. -
If using Tailscale or another non-localhost path, repeat
/v1/modelsand one chat request through that path.
The bridge is running and requires client auth, but the smoke process did not receive the correct BRIDGE_API_KEY.
Fix:
set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
npm run smokeThe live service probably uses a global npm install. Run npm pack, npm install -g, and restart the actual service manager.
ss -ltnp '( sport = :9992 )'
systemctl --user status commandcode-bridge --no-pagerStop the manual process or change PORT in the env file.
Check linger and enablement:
loginctl show-user "$USER" -p Linger
systemctl --user is-enabled commandcode-bridgeIf linger is disabled:
sudo loginctl enable-linger "$USER"Use routing-only smoke only to prove fail-closed behavior:
SMOKE_ACCEPT_UPSTREAM_ERRORS=1 npm run smokeFor real generation readiness, top up the account and run smoke without that flag.