Skip to main content

proxycli

proxycli is an optional Gateway extension. It collects CLIProxyAPI account quota into one snapshot in shared Valkey. The Orbit web app, the CLI, MCP, and CodexBar read provider pools from that snapshot.

Switch and setup

The extension has one switch and one fleet setup. They are separate.
  • orbit extension:enable proxycli shows the proxycli:* commands, MCP tools, API operations, and the Quota section to every client. It deploys nothing. See extension.
  • orbit proxycli:setup deploys the collector Process and publishes https://collector.cli-proxy-api.orbit.
  • orbit proxycli:teardown stops the collector and withdraws the hostname.
  • orbit extension:disable proxycli hides and refuses the extension. It does not tear down the collector.

Valkey placement

No Node role holds a cache. Shared Valkey runs on the database role as a Node-targeted Docker Process, and a Redis Database connection registers it. Setup refuses a cache connection that does not fit: A connection that names no Node, such as an external Redis host, is accepted. Bind Valkey on the database Node’s WireGuard address. The collector Node and the Gateway both connect to it with the host, port, username, and password of the connection.

Set up

Place Valkey, register it, enable the extension, then set up the collector. This example uses a loopback URL with task VMs off; with task VMs enabled, replace it with ORBIT_TASK_VMS_MODEL_PROXY_ORIGIN:
The collector Node must be an active Linux Node with a WireGuard address. Otherwise setup fails with proxycli.node_invalid. --cliproxy-url is the CLIProxyAPI Management API origin, as the collector Node sees it. With task VMs off, use http://127.0.0.1:8317 when CLIProxyAPI runs on that Node. When task VMs are enabled (ORBIT_TASK_VMS_ENABLED), the URL must equal ORBIT_TASK_VMS_MODEL_PROXY_ORIGIN (task_vms.model_proxy_origin), the CLIProxyAPI origin used by Pi on the VMs. Use that configured reachable origin, for example http://10.44.0.3:8317, rather than copying a collector’s old loopback URL. Setup refuses a mismatching URL before saving settings or changing the collector, with HTTP 422 validation.failed for cliproxy_url. It never silently changes the task VM origin. This check belongs to proxycli setup; it does not change task VM provisioning or scheduling. Configure task VMs owns that origin’s format and reachability requirements. The Gateway stores the management key as a secret setting and passes it to the collector Process. No API response contains it. Setup can run again. It keeps the read and control tokens, writes the collector script, updates the collector Process in place, and publishes the hostname again. On the same Node, the collector keeps its Process ID rather than being destroyed and created again. The fleet rollout and orbit node:converge also write a changed script after a Gateway release and restart the collector Process. A script that already matches stays, and a completed collector keeps running. A legacy collector without a source revision first compares the remote source without changing it; matching source does not require restart. Before publishing changed source, the Gateway records pending source activation on the Process; it clears that intent and records the revision only after activation succeeds. An interruption after publication leaves activation pending, so retry restarts an existing running collector even when the source file is already current. A completed unchanged retry does not restart the collector. Setup with another --node sets up only the new Node and leaves the old Node’s collector in place. To move the collector, run proxycli:teardown first.

Follow the cache connection

The collector depends on the configured Database connection, not a permanent copy of its credentials. Changing that connection’s host, port, username, or password reconverges the dependent Process environment. The Gateway rebuilds the collector’s Redis environment from the current connection and updates the same Process in place. A running collector restarts to use the new values; a stopped collector stays stopped. Its Process ID, configured cache connection, management key, read token, and control token stay the same. No manual teardown or second setup is needed for a connection update. The environment is a name-to-value map, including for this Gateway-owned systemd Process. The connection-derived keys are PROXYCLI_CACHE_HOST, PROXYCLI_CACHE_PORT, PROXYCLI_CACHE_USERNAME, and PROXYCLI_CACHE_PASSWORD; the port is a string value too. Rebuilding these keys preserves the collector’s other required environment values. Responses, Activity, errors, and logs do not reveal credentials. Processes and schedules owns the shared representation and redaction contract. The Gateway reconverges only dependent Processes, not every Process on the Node. An invalid cache connection is refused by the same placement and driver checks as setup. Before accepting a connection update, the Gateway validates both the connection fields and these dependent-use constraints. A validation refusal leaves the stored connection and collector unchanged.

Connection persistence and retry recovery

