cli2api
Endpoints

Usage

Per-call records cli2api writes for every upstream request — answer "what did I spend my MuleRun credits on" without depending on MuleRun's dashboard.

Why

MuleRun's own dashboard exposes a current balance (mulerun user balance) but no per-call API for consumption history — you can see today's total but not which model burned it. cli2api sits between every client and MuleRun, so it can record the same data and surface it.

Since v0.3.0, every request that hits a /v1/... endpoint is logged to a per-request usage record. The records live in libsql (or in-memory if no DSN is set) and are exposed through GET /v1/usage.

What's captured

FieldSource
timestamprequest start
endpointURL path (e.g. /v1/chat/completions)
modelparsed from JSON response body (model field)
prompt_tokens, completion_tokensparsed from response usage.{prompt,completion}_tokens or usage.{input,output}_tokens
statusupstream HTTP status (200, 502, 413…)
duration_mshandler wall-clock
bytes_outresponse size to the client
request_idfor cross-referencing with journalctl -u cli2api
api_key_hashfirst 12 hex of SHA-256(inbound key) — for multi-tenant attribution; never plaintext

Streaming responses (SSE text/event-stream) skip body capture entirely so they aren't held in memory. The record still gets endpoint/status/bytes but model and tokens are 0 — that's the trade-off for not breaking streaming. Most chat clients are non-streaming so token coverage is high.

GET /v1/usage

Query params (all optional):

ParamDefaultNotes
from24h agounix seconds OR RFC3339
tonowunix seconds OR RFC3339
group_bymodelmodel / endpoint / status / day / hour
model(none)filter to one model
endpoint(none)filter to one endpoint path
# What did the last 24h cost?
curl http://localhost:51222/v1/usage \
  -H "Authorization: Bearer $CLI2API_KEY" | jq

# Hourly breakdown for gpt-5.5 specifically
curl "http://localhost:51222/v1/usage?model=openai/gpt-5.5&group_by=hour&from=$(date -d '-2 day' +%s)" \
  -H "Authorization: Bearer $CLI2API_KEY" | jq

# Error rate by endpoint
curl "http://localhost:51222/v1/usage?group_by=endpoint" \
  -H "Authorization: Bearer $CLI2API_KEY" | jq '.rows[] | {endpoint:.bucket, errors, calls}'

Sample response:

{
  "from":     "2026-06-28T03:00:00Z",
  "to":       "2026-06-29T03:00:00Z",
  "group_by": "model",
  "filter":   { "model": "", "endpoint": "" },
  "rows": [
    { "bucket": "openai/gpt-5.5",     "calls": 142, "errors": 1, "prompt_tokens": 18234, "completion_tokens": 6543, "bytes_out": 412390 },
    { "bucket": "deepseek-v4-flash",  "calls":  89, "errors": 0, "prompt_tokens":  7800, "completion_tokens": 4500, "bytes_out": 198044 }
  ],
  "totals":   { "calls": 231, "errors": 1, "prompt_tokens": 26034, "completion_tokens": 11043, "bytes_out": 610434 }
}

Configuration

VariableDefaultPurpose
CLI2API_USAGE_DSN(empty = memory)libsql DSN. Same shape as CLI2API_JOBSTORE_DSNfile:/abs/path/usage.db for local, libsql://...?authToken=... for Turso.
CLI2API_USAGE_RETENTION720h (30d)Records older than this are deleted by the reaper. 0s = never expire.
CLI2API_USAGE_BUFFER1024Async writer channel size. Higher = more memory pressure tolerated at peak.

The reaper sweeps on CLI2API_REAPER_INTERVAL (shared with the job store reaper). Set it independently if you want different cadences.

Recorded asynchronously

The recorder writes off the request hot path via a buffered channel; under extreme backpressure (e.g. libsql down + sustained burst) excess records drop and the count is logged on shutdown. Handlers never block on the recorder.

Approximation, not billing

Tokens here are what the upstream reported in its response — they match what MuleRun charged you for chat. For image/video/audio surfaces there's no token field; the record carries endpoint + status + bytes only. Treat this as observability, not a substitute for MuleRun's account ledger.

On this page