CLIProxyAPI Gateway Runbook

September 7, 2026 · View on GitHub

remora does not install or manage CLIProxyAPI. This runbook documents the boundary between them and keeps OAuth enrollment as an explicit human step.

Contents

繁體中文

Quick Docker Compose deployment

This example follows the upstream Docker Compose guide and configuration schema. It exposes the proxy and management UI to the LAN while keeping the Codex OAuth callback on loopback for an SSH tunnel.

RequirementMinimum
HostThe remora computer itself, or a Linux server/NAS reachable from it
RuntimeDocker Engine with the Compose plugin
NetworkTrusted LAN or VPN; use TLS when crossing an untrusted network
SecretsSeparate random proxy API key and management key

Create a private deployment directory and generate two independent secrets:

mkdir -p ~/containers/cliproxyapi/{auths,logs}
chmod 700 ~/containers/cliproxyapi ~/containers/cliproxyapi/auths
cd ~/containers/cliproxyapi

openssl rand -base64 32  # proxy API key: remora uses this
openssl rand -base64 32  # management key: browser GUI uses this

⚠️ Keep the two values separate. The proxy API key authorizes model requests. The management key can change configuration and OAuth credentials, so it is more privileged.

Choose the host topology

CLIProxyAPI can run on the same computer as remora or on another machine. Decide before creating the files because the published port, management policy, callback handling, and remora URL differ.

SettingSame computerSeparate host / home lab
Proxy portBind 8317 to loopback onlyPublish 8317 to a trusted LAN/VPN
Management APIallow-remote: falseKeep allow-remote: false; reach it through SSH
Management URLhttp://127.0.0.1:8317/management.htmlhttp://127.0.0.1:8318/management.html through the tunnel
OAuth callbackBrowser reaches local 1455 directlyKeep server 1455 on loopback and use an SSH tunnel
remora base_urlhttp://127.0.0.1:8317http://SERVER_LAN_IP:8317
Network controlNo LAN exposureFirewall 8317 to the remora client subnet/address

The examples below use the safer same-computer defaults. For a separate host, apply every change marked separate host.

Create config.yaml, replacing both placeholder values:

host: ""
port: 8317

remote-management:
  allow-remote: false
  secret-key: "REPLACE_WITH_MANAGEMENT_KEY"
  disable-control-panel: false

auth-dir: "/root/.cli-proxy-api"

api-keys:
  - "REPLACE_WITH_PROXY_API_KEY"

debug: false
logging-to-file: false
usage-statistics-enabled: false
request-retry: 3
max-retry-interval: 30
disable-cooling: false
save-cooldown-status: false

Protect the configuration after saving it:

chmod 600 config.yaml

Create compose.yaml:

services:
  cli-proxy-api:
    image: eceasy/cli-proxy-api:latest
    pull_policy: always
    container_name: cli-proxy-api
    ports:
      - "127.0.0.1:8317:8317"
      - "127.0.0.1:1455:1455"
    volumes:
      - ./config.yaml:/CLIProxyAPI/config.yaml
      - ./auths:/root/.cli-proxy-api
      - ./logs:/CLIProxyAPI/logs
    restart: unless-stopped

For a separate host, publish only the proxy port to the LAN; the configuration can keep allow-remote: false:

# compose.yaml, under services.cli-proxy-api.ports
ports:
  - "8317:8317"
  - "127.0.0.1:1455:1455"

Keep port 1455 on server loopback. Restrict 8317 with the host firewall to the remora machine or trusted VPN subnet. Although the management page and model endpoint share this port, allow-remote: false rejects management requests that do not arrive from server localhost.

If a trusted LAN must access the management UI directly, allow-remote: true is an explicit alternative. It makes the management key the remaining protection for those routes; prefer the SSH method below.

⚠️ latest follows the upstream quick start but is not reproducible. After validating a version, pin its immutable image digest for a shared or production-like deployment.

Do not commit the deployment directory. If it sits inside another repository, add these entries to that repository's .gitignore:

config.yaml
auths/
logs/

Start and inspect

Start the service and confirm that it loaded one configuration without exposing either key:

docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail 100 cli-proxy-api

The management panel is served by CLIProxyAPI itself. On a separate host, create the management and OAuth callback tunnels together:

ssh \
  -L 8318:127.0.0.1:8317 \
  -L 1455:127.0.0.1:1455 \
  USER@SERVER_LAN_IP

Choose the management URL for the topology:

Same computer: http://127.0.0.1:8317/management.html
Separate host through SSH: http://127.0.0.1:8318/management.html

Enter the management key when prompted. If the browser is not on the same trusted network, use a VPN or TLS reverse proxy instead of exposing port 8317 directly to the internet.

