A local, token-safe Usage Observatory for CLIProxyAPI.
See every request, split input/cache/output tokens, estimate API-equivalent
cost, observe quota resets, and understand account-pool retries from one
private-by-default dashboard. It runs as a transparent sidecar: clients can
send traffic through 8327, while the optional read-only queue collector also
captures clients that still use 8317.
The screenshot is a real dashboard render: token totals, cost estimates, quota percentages, trends, model usage, and every account card remain visible. Only account names/identifiers and the account column in recent tables are replaced with neutral demo labels.
This is an observability tool, not a billing API. It cannot read an official ChatGPT subscription balance. Dollar figures are API-price equivalents, and quota figures are observed/provider-reported windows—not invoices or guaranteed remaining balances.
client -> 127.0.0.1:8327/v1/... -> usage meter -> 127.0.0.1:8317/v1/...
CLIProxyAPI can fan one logical request across several subscription accounts. That makes a raw proxy log difficult to answer: what did I actually consume, which account handled it, how much was cached, and why did the account pool retry? This sidecar keeps those questions separate and auditable in SQLite.
| Capability | What is tracked |
|---|---|
| Token accounting | Non-cached input, cached input, output, reasoning subset, and raw API processing |
| Cost estimation | Collector-frozen per-request prices or official OpenAI short/long-context prices, split by token type |
| Account behavior | Per-subscription calls, success/failure, models, dates, and token totals |
| Quota visibility | Read-only 5-hour/week/month snapshots, reset times, cooldowns, and observed floors |
| Collection paths | Transparent 8327 proxy, optional destructive-read 8317 usage queue, and default-on read-only Cockpit Tools import |
| Dashboard | Inline, dependency-free /usage HTML with trend, account, model, and recent-call views |
| Privacy boundary | Loopback by default; credentials and request metadata discarded; email is memory-only |
git clone https://github.com/LuckyJoeshp/cliproxyapi-usage-meter.git
cd cliproxyapi-usage-meter
# Keep CLIProxyAPI on 8317; point only the clients you want observed at 8327.
PORT=8327 UPSTREAM=http://127.0.0.1:8317 \
scripts/start_cliproxy_usage_meter.shOpen http://127.0.0.1:8327/usage.
The launcher applies an owner-only umask; on POSIX the meter also attempts to
repair existing SQLite, WAL, and SHM permissions to 0600 when opened
directly. Runtime databases remain local data and are intentionally not part
of the repository (Windows relies on ACLs).
The dashboard also monitors direct ChatGPT App Codex sessions when local Codex
JSONL history is available. By default it follows the current account in
CODEX_APP_HOME (~/.codex) and auto-discovers Cockpit Codex instance homes
from ~/.antigravity_cockpit/codex_instances.json. Each home is resolved and
must remain inside the current user's home directory. Only token-count and
rate-limit metadata is read; prompts, code, tool output, and credentials are
never persisted.
The first dynamic scan of a home or newly discovered JSONL establishes an
end-of-file safety boundary. An account change establishes a new boundary
before collection resumes, so unread historical records cannot be guessed to
belong to the newly selected account. New token_count records after that
boundary are attributed to the canonical subscription when structured member
evidence is available; sparse workspace-only auth remains anonymous and
produces no quota card. Set
CODEX_APP_USAGE_ALIAS or pass --codex-app-alias only when strict legacy
single-home matching is desired. The /usage manual import form is exposed in
that fixed-alias mode only. Dollar values are API-equivalent estimates, not Pro
subscription billing.
The Cockpit Tools collector is enabled by default. It opens Cockpit's request
log database read-only and imports request_logs rows into the same dashboard
with source=cockpit_tools. Set COCKPIT_TOOLS_DATA_DIR or pass
--cockpit-tools-data-dir /path/to/cockpit-data to select the data directory;
otherwise it looks for
~/.antigravity_cockpit/codex_local_access_logs.sqlite. An existing database
with an empty request_logs table is a normal initial state, not a startup
error.
On macOS the meter also auto-discovers Cockpit's WebKit accounts cache for
identity and quota mapping. Use
COCKPIT_TOOLS_LOCALSTORAGE_DB or
--cockpit-tools-localstorage-db /path/to/cockpit-webkit/localstorage.sqlite3
to override that discovery. Only allowlisted identity/quota fields are
extracted: credentials and raw account-cache records are never copied into the
meter database. Imported requests retain Cockpit's cache-read/cache-write token
breakdown and frozen input/cache/output price snapshot, so historical totals do
not follow later meter price syncs. If Cockpit increments its pricing version
and rewrites those snapshots, the same imported rows are updated in place
without being counted twice. Compatibility is capability-based rather than tied
to a Cockpit Tools application or accounts-index version: newer version values
and additional database/index fields continue to import automatically. A
breaking removal, rename, or semantic change to a required field is isolated in
importer health instead of taking down the 8327 dashboard.
For migration, point request traffic directly at Cockpit Tools and keep this
meter running only to import Cockpit data and serve
http://127.0.0.1:8327/usage. Tune the importer with
COCKPIT_TOOLS_USAGE_POLL_SECONDS or
--cockpit-tools-poll-seconds SECONDS, or disable it with
--no-cockpit-tools-import. Sanitized importer state is available at
http://127.0.0.1:8327/healthz.
Double-count warning: if clients send the same requests through
8327with Cockpit Tools configured as its upstream while Cockpit import remains enabled, the meter sees both the proxied request and Cockpit's log row. Route clients directly to Cockpit, or add--no-cockpit-tools-importwhen using8327as that proxy path.
Keep the reusable migration helper in scripts/migrate_cliproxyapi_to_cockpit.py.
It is read-only by default and compares the local CLIProxyAPI auth directory
with Cockpit's credential cache, importing only new accounts or changed token
chains on an explicit apply:
python3 scripts/migrate_cliproxyapi_to_cockpit.py --json
python3 scripts/migrate_cliproxyapi_to_cockpit.py --apply --yes --jsonThe apply path uses Cockpit's supported cockpit-tools://import flow and a
one-shot loopback bundle; it does not rewrite either account store. Shared
workspace/account IDs are not treated as the same member when the emails or
user identities differ, and Cockpit-only accounts are preserved. A private
keyed migration history is kept at
~/.antigravity_cockpit/cliproxyapi_migration_history.json; it contains only
counts, status, and non-reversible fingerprints. Before an apply, encrypted
Cockpit account files receive a local rollback snapshot under
~/.antigravity_cockpit/backups/cliproxyapi_migration_*.
The helper reopens Cockpit's WAL-backed SQLite cache/log files without taking write locks. If an active journal has uncheckpointed bytes, it waits/fails closed rather than importing a stale snapshot; rerunning the same command is safe and idempotent.
The repository includes scripts/codex-cliproxyapi,
which follows the verified codex-quant profile (cliproxy, Responses API,
and the local CLIProxyAPI account pool) without changing the working directory.
Install it once on macOS so it is available from every folder:
install -m 755 scripts/codex-cliproxyapi /opt/homebrew/bin/codex-cliproxyapi
cd /path/to/any/project
codex-cliproxyapiThe command uses the current directory as Codex's workspace. It does not embed
QuantSystem or any other project path. CLIProxyAPI must be running on
127.0.0.1:8317; the existing ~/.codex/cliproxy.config.toml profile supplies
the endpoint and reads the bearer key at runtime.
The repository also includes scripts/codex-cockpit.
It layers the same cliproxy profile used above, then replaces only the
provider endpoint and auth command for the current Cockpit Tools API Service.
At each launch it reads codex_local_access.json, probes the authenticated
/v1/models endpoint with proxy bypass for loopback, and reads the current key
again whenever Codex refreshes credentials. Port changes, API-key rotation,
and additional Cockpit state fields therefore do not require editing a Codex
profile or this repository; no application-version comparison is used.
Install the launcher and its sibling helper together:
install -m 755 scripts/codex-cockpit scripts/cockpit-tools-api \
scripts/cockpit_tools_api.py /opt/homebrew/bin/
cd /path/to/any/project
codex-cockpitCockpit Tools' Codex API Service must be enabled and listening on its local
loopback endpoint. The launcher keeps the caller's working directory and
passes all arguments to Codex. COCKPIT_TOOLS_DATA_DIR selects a non-default
Cockpit data directory, COCKPIT_CODEX_PROFILE selects another base profile,
and COCKPIT_CODEX_SKIP_HEALTHCHECK=1 is available for troubleshooting only.
COCKPIT_TOOLS_API_URL can override the endpoint for a loopback-only test;
the helper refuses LAN/remote URLs so the Cockpit key is not sent elsewhere.
Treat a provider-reported 0% as telemetry, not a routing lock: Codex can
continue returning successful responses for some time after WHAM rounds or
reports the remaining percentage as zero. CLIProxyAPI should lock a credential
only after the execution endpoint itself returns a confirmed
usage_limit_reached 429.
For a large account pool, keep cooldown persistence enabled and let one logical request continue through every distinct currently eligible credential. Use weighted round-robin so a confirmed-exhaustion guard can exclude one credential with weight zero without disabling or moving its auth file:
max-retry-credentials: 0
save-cooldown-status: true
routing:
strategy: weighted-round-robinmax-retry-credentials: 0 means “no per-request credential-count cap”; it does
not disable cooldowns or cause an already cooled credential to be selected.
This avoids returning 429 merely because the only healthy credential happened
to be ninth in a pool whose old cap was eight. Equal default weights preserve
normal round-robin distribution.
Enable the sidecar guard when starting the direct-8317 queue collector:
CLIPROXY_QUOTA_ROUTING_GUARD=1 \
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/owner-only-management.key \
scripts/start_cliproxy_usage_meter.shCLIProxyAPI already persists a long cooldown when the 429 includes
resets_at/resets_in_seconds. If an exact usage_limit_reached 429 arrives
without that hint, the guard uses the matching fresh WHAM reset deadline and
sets only that credential's weight to zero. It restores the previous explicit
weight (or removes the temporary field) after the deadline. A provider-reported
0%, a generic 429, and a transient rate_limit_error never trigger this
guard. The owner-only guard state stores no token, email, auth filename, or
management key.
If Codex grants an early reset, clear only that credential's routing lock with the official management endpoint through the token-safe helper:
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/owner-only-management.key \
python3 scripts/cliproxyapi_reset_quota.py --list
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/owner-only-management.key \
python3 scripts/cliproxyapi_reset_quota.py codex-1
# After confirming the whole Codex pool reset early, preview and clear every
# confirmed official/guard lock, including credentials without a codex-N alias.
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/owner-only-management.key \
python3 scripts/cliproxyapi_reset_quota.py --all --dry-run
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/owner-only-management.key \
python3 scripts/cliproxyapi_reset_quota.py --allThe helper clears both CLIProxyAPI's official quota cooldown and any guard-owned
weight-zero lock, so it is also the early-reset path. The key file must be mode
0600. An explicitly requested --from-chrome
fallback can reuse the existing local management session without printing or
persisting its key. The helper refuses remote management origins, ambiguous
aliases, and credentials that are not currently in a confirmed quota cooldown.
Batch mode prevalidates the complete guard inventory before changing anything,
then calls POST /v0/management/reset-quota only for confirmed locks; it never
deletes .cds files directly.
The usage dashboard labels these states separately: 上游报告 0%,
上游 0% · 实测可用 after the latest execution still returned 200, and
已确认耗尽 · 冷却中 only after a quota-classified execution 429.
For a direct-8317 queue collector, use an external owner-only key file (never put a management credential in this checkout):
chmod 600 /path/to/cliproxy-management.key
CLIPROXY_MANAGEMENT_KEY_FILE=/path/to/cliproxy-management.key \
PORT=8327 UPSTREAM=http://127.0.0.1:8317 \
scripts/start_cliproxy_usage_meter.shIf Chrome already has the local CLIProxyAPI management page open,
scripts/start_cliproxy_usage_meter_from_chrome.py can pass its key in memory
without writing or printing it.
A Team workspace's chatgpt_account_id identifies the workspace, not a unique
member. The meter reads the email only from structured auth JSON or JWT claims;
text embedded in a timestamped .cpa...json filename is never treated as an
email. At runtime it derives a keyed identity from workspace + email, using a
random owner-only local key. Tagged mailbox names are preserved. A structured
provider subscription ID or JWT principal is used only when email is absent.
This keeps Team members separate while treating token/file rotation for the
same mailbox as one subscription.
If an old refresh-token file remains after rotation and returns 401, the quota poller tries the other structured record for that same mailbox before counting the subscription as unavailable; duplicate files therefore do not create a second card or a second quota window.
Email and optional codex-N aliases exist only in memory for the loopback
dashboard. SQLite stores the keyed subscription ID plus token/model/status/cost
statistics; it does not store email, auth filenames, tokens or token digests,
workspace/user IDs or tails, request/session/thread/turn IDs, project names,
endpoints, error bodies, or local source paths. Quota polling uses management
name/id only for the in-memory match and auth_index only as the opaque
selector for that call. Free-form upstream error types are discarded, and
model labels must match a narrow model-ID grammar before they are stored.
The owner-only identity key defaults to
~/.config/cliproxy-usage/identity.key. Back it up together with the SQLite
database and keep both private. Deleting or rotating that key changes every
derived subscription ID, so existing per-account history can no longer be
safely reattached; the meter will preserve uncertain history only anonymously.
The first complete auth inventory establishes a baseline. A missing
subscription is hidden immediately; after three complete misses spanning at
least ten minutes, its call rows are folded into identity-free daily token
statistics and its quota/plan/renewal/account details are deleted. A suddenly
empty inventory, or a drop to half the previous account count or less, requires
five confirmations spanning at least thirty minutes. That stricter policy stays
attached to each missing subscription until it authoritatively reappears.
Malformed, partial, paginated or failed inventories never advance deletion,
and disabled auth files still count as present. A keyed tombstone blocks late
queue/import rows from recreating a retired account; only a later complete
inventory can reactivate it. Dynamic /usage and /healthz responses use
Cache-Control: no-store.
- It does not modify, restart or replace CLIProxyAPI, port
8317, Codex config, shell aliases or existing client base URLs. - It listens on loopback by default. Do not expose it publicly without adding an authentication boundary of your own.
- Authorization, API keys, OAuth tokens and management keys are never printed or persisted. The only durable account linkage is an owner-keyed subscription ID. The loopback dashboard may display each mapped subscription email from a structured local auth claim; email is kept in memory and is not written to the usage/quota database or logs.
- Management credentials must come from an owner-only (
0600) external file or environment variable. They are never committed here. - SQLite files, WAL files,
.envfiles, key files, caches and local paths are ignored. Tests use fake upstream servers and fixture credentials only. - Unknown pricing remains
NULL; the project does not invent a subscription price or balance.
PYTHON_BIN="${QLAB_PYTHON_BIN:-python3}"
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --summary today
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --summary all --json
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --by-account 7d
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --by-model 7d
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --quota-summary 30d
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --list-prices
"$PYTHON_BIN" scripts/cliproxy_usage_meter.py --sync-official-pricesOfficial-price sync accepts only the documented OpenAI pricing hosts and
atomically keeps the previous table when fetch/parsing fails. Manual prices can
be set with --set-price MODEL_PATTERN INPUT_PER_M OUTPUT_PER_M plus
--cached-input-price and --price-source-note.
python3 -m unittest discover -s tests -p 'test_*.py' -vThe suite covers Responses and Chat Completions usage normalization, streaming
byte transparency, error redaction, account mapping, usage-queue polling,
quota/reset logic, official-price parsing, dashboard rendering and all CLI
queries. It never contacts a real 8317 service.
Detailed design and the original requirements are in
docs/cliproxy_usage_meter.md and
docs/cliproxy_usage_meter_requirements.md.
The runtime SQLite database lives under datas/ and is ignored by Git,
including WAL files. Raw authorization headers, OAuth tokens, refresh tokens,
API keys, and management keys are never persisted. The public repository
contains source, tests, documentation, and a screenshot with only account
identities masked—never local usage history or machine-specific paths.
MIT. See LICENSE.
