Claude Code statusline behind LiteLLM and other gateways
How the Sprag statusline reads spend, budgets and cache TTL when Claude Code runs through LiteLLM or another API gateway, and why the cache countdown can look frozen.
🌐 Behind a gateway (Bedrock / Vertex)
A gateway reports the cache-creation total but never the 5m/1h split. That left the tool unable to tell "nothing cached yet" from "this provider does not say", and the fallback assumed an hour — for a window that is really five minutes on Bedrock, overstating it twelvefold.
Since v3.26.0 the gateway is detected from the model ids in the transcript, which fixes:
- The countdown falls back to 5 minutes, labelled
5m?. Three grades of certainty get three labels: measured (5m), inferred (5m?), unknown (?). - In a 5-minute bucket the countdown colour follows absolute time rather than a percentage. 30% of five minutes is 90 seconds, and green there promised comfort that was not there.
- The
⚠ 5m TTLwarning finally reaches these users — with different advice, since no subscription plan changes a gateway's TTL. Extra cost if 5m-onlyis only asked of sessions that have 1h writes to lose. Elsewhere the arithmetically honest+$0read as an endorsement of the bucket you are already stuck in.- Delegated runs dropped for an unpriceable model id show as
🔀 N unresolvedinstead of nothing, which used to be indistinguishable from never having delegated. - Environment variables set to a
foundation-modelARN now resolve. An opaqueapplication-inference-profileid still does not: guessing at it is how wrong prices enter the ledger.
Cost and routing savings on a gateway use the gateway's prices. A Claude session with a gateway ANTHROPIC_BASE_URL is billed by the gateway, so Actual cost, Sep spend and Routing saved are computed from its /model/info prices (refreshed with the model map, valid for seven days). A model the gateway does not price stays unpriced: the session is left out and counted (+N unpriced) instead of being filled in from the Anthropic list prices. Sessions that do not go through a gateway keep using the list prices below. A delegation to a pricier model is recorded as a loss (-$0.12), not clamped to zero.
If the detection is wrong, pin it with sprag mode ttl=5m (or ttl=1h). An explicit value outranks the measurement.
LiteLLM: your key budget stands in for the missing 5h/7d caps (v3.35.0)
Behind a LiteLLM proxy (Bedrock and friends), Claude Code's stdin never carries rate_limits, so the ✦ current / 📅 weekly gauges simply do not exist. LiteLLM does track per-key budgets, so the statusline draws a budget gauge in their place.
- Detection:
ANTHROPIC_BASE_URLpoints somewhere other than the official endpoint. The key comes fromANTHROPIC_AUTH_TOKEN/ANTHROPIC_API_KEY, or — when neither is set — from running theapiKeyHelperscript insettings.json. The helper only runs inside the 5-minute refresh child, never on the render path (v3.43.0). That call setsAPIKEY_HELPER_NONINTERACTIVE=1, so a helper that checks it fails fast instead of opening a login window when it has no cached token; the refresh runs every five minutes with nobody at the keyboard, and would otherwise stack login windows while you are away. - A 404 from
/key/infono longer aborts the refresh: custom-auth deployments (Okta JWT and friends) always 404 there, and the budget lives in/user/info. Only a failure of both endpoints counts as a failure (v3.43.0). - When the gauge falls back to the internal-user budget it is labeled
budget (user). LiteLLM enforces the team-membership limit, which can differ from the user limit by orders of magnitude (v3.43.0). - The proxy is asked via
GET /key/infoandGET /user/info— only the calling key's own data. Renders read a cache file; a detached background process refreshes it every 5 minutes (same shape as the update check), so the statusline never waits on the network. - Budget source priority follows real-world usage: the team-membership budget (
team_memberships[].spend+ its linked budget table row) first, then the key's ownmax_budget, then the internal-user budget. Verified against a Dockerized LiteLLM, including memberships whose budget diverges from the team max into a separate budget-table row. - Unlimited keys (no
max_budget) get no gauge. The💵monthly-spend segment still shows, since it comes from session logs. - Inspect with
sprag litellm-budget(gauge with used/remaining amounts),--json(raw cache), or--refresh(query now).
claude.ai Enterprise: usage-credit budget gauge (v3.93.0)
Enterprise users who sign in with claude.ai do not get rate_limits in the statusline input either. The statusline now reads extra_usage from GET https://api.anthropic.com/api/oauth/usage and draws 💳 budget ▰▱ 54% $271/$500 in the same place.
- On by default. Turn it off with
sprag mode no-oauth-budget(back on withsprag mode oauth-budget), or setSPRAG_OAUTH_BUDGET=0. - It stays out of the way for everyone else: a gateway,
ANTHROPIC_API_KEYorANTHROPIC_AUTH_TOKEN, or Bedrock, Vertex or Foundry skips it before any token is read. A Pro or Max login (subscriptionTypein the stored credentials) skips the request and rests for 24 hours, since those plans getrate_limits. - Source priority: ①
rate_limitsfrom the statusline input, ② the LiteLLM budget, ③/api/oauth/usage. When an earlier source supplies a window, this one does not run at all. - The endpoint is not in the official documentation. If the response has a different shape (no
extra_usage, extra usage disabled, a non-USD currency), nothing is drawn. - The OAuth token is read from
CLAUDE_CODE_OAUTH_TOKEN, the macOS keychain itemClaude Code-credentials, or.credentials.jsonin the Claude config directory, only inside the background refresh process. It is held in memory and never written to the cache, logs or arguments. Refreshing an expired token stays with Claude Code. - The response carries no reset time, so the gauge shows none.
- If the token cannot be read (a denied or timed-out keychain prompt included), the refresh rests for 6 hours so permission dialogs do not pile up every five minutes.
- Inspect with
sprag oauth-budget,--json(raw cache), or--refresh(query now, works even when turned off).
One related non-bug: if your session model is already sonnet, a sonnet-delegation (T1) rule can never save anything, because there is no price gap to capture. That is correct, but route-scan rules displayed it identically to "no delegations yet", so it now says outright that the rule does not apply at the current default model.
LiteLLM: model aliases come from the gateway (sprag profile-map --refresh)
Behind a LiteLLM gateway, sprag profile-map --refresh reads model aliases straight from the gateway's own GET /model/info, so route-scan no longer needs to wait on learned votes or a hand-edited profile-map.json. The refresh uses the same 5-minute check and 24-hour cache as the LiteLLM budget gauge. It never stores your auth token: each refresh reads it from apiKeyHelper at call time and lets it expire on the helper's own schedule. The three-step resolution order and the manual profile-map.json format are in route-scan.
Pricing (Jul 2026)
Per million tokens (USD), as used by the cost estimator:
| Tier | Models | Input | 5m Write | 1h Write | Read | Output |
|---|---|---|---|---|---|---|
claude-fable-5-1 | Fable 5.1 / Mythos 5.1 | $10 | $12.50 | $20 | $0.25 | $50 |
claude-fable-5 | Fable 5 / Mythos 5 | $10 | $12.50 | $20 | $1 | $50 |
claude-opus-5-5 | Opus 5.5 | $4 | $5 | $8 | $0.20 | $20 |
claude-opus-new | Opus 4.5 / 4.6 / 4.7 / 4.8 / 5 | $5 | $6.25 | $10 | $0.50 | $25 |
claude-opus-legacy | Opus 4 / 4.1 / 3 | $15 | $18.75 | $30 | $1.50 | $75 |
claude-sonnet-5-5 | Sonnet 5.5 | $2 | $2.50 | $4 | $0.10 | $10 |
claude-sonnet-5 | Sonnet 5 | $2 | $2.50 | $4 | $0.20 | $10 |
claude-sonnet | Sonnet 3.7 / 4 / 4.5 / 4.6 | $3 | $3.75 | $6 | $0.30 | $15 |
claude-haiku-5-5 | Haiku 5.5 (prompt ≤ 100k tokens) | $0.10 | $0.125 | $0.20 | $0.01 | $0.50 |
claude-haiku-5-5 (long) | Haiku 5.5 (prompt > 100k tokens) | $0.50 | $0.625 | $1 | $0.05 | $2.50 |
claude-haiku-4-5 | Haiku 4.5 | $1 | $1.25 | $2 | $0.10 | $5 |
Source: Anthropic pricing docs. Sonnet 5 stays at $2/$10; the planned rise to $3/$15 was cancelled. Haiku 5.5 prices each request by its own prompt length (input + cache writes + cache reads), so a request over 100k tokens pays the long rates. Versions ≤ 2.16.x priced Fable 5 at the Sonnet tier (~3× under-estimate) — upgrade to 2.17.0+.
Cache TTL by plan
| Plan | TTL | Controlled by |
|---|---|---|
| Max ($100–200/mo) | 1h auto | tengu_prompt_cache_1h_config flag |
| Pro ($20/mo) | 5m fixed | not configurable |
| API key | 5m default (1h via beta header) | cache_control.ttl |
FAQ
Is this the same tool as claude-token-saver? Yes. Sprag is the new name and the npm package is sprag-cli. The old claude-token-saver package stopped updating at 3.55.0, so move to sprag-cli to keep receiving releases.
Does it call an LLM or need an API key? No. Everything is post-hoc analysis of the session logs Claude Code already writes on your machine. No extra model calls, no key, no telemetry leaves your computer.
Will it change how I prompt? No. It installs hooks and a statusline once, then works from what you already do. Rules, delegation, and warnings show up inside your normal sessions.
Where do the savings numbers come from? Each delegated run writes a ledger entry with the actual price difference. The tier criteria behind the routing are benchmarked on public data; the method is in docs/BENCHMARK.md.
How it works · Environment
Claude Code logs every API call to ~/.claude/projects/<dir>/<session>.jsonl. This tool dedupes streaming chunks by requestId and aggregates cache_read_input_tokens / cache_creation.ephemeral_5m/1h_input_tokens by day and session.
Node.js ≥ 20 · macOS / Linux / Windows / WSL · zero dependencies.
Known quirks · Migration · Background
IntelliJ Claude Code plugin — the statusline widget fuses frames at the character level when emoji are present (59:548 artifacts). v2.8.5+ detects TERMINAL_EMULATOR=JetBrains-JediTerm and falls back to text mode automatically.
If the countdown looks frozen: ticking while idle requires Claude Code to re-run the statusline command on a timer, controlled by statusLine.refreshInterval (seconds, Claude Code v2.1.97+) in ~/.claude/settings.json. Without it the line only redraws when the conversation updates. If behavior differs per terminal, check three things: ① that machine's Claude Code is ≥ 2.1.97; ② no project .claude/settings.json / settings.local.json overrides statusLine without a refreshInterval; ③ the statusline wrapper actually finds sprag on PATH instead of falling back to a multi-second npx run on every render (typical when nvm is not loaded in non-login shells). Re-running sprag install restores refreshInterval=5.
Migration from claude-cache-monitor:
npm uninstall -g claude-cache-monitor && npm i -g sprag-cli
Also update statusLine.command in ~/.claude/settings.json to sprag ….
Background: GitHub Issue #46829 (cache TTL regression) · HN discussion
More docs
- Codex: hooks, ratchet rules, model delegation, and a companion panel
- Claude Code statusline: cache hit rate, cache TTL countdown, 5-hour and 7-day rate limits
- Why realtime model routing can cost more in Claude Code
- Delegate repeat Claude Code work to Haiku and Sonnet subagents with route-scan
- Claude Code harness: ratchet rules, evidence-gated completion, Plan-Execute-Verify
- Convert pptx, xlsx, pdf, docx and fig to Markdown before Claude Code reads them
- Install Sprag for Claude Code and Codex
- Sprag command reference
- What Sprag sends: open source, Pro and Enterprise
- Korean writing style gate for Claude Code output
- How to reduce Claude Code token usage
- Claude Code 5-hour and weekly usage limits, explained
- How to reduce Codex CLI token usage
- Sprag vs ccusage, claude-usage, and statusline tools