Skip to content

Latest commit

 

History

History
542 lines (402 loc) · 23.4 KB

File metadata and controls

542 lines (402 loc) · 23.4 KB

CommandCode Bridge Deployment Guide

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

Recommended production shape

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_KEY enabled for anything beyond local ad-hoc testing.
  • Bind to 127.0.0.1 for local-only use, or 0.0.0.0 only 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.

Current host deployment

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:19992 via 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 Linger

Expected:

  • 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 -f

Router health:

curl -sS http://127.0.0.1:9992/health | jq

Direct local backend health, bypassing the router:

curl -sS http://127.0.0.1:19992/health | jq

Authenticated 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" | jq

Router 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" | jq

Smoke 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 smoke

If 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 smoke

SMOKE_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" | jq

The 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.

Router mode for multiple bridge hosts

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=60000

If 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.

One-command user systemd install/uninstall

For Raspberry Pi or Linux hosts that should run the bridge as the current user, run this from the repository root:

./install.sh

The 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 9992

Remove the service and installed files while preserving credentials/env:

./uninstall.sh

Remove credentials/env too:

./uninstall.sh --purge-config

User systemd deployment

Use 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-running

Enable linger so the user service starts at boot even before login:

sudo loginctl enable-linger "$USER"
loginctl show-user "$USER" -p Linger

Create 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/env

Minimal 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=false

If 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_two

Create 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
EOF

Enable and start:

systemctl --user daemon-reload
systemctl --user enable --now commandcode-bridge
systemctl --user status commandcode-bridge --no-pager

System-level systemd deployment

Use 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=dev

Create the environment file:

sudo cp release/env.production.example /etc/commandcode-bridge.env
sudo chmod 600 /etc/commandcode-bridge.env
sudoedit /etc/commandcode-bridge.env

Start 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-pager

Operate:

sudo journalctl -u commandcode-bridge -f
sudo systemctl restart commandcode-bridge
sudo systemctl stop commandcode-bridge

Docker Compose deployment

Use 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 --build

Verify:

export BRIDGE_API_KEY='<same value as release/env.production>'
./smoke-curl.sh http://127.0.0.1:9992

Notes:

  • 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:9992 for local-only exposure.
  • Do not commit release/env.production.

Updating the global npm deployment

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-pager

Then run smoke:

set -a
. "$HOME/.config/commandcode-bridge/env"
set +a
npm run smoke

Configuration options

Server and client-auth options

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.

CommandCode upstream options

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"]
    }
  ]
}

Multi-key routing options

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_aware prefers keys whose expiring/monthly credits need to be consumed before reset.
  • Every policy first prioritizes credentials with a positive monthly/free expiringBalance and 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.

Empty visible-content policy

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 alert options

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.

Multi-key canary checklist

Run this only after adding enough balance/top-up for real content generation.

  1. Configure at least two credentials with distinct IDs using COMMANDCODE_API_KEYS or COMMANDCODE_CREDENTIALS_FILE.

  2. Restart the service.

  3. 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
  4. Confirm every canary key has positive usable balance.

  5. Run npm run smoke without SMOKE_ACCEPT_UPSTREAM_ERRORS.

  6. Send several low-token requests.

  7. Check admin metrics again and confirm selection/routing movement according to COMMANDCODE_ROUTING_POLICY.

  8. If using Tailscale or another non-localhost path, repeat /v1/models and one chat request through that path.

Troubleshooting

/health works but smoke returns 401

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 smoke

Source checkout verifies but live behavior does not change

The live service probably uses a global npm install. Run npm pack, npm install -g, and restart the actual service manager.

Port 9992 is already in use

ss -ltnp '( sport = :9992 )'
systemctl --user status commandcode-bridge --no-pager

Stop the manual process or change PORT in the env file.

Service does not start at boot

Check linger and enablement:

loginctl show-user "$USER" -p Linger
systemctl --user is-enabled commandcode-bridge

If linger is disabled:

sudo loginctl enable-linger "$USER"

Upstream balance/credit failure

Use routing-only smoke only to prove fail-closed behavior:

SMOKE_ACCEPT_UPSTREAM_ERRORS=1 npm run smoke

For real generation readiness, top up the account and run smoke without that flag.