OAuth enrollment in the GUI

OAuth is intentionally a human handoff. Open the management panel, select the OAuth section, start a Codex login, and follow the displayed authorization flow. The official Management Center supports Codex OAuth and callback submission.

On the same computer, no tunnel is needed because the browser can reach the loopback callback directly. On a separate Docker host, keep the two-forward SSH session from the previous section open until the browser flow completes. If the panel asks for the final callback URL or authorization result, paste it into the panel rather than storing it in a shell script. Successful enrollment creates a Codex JSON credential under the mounted auths/ directory.

⚠️ Never automate browser login or copy OAuth JSON into remora. The auths/ directory contains refresh material and belongs only on the gateway host.

The deployment is ready for remora when the gateway returns a model catalog with the configured bearer token:

curl --fail --silent --show-error \
  -H "Authorization: Bearer ${REMORA_AUTH_TOKEN}" \
  http://127.0.0.1:8317/v1/models

For a separate-host deployment, replace loopback with the gateway's LAN or VPN address. An HTTP 200 response and a non-empty .data model list confirm the proxy API key and OAuth credential are both usable.

remora connection

[proxy]
base_url = "http://127.0.0.1:8317"
auth_token_env = "REMORA_AUTH_TOKEN"
auth_token_command = []

If the gateway runs on another trusted host, replace loopback with its LAN or VPN address. Prefer HTTPS when traffic crosses an untrusted network.

Model aliases

remora forwards model names exactly as configured. Confirm every name appears in the gateway model catalog or is a documented alias:

remora fieldDefault
models.maingpt-5.6-sol
models.default_opusgpt-5.6-sol
models.default_sonnetgpt-5.6-sol
models.default_haikugpt-5.6-luna

Run the local checks after changing aliases:

remora agents
remora doctor --online
remora dry-run

Context-window alignment

Do not copy the public OpenAI API context number into CLIProxyAPI metadata. The official GPT-5.6-sol, GPT-5.6-terra, and GPT-5.6-luna docs each state 1,050,000 total context and 128,000 maximum output. In the live CLIProxyAPI catalog on 2026-08-17, all three returned context_window: 272000, max_context_window: 921000, and auto_compact_token_limit: null.

Stock CLIProxyAPI exposes that value without any server modification:

curl -fsS \
  -H "Authorization: Bearer $REMORA_AUTH_TOKEN" \
  'http://127.0.0.1:8317/v1/models?client_version=remora' \
  | jq '.models[] | select(.slug | test("^gpt-5\\.6-(sol|terra|luna)$")) | {slug, context_window, max_context_window, auto_compact_token_limit}'

Direct /v1/responses probes through the Codex OAuth upstream for each slug accepted reported usage.input_tokens=921,858; the adjacent 921,859 returned invalid_request_error with code context_too_large. Payload calibration measured fixed 302-token translated overhead, so repeated-input counts were 921,556 versus 921,557. On Luna, max_output_tokens values 16, 17, 128, 1000, and 128000 all accepted at reported input 921,858, so that parameter did not move the observed boundary. The official 1.05M total and 128K maximum-output facts are consistent context, but do not independently establish this exact OAuth input boundary.

EvidenceAll three exact GPT-5.6 slugs
Official total context1,050,000
Largest accepted OAuth input921,858
First rejected OAuth input921,859
CLIProxyAPI max_context_window (2026-08-17)921,000
Fresh Codex cache max_context_window (2026-08-17 08:41 UTC)872,000

Remora reads this metadata read-only; it never writes native Codex or CLIProxyAPI configuration. In calico mode, max_context_window is considered only for the exact gpt-5.6-sol, gpt-5.6-terra, and gpt-5.6-luna slugs. It must be a positive non-bool integer; missing, null, boolean, string, zero, or negative values fall back to the existing context_window (and gateway context_length) fields. Other slugs keep their existing field semantics.

Remora compares the gateway value with fresh Codex models_cache metadata for each configured model and uses the smaller value. With a 921,000 client window, the configured 90% compact ratio produces an 828,900 trigger. A missing, stale, or incomplete cache uses the configured fallback window instead. CLIProxyAPI needs no context YAML edit or restart.

remora's safe default follows stock Claude Code's 200K limit for unknown custom model ids. In stock mode it does not inject context or compact overrides; Claude's native output reserve and precompute policy remain authoritative.