After validation, the Gateway persists the connection change before reconverging its dependent Processes. The connection record is the source of truth for rebuilding their environment. A failure in remote convergence does not roll back the connection or restore the old credentials. The caller receives failure, and each failed Process records its lifecycle failure rather than claiming that the collector has refreshed. Other dependents may already have converged; a Database connection update is not an atomic transaction across all runtimes. Every accepted connection update, including an identical retry with no changed connection fields, rebuilds and checks the environment of its dependents. It must not skip this work merely because the stored connection already matches the request. This repairs a crash after the connection was saved but before any dependent was updated, as well as a failure partway through updating several dependents. Each Process follows the persisted intent and retry recovery contract: stale or incomplete work reconverges in place, while an unchanged, completed runtime is not restarted. If a response is lost or the Gateway crashes, inspect the saved connection and dependent Process states, then repeat the identical connection update after fixing the cause. Retry can resume from either the old collector environment or an already saved but unapplied environment; both are rebuilt from the current connection. Do not interpret a saved connection as proof of completion, or overwrite a newer connection change by blindly replaying an older request. Setup on the same Node can also reconverge the collector with the current connection, preserving its ID and tokens. These retries may repeat an unrecorded restart; they do not promise exactly one remote operation.

What setup deploys

Setup places these pieces on the collector Node and in Gateway settings. The Process runs /usr/bin/python3 /var/lib/orbit/proxycli/server.py. systemd does not search PATH, so the command names the absolute path. The unit receives the PROXYCLI_* values as Environment= directives: the CLIProxyAPI URL and management key (PROXYCLI_MANAGEMENT_KEY), the read and control tokens, the port, and the Valkey host, port, username, and password. The script uses that management key to read auth-file models. It receives no client API key. Processes and schedules describes that environment. While setup holds the collector, process:destroy refuses to remove the Process with process.required_by_proxycli. collector.cli-proxy-api.orbit is a reserved platform name, so route:create refuses it with route.domain_conflict. The apex cli-proxy-api.orbit is not reserved. Publish the CLIProxyAPI management UI there as a custom proxy Route to http://127.0.0.1:8317. Before it changes anything, setup checks for a Route on collector.cli-proxy-api.orbit. It takes over a custom proxy Route on the collector Node to http://127.0.0.1:8787 with no Process target: one Caddy reload moves the name to the collector site, and setup then removes the Route. Any other Route on that name fails setup with proxycli.hostname_taken (409). Setup runs the shared Caddy package step before it publishes the Caddy site and updates the collector Process in place. An installed Caddy at or above the floor stays in place. The site retries the loopback collector for up to 5 seconds, so a request that arrives during the restart waits instead of failing.

Collection

The collector is the only process that calls CLIProxyAPI for quota. Every minute it takes the Valkey lock orbit:proxycli:lock for up to 120 seconds. When another holder has the lock, it skips the round. It lists the CLIProxyAPI auth files, fetches quota for each account that is due through POST /v0/management/api-call, and writes orbit:proxycli:raw and orbit:proxycli:snapshot. It remembers each auth file’s models in orbit:proxycli:model-files. Each account’s next check is stored in Valkey, so a restart or an account toggle does not reset the schedule. collected_at is the time of the last snapshot write. checked_at and next_check_at belong to each account. The collector reads Claude, Codex, Grok, Kimi, and Antigravity windows from CLIProxyAPI’s wrapped response. It names common windows by duration, such as 7d and 5h. It sorts by the last character of the label: labels that end in d or w first, then h, m, or s, then all others, such as Monthly credits. A window the provider did not return is absent. The collector never reports a missing window as zero use, and never labels a window Primary or Secondary. The collector’s own endpoints leave out a window whose reset time has passed. Gateway reads still show it until the next check. When Grok returns no percentage, the collector calls Grok’s billing RPC through CLIProxyAPI. It accepts zero use only from a complete response with an active weekly or monthly period, as CodexBar does.

Collector endpoints

The collector serves these paths on https://collector.cli-proxy-api.orbit. The read token and the control token are bearer tokens in Gateway settings. Account updates trust only the Orbit CA at ORBIT_HOME/ca/root.pem. The Gateway checks that CA in place and sends through Laravel’s HTTP client with the system CA directory disabled and a 10-second overall deadline. It does not copy the CA to a temporary file. Every read uses the Valkey snapshot and never calls CLIProxyAPI. An account toggle reads GET /v0/management/auth-files to find the account’s file, sends PATCH /v0/management/auth-files/status, then compiles the pools again from the cached snapshot. It fetches no quota. An account without a file returns 404. The CLIProxyAPI management key is not a CodexBar credential.

