Skip to content

Coverage ​

TokenOps instruments AI usage on three planes. Which ones a given client supports is the whole integration story — there is no single "install," just these three surfaces, in order of decreasing passivity.

PlaneWhat it needsWhat you get
Passive readTokenOps reads logs/DB the client already writesPer-turn token attribution, zero wiring
MCP (tokenops serve)The client is an MCP hostThe agent calls TokenOps (budget, headroom, coach, status)
Proxy (base-URL override)The client lets you set its API base URLGround-truth token/cost accounting + live optimization

The atomic unit is a turn (one request→response), stamped agent_id = <client>:<project> and workflow_id = <client>:<project>:<session>, so everything rolls up turn → session → project.

Clients ​

ClientPassive readMCPProxyNotes
Claude Code✅ ~/.claude/projects✅✅ ANTHROPIC_BASE_URLreference integration; also the read-guard hook
Codex CLI✅ ~/.codex/sessions✅✅ OPENAI_BASE_URLreader surfaces OpenAI's official rate-limit %
opencode✅ SQLite store✅✅ per-provider baseURLreader is multi-provider
Cursor✅ (via the stop hook)✅❌ (agent traffic goes through Cursor's backend)the usage cookie gives plan consumption; the hook gives per-turn tokens
Gemini CLI✅ ~/.gemini/tmp/*/chats✅✅ base-URL overrideeach model turn's tokens from its chat recordings (logs.json is prompts only and is not read)
Desktop apps❌✅ (if MCP host)❌ (no base-URL override)MCP tools only; Claude usage meter for Max %
GitHub-hosted (Copilot agent)⚠️ quota only❌❌the Copilot quota endpoint gives bucket %, nothing per-turn
Jules / hosted❌❌❌out of reach — see Boundaries

What reaches which client ​

The plane table above is about where the data comes from. This one is about what you actually get, which is the question you have when choosing a client — and the honest answer is that parity does not exist.

Published rather than papered over. A matrix with gaps in it is worth more than a promise of everything: you can plan around a gap, and the gaps here are consequences of where a client keeps its state, not of what we got round to.

CapabilityNeedsClaude CodeCodex CLIopencodeCursorGemini CLIDesktopGitHub-hosted
Spend + token accountinga local token log✅✅✅✅✅❌⚠️
Ground truth + routing enforcementa base-URL override✅✅✅❌✅❌❌
Routing advice (tokenops_routing (action=advise))an MCP host✅✅✅✅✅✅❌
Prompt + reply coachingprompt text on disk✅✅✅❌❌❌❌
Agent DX metrics (dx)a transcript reader✅✅✅✅❌❌❌
Work storytelling (story)a reader and prompt text✅✅✅✅❌❌❌
Proactive coaching (nudges)an end-of-turn hook✅✅✅✅❌❌❌
Routing advice per turn (route-guard)a prompt-time hook✅✅✅✅❌❌❌
read-guard (intervene)a blockable file-read hook✅🚫✅🚫❌❌❌
tokenops fmt compressionthe agent runs shell commands✅✅✅✅✅❌❌
MCP tools (ask anything)an MCP host✅✅✅✅✅✅❌

⬜ means possible and not built yet. 🚫 means the client cannot support it — not a backlog item. ⚠️ means partial, and each one means something specific:

  • GitHub-hosted spend is quota, not tokens. The Copilot endpoint reports how much of the plan is gone and never a per-turn count. Useful for headroom; useless for "which project burns the most".

    Cursor used to be in this bucket and no longer is. Its usage endpoint still reports only a percentage, but its stop hook carries the turn's input, output, cache_read and cache_write — so installing the coaching hook (tokenops hooks install --coach --client cursor) is also what gives Cursor per-turn spend. Without the hook it falls back to quota only.

Coaching is pull-only on Desktop and GitHub clients ​

Claude Desktop, Codex Desktop and the GitHub clients have no local token log and no base-URL override. There are no transcripts to read, which means no storytelling, no proactive nudges, and no routing enforcement — nothing TokenOps does to a session is reachable, because nothing of the session is reachable.

What is left is the MCP surface, and on those clients it is the whole product. So it is the one that has to be genuinely good there: every question — spend, headroom, burn rate, forecast, the work account, the DX metrics, which model this turn should run on — is a call to one of the sixteen tools the server lists. That is a real answer, just a smaller one than a client whose session we can sit inside.

Parity is not available on those clients and will not become available; saying so is the differentiator against a matrix that promises everything.

Where plan windows come from ​

A plan's windows (Claude's session and week, Codex's week) are read from several sources, tried in order from least to most invasive (ADR 0011). Each reading keeps its source and time. Where two sources report the same window, the newer one wins.

ProviderSources, in orderCovers
Claude1. Claude Code's status line (rate_limits), installed by tokenops initevery open Claude Code session, no credential
2. the claude.ai usage meter (tokenops vendor-usage setup claude-subscription)between sessions; re-reads the browser's session when claude.ai expires it
3. Claude Code's own sign-in, opt-in (tokenops vendor-usage setup claude-code)between sessions without a browser session; the token is read from Claude Code's credentials file or, on macOS, the Keychain (macOS asks), held in memory, sent only to api.anthropic.com and never refreshed
Codex1. codex app-server's account/rateLimits/read, every 15 minutes: Codex signs its own request, TokenOps reads no credentialbetween turns, on by default wherever codex is installed
2. the rate_limits block in each rolloutevery Codex turn
Gemini, Copilot, Cursor, account providerstheir quota endpoint or account keywhile the key works

A window whose source has stopped is never shown as current. A source that polls on a schedule, like the claude.ai meter, has stopped when its newest reading is more than 30 minutes old. A source that reads as you work, like the status line or Codex's rollouts, is idle when its reading is old, not stopped.

When every window of a plan is stale:

  • the cards and the menu bar show each window's age in place of its pace;
  • the coach raises a finding, "Claude's plan reading is 20h old", with how to get it back;
  • the coach's hook says so once every six hours instead of quoting dollars.

Providers (proxy plane) ​

Every provider below is routable through the proxy. Bind one with tokenops provider set <name> (omit the URL to use the built-in preset); tokenops provider list shows them all.

ProviderPreset base URL
openaihttps://api.openai.com
anthropichttps://api.anthropic.com
geminihttps://generativelanguage.googleapis.com
mistralhttps://api.mistral.ai
groqhttps://api.groq.com/openai
deepseekhttps://api.deepseek.com
xaihttps://api.x.ai
perplexityhttps://api.perplexity.ai
fireworkshttps://api.fireworks.ai/inference
cerebrashttps://api.cerebras.ai
togetherhttps://api.together.xyz
openrouterhttps://openrouter.ai/api
coherehttps://api.cohere.com
ollamahttp://localhost:11434 (local, no key)
lmstudiohttp://localhost:1234 (local, no key)
litellmhttp://localhost:4000 (self-hosted gateway)
vercelhttps://ai-gateway.vercel.sh

OpenRouter is the universal fallback for ground truth. Any client with no local reader but a base-URL override can route through OpenRouter-via-TokenOps and every turn becomes visible.

Cohere is not OpenAI-wire-format — it has a dedicated normalizer for its v2 (/v2/chat) and v1 (/v1/chat) shapes — but auth is a Bearer header so the passthrough proxy handles it.

Pricing ships for the single-model-family providers (groq/deepseek/xai/perplexity/cerebras/cohere). Fireworks, Together, and OpenRouter multiplex arbitrary third-party models under namespaced names, so a static rate card can't price them accurately — token counts are still metered; attach $ cost via a pricing override (below).

Pricing overrides & drift ​

The built-in rate card is public list prices captured at a point in time — they drift, and negotiated/enterprise rates differ. Layer your own rates over the defaults without touching the binary:

yaml
# ~/.config/tokenops/config.yaml
pricing:
  path: ~/.config/tokenops/pricing-override.yaml   # or env TOKENOPS_PRICING_PATH

The override file uses the same schema as the built-in catalog (USD per million tokens; * = prefix match, longest wins) and is merged over the defaults — a matching provider+model row replaces that rate, a new row adds one. This is also how you price the multiplexers: copy the model string TokenOps records (RequestModel) verbatim and give it the underlying model's rate. A ready-to-copy example lives at docs/examples/pricing-override.yaml.

How an agent bootstraps setup ​

The MCP surface is self-describing. An agent calls tokenops_status and gets back signal_quality.level plus blockers[] and next_actions[] — the exact commands to upgrade — and tokenops_status (view=data_sources) reports which planes are live. So an agent on a fresh install can tell you what to run to give it better data.

Boundaries ​

These are honest limits of a local-first, no-telemetry tool — not gaps to fill:

  • AWS Bedrock is not proxy-metered. It requires SigV4 request signing; the proxy is pure passthrough with no per-provider auth hook.
  • Jules and fully-hosted agents are out of reach. No local logs, no MCP host you control, no proxy you can insert. TokenOps can only instrument agents that run where you can read their logs, mount an MCP server, or sit in front of their API.
  • Desktop and GitHub clients get the MCP surface and nothing else. No local token log and no base-URL override means no transcripts, so coaching there is pull-only. See the capability matrix above.
  • read-guard is permanently unavailable on Codex and Cursor, for two different reasons and neither of them ours. Codex has no file-read tool at all: across 40 real rollouts every tool call was exec_command, MCP, wait, write_stdin or request_user_input, so reading a file is a shell command and there is no read to intervene in. Cursor has beforeReadFile, but only beforeShellExecution and beforeMCPExecution honour a permission decision — its own docs call beforeReadFile a tripwire, not a lock. tokenops hooks install --read-guard --client codex refuses and says so rather than writing a hook that never fires.
  • Cursor's reader is the least exercised. It reads the same structure as the others and carries prompt text, but no Cursor store was available to verify it against real history the way Claude Code, Codex and opencode were. Treat its numbers as unconfirmed until you have checked them against your own sessions.

Apache 2.0 licensed.