The optional calico mode requires a verified Calico Claude binary. It takes the smaller value from the gateway catalog and a fresh local Codex runtime cache for every configured model, then passes that exact map into Calico's dormant adapter. If both sources advertise 921K, the resulting client window is 921K, effective context is 874.95K, and the compact trigger is 828.9K. If the fresh cache advertises 872K, discovery intentionally caps the client at 872K. A missing, older-than-five-minutes, or incomplete Codex cache uses the configured fallback, which defaults to 272K. remora refuses to launch this mode if the binary does not contain the adapter marker. The discovered window is separate from OAuth entitlement and does not change remora's [context].auto_compact_percent behavior.

SourceMeaning
Gateway model metadatamax_context_window for the exact GPT-5.6 family; existing context_window/context_length otherwise
Fresh Codex models_cache metadataAuthoritative Codex runtime ceiling; read-only, metadata only
[context].mode = "stock"Stock-safe 200K client behavior; default
[context].mode = "calico"Explicit opt-in to the verified custom-context adapter
[context].stock_windowStock Claude Code custom-model window, normally 200K
[context].fallback_windowConservative value used when catalog lookup is unavailable or incomplete
[context].codex_fallback_windowSafe Codex ceiling when its runtime cache cannot be trusted; defaults to 272K
[context].codex_cache_ttl_secondsFreshness limit for the Codex runtime cache; matches Codex's 300-second TTL
[context].codex_models_cacheOptional path override; otherwise uses $CODEX_HOME/models_cache.json or ~/.codex/models_cache.json
[context].effective_window_percentDiagnostic effective-input ratio; Codex defaults to 95%
[context].auto_compact_percentChild auto-compaction ratio; Codex defaults to 90%
Existing Claude auto-compact environment variablesExplicit user overrides, capped to the Codex client ceiling in Calico mode

Compact request hardening (Calico + gateway)

remora decides when auto-compact may fire (context map + ratio). It does not rewrite compact product fields and does not inject CALICO_COMPACT_* by default. Only REMORA_ACTIVE=1 is set on the child so a verified Calico binary can opt into remora-scoped compact behavior.

LayerOwnerResponsibility
remora launcherremoraChild-only REMORA_ACTIVE=1; calico context map and auto-compact ratio
Compact body policyCalico (when REMORA_ACTIVE=1 and query source is compact)Optional env: CALICO_COMPACT_EFFORT (default medium), CALICO_COMPACT_MODEL (empty keeps session model), CALICO_COMPACT_DISABLE_THINKING (1 forces thinking off; default leaves session thinking)
Compact class guardCLIProxyAPIOn X-Calico-Request-Source: compact only: absolute wall-clock + single-shot stream retries; never rewrites model/effort/thinking

Calico emits x-calico-request-source: compact so the gateway can classify compact without inspecting the body. Spoofed custom headers with that name are stripped on every remora request; the owned value is re-added only for true compact sources.

The stock eceasy/cli-proxy-api:latest image does not implement the compact class guard. A compatible build must contain it; on stock the streaming.compact block below is silently ignored because unknown YAML keys are not rejected, so compact requests keep the gateway default stream behavior. remora never installs, upgrades, or selects a CLIProxyAPI build — it only points the child at the configured base_url.

Recommended gateway config for remora + Calico long sessions:

streaming:
  compact:
    enabled: true
    # Successful auto-compacts often need 2–5 minutes of user-wait wall-clock on
    # ~245K contexts. 90s mis-kills healthy attempts; 600 is the config cap.
    max-duration-seconds: 600

Stock Claude binaries and non-remora Calico launches keep native compact behavior. Gateway compact guards stay fail-closed until both enabled: true and the Calico header are present. Details and env defaults live in the Calico Claude README.

Claude Code also floors stream idle at 5 minutes (max(env, 300000)). remora injects child-only CLAUDE_STREAM_IDLE_TIMEOUT_MS and CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS (default 600000) so long compact streams are less likely to abort with context canceled before the gateway 600s cap. Configure under [runtime]:

[runtime]
stream_idle_timeout_ms = 600000
byte_stream_idle_timeout_ms = 600000
# Set either key to 0 to skip injection for that env.

Experimental active-turn bridge

The stock eceasy/cli-proxy-api:latest image does not currently preserve Codex x-codex-turn-state across separate Claude tool-result requests. A compatible build must contain the v1 bridge and must opt in explicitly:

codex:
  active-turn-bridge: true

Version 1 fails closed unless the runtime topology is safe:

RequirementWhy it is mandatory
Exactly one enabled Codex credentialA server-issued turn token belongs to the credential that received it
Credential-level disable_cooling: true or global cooling disabledThe selector must not hide that credential before a recognized continuation reaches the executor
One local CLIProxyAPI processTurn state is intentionally memory-only and is not shared through Home KV
Calico binary with calico-active-turn-adapter:v1Stock Claude does not send a stable user-prompt boundary
remora doctor --online reports protocol v1 readyThe binary and gateway capability must agree before parity is assumed

The gateway advertises readiness only as:

X-CLIProxyAPI-Codex-Active-Turn: 1

when every runtime requirement above is satisfied. Multiple credentials or Home mode remove the header rather than silently using unsafe failover. The raw backend turn-state value is retained only in bounded process memory, masked from request logs, and represented in debug diagnostics only by a truncated SHA-256 fingerprint.

Availability: this bridge is not part of upstream CLIProxyAPI v7.2.71. Until a remora-maintained build or upstream release is published, the normal deployment instructions above intentionally install the stock gateway and doctor --online will report active-turn mode as degraded.

429 diagnosis

OpenAI documents an important Codex behavior: when a usage limit is reached during an active turn, Codex may continue that turn under fair-use limits and enforce the limit afterward. This is a turn-level product behavior, not a promise that every intermediary request will avoid HTTP 429. See What happens if I reach a usage limit while Codex is working?.

CLIProxyAPI v7.2.67 operates at request and credential level. Its Codex executor recognizes usage_limit_reached and reset metadata, but the auth scheduler marks every resulting HTTP 429 as quota exhaustion and cools the credential/model unless cooling is disabled. A Claude Code turn can require multiple translated upstream requests, so a local cooldown can interrupt the remaining work even when native Codex would have been allowed to finish its active turn.

LayerObserved responsibility
OpenAI Codex productMay grant active-turn continuation after a plan limit is crossed
OpenAI upstream endpointCan still return usage_limit_reached, capacity, connection, or transient rate-limit errors
CLIProxyAPI executorConverts Codex limit signals and reset metadata into an HTTP 429 result
CLIProxyAPI auth schedulerTreats that 429 as quota and makes the credential/model locally unavailable
Claude CodeRetries according to its own client policy, but cannot select a credential hidden by the gateway scheduler

Use latency and response body to separate upstream limiting from local gateway cooldown:

ObservationInterpretation
First 429 takes hundreds of milliseconds or longerRequest likely reached the upstream provider
Later 429 responses return in a few millisecondsGateway selector is rejecting a cooled credential locally
Response contains model_cooldownNo credential is currently selectable for that model
Restart immediately clears the conditionCooldown state was memory-only, not an expired OAuth token

For CLIProxyAPI v7.2.67, generic 429 handling can promote the credential/model into a quota-style cooldown. With one credential, that becomes a full model blackout. Keep cooldown persistence disabled unless you explicitly need it, lower remora concurrency when the provider is sensitive, and retain the original upstream 429 body when debugging.

To keep a single file-backed Codex OAuth credential selectable after an upstream 429, add the following top-level field to that credential's JSON file inside the mounted auths/ directory:

{
  "disable_cooling": true
}

The fragment above is illustrative: preserve every existing token and account field. Back up the file, restrict it to the service account, and restart or reload CLIProxyAPI after editing it. This credential-scoped override is preferable to the global disable-cooling: true setting when the gateway also serves other providers.

What the override changesWhat it does not change
Prevents CLIProxyAPI from placing that credential/model into its local cooldown schedulerCannot suppress or bypass a 429 returned by OpenAI
Allows the client retry policy to keep reaching the upstream providerCannot make an exhausted quota usable
Avoids a memory-only model_cooldown blackout with a single credentialDoes not provide failover; add another eligible credential for that

Current conclusion: the evidence supports a gateway-induced second-stage block, not proactive blocking based on the usage percentage. CLIProxyAPI first observes an upstream failure, then its local scheduler prevents later requests. Credential-scoped disable_cooling removes that second-stage block, but it cannot guarantee native Codex active-turn continuation because Claude Code and Codex may divide a turn into different upstream request boundaries.

To capture a future reproduction without exposing source or tokens, record the first failing response's timestamp, latency, HTTP status, error.type, error.code, reset fields, and whether the next response is an immediate model_cooldown. This distinguishes usage_limit_reached from transient 429 and provides enough evidence for an upstream CLIProxyAPI issue.

⚠️ A container restart is a recovery lever, not a root-cause fix. It erases memory-only selector state and can send traffic straight back into a real upstream rate limit.

Data and backup policy

PathContainsBackup guidance
config.yamlAPI keys, management policy, aliasesEncrypted secret backup
auths/OAuth access and refresh materialEncrypted, access-restricted backup
logs/Requests/errors; may contain source or prompts when debuggingShort retention; inspect before sharing
remora TOMLGateway address and token retrieval commandSafe to version only after removing host-specific secrets