Clients

The Gateway reads the snapshot from Valkey through the cache connection. proxycli:list, proxycli:show, proxycli:models, and the Quota pages use it. A read or toggle before setup, or after teardown, fails with proxycli.disabled (409). proxycli:update checks that the account is in the snapshot, or fails with resource.not_found. The Gateway then sends PATCH https://{node-wireguard-ip}:443/v1/accounts/{account} with Host: collector.cli-proxy-api.orbit, Orbit CA verification, and the control token. The account ID must match [A-Za-z0-9._-]+. The Gateway then writes the snapshot again with the new state. That write sets collected_at to the toggle time and drops each account’s checked_at and next_check_at until the next collection round. The schedule itself is unchanged. When the collector refuses or cannot be reached, the call fails with proxycli.upstream_failed (502). The web app shows the Quota section while the proxycli extension is enabled, and reads it every 60 seconds. It shows provider quota when the collector is configured, whether or not the tasks extension is enabled. When the collector is not configured, the page names what is missing. A provider page lists the accounts, the remaining quota and reset time of each window, and the controls to enable or disable an account. proxycli:status reports whether the collector is set up, its hostname, Node, cache connection, and collected_at.

Models

GET /api/v1/proxycli/models (proxycli:models) lists the models CLIProxyAPI offers. Each item is {id, provider}. The route reads that list from the collector snapshot and does not call CLIProxyAPI. On each poll, for every auth file from GET /v0/management/auth-files, the collector reads GET /v0/management/auth-files/models?name= that file’s name. It sends the management key as Authorization: Bearer. It does not call GET /v1/models. That route checks a client API key, and the management key is valid only for /v0/management. When that request fails, the collector keeps the models it stored for that auth file on the previous poll. A file that answers replaces only its own models. A file the auth list does not include drops out of the union. A models entry always has id. It includes owned_by, display_name, and type only when the registry set them. The collector stores one {id, provider} per id. It does not store display_name or type. A repeated id keeps the provider from the first auth file. When owned_by is present, the collector maps it with the table below. When owned_by is absent, it maps the auth file’s provider with that same table. The management list sets that file’s provider and type to one value. The entry’s type is not a provider, so the collector does not map it. A model with neither owned_by nor an auth-file provider is omitted. provider is the ProxyCli provider that serves the model: antigravity keeps its name, and it is a ProxyCli provider. The route refuses with proxycli.disabled (409) while the collector is not set up. A snapshot that has no models returns an empty list. A collector deployed before this read has no models until proxycli:setup runs again and the next poll stores them.

Check the collector

After setup, confirm that exactly one collector polls:

Teardown

Tear down the collector when collection should stop for the whole fleet.
Teardown removes the collector Process and script, withdraws the Caddy site and its certificate, and publishes private DNS without the collector name. It deletes the stored management key and the read and control tokens. A later setup needs the key file again and gives CodexBar a new read token. Valkey data and the Redis connection stay. Teardown does not change the extension switch.

Errors

These codes come from the Gateway on setup, teardown, reads, and toggles.

Why it works this way

These reasons explain the design. Check them before you propose a change.

An extension, not a Node role

Quota collection is optional fleet infrastructure, not a capability of one Node. A proxycli or valkey role would split cache placement from the other database Processes on the database role. So the extension places one Process and reuses a registered Redis connection.

One collector, one snapshot

A refresh of the web page or CodexBar must never start an upstream poll. A second polling loop in the Gateway would call CLIProxyAPI on every refresh. So one collector polls under a Valkey lock, and every client reads the snapshot. proxycli:models reads the model list from that same snapshot.

A reserved name, not a Route

If a custom proxy Route served the collector, anyone who changed or destroyed that Route would change the CodexBar endpoint outside the extension lifecycle. So the Gateway reserves collector.cli-proxy-api.orbit and publishes it itself. The collector name sits under cli-proxy-api.orbit, the name of the management service it reports on, and the apex stays free for the management Route.

Missing quota is not zero

A window the provider did not return is unknown. Showing it as zero use would tell the operator that quota is free when it may be exhausted. So a missing window is absent, and the collector names windows by duration, as the CLIProxyAPI management UI does.