Skip to content

feat(statusline): add current-month total cost placeholder#55

Open
Brusdeylins wants to merge 3 commits into
rse:masterfrom
Brusdeylins:feat/statusline-monthly-cost
Open

feat(statusline): add current-month total cost placeholder#55
Brusdeylins wants to merge 3 commits into
rse:masterfrom
Brusdeylins:feat/statusline-monthly-cost

Conversation

@Brusdeylins

Copy link
Copy Markdown
Collaborator

Summary

Adds a new %Y statusline placeholder showing the cumulative cost (USD) across all Claude Code sessions of the current calendar month — complementing %X, which only reflects the current session (cost.total_cost_usd).

Example: ∑ month: $1223.47

Data source & pricing

Claude Code's local session transcripts (~/.claude/projects/**/*.jsonl, honoring CLAUDE_CONFIG_DIR) — the same source ccusage uses. These logs carry no precomputed costUSD field, so cost is computed from each entry's message.usage token counts × per-model price:

  • input + output tokens, plus cache tokens: read 0.1×, 5-minute write 1.25×, 1-hour write 2× of the model's input price.
  • Prices cross-checked against the LiteLLM model_prices_and_context_window.json table (Opus 4.x $5/$25, Sonnet 4.6 $3/$15, Haiku 4.5 $1/$5 per 1M).
  • Only entries of the current month (local time) are summed; duplicate messages (same id+requestId across session logs, e.g. after resume/fork) are de-duplicated.

Note on accuracy vs. ccusage: ccusage prices all cache_creation_input_tokens at the flat 1.25× rate (it doesn't read LiteLLM's cache_creation_input_token_cost_above_1hr). This implementation splits the cache_creation.ephemeral_5m/ephemeral_1h breakdown and charges 1-hour cache writes at 2× — matching Anthropic's real billing and LiteLLM's above_1hr value, so it is more accurate than ccusage for Claude Code traffic (which uses 1-hour caching heavily). No current Claude model carries a >200k long-context premium, so per-request token tiering is intentionally omitted.

Performance (non-blocking)

The statusline renders very frequently, and a full transcript scan takes ~1.5s. So the scan never runs in the render path:

  • The result is cached in the temp directory (MonthCostCache: month/cost/timestamp).
  • A render serves the last cached value immediately; when the cache is missing, stale, or from a previous month, it spawns a detached background process (ase statusline --refresh-month-cost) that recomputes and rewrites the cache. The next render picks up the fresh value.
  • TTL is configurable via --month-cost-ttl <seconds> (default 300).
  • Whole files not modified this month are stat-skipped to keep the scan bounded.

Robustness

Missing/empty logs, unreadable files, malformed JSON lines, and unknown models all degrade gracefully — the placeholder simply renders nothing, never an error.

Verification

  • npm --prefix tool start buildlint + tsc green.
  • Unit-level, synthetic JSONL in a temp CLAUDE_CONFIG_DIR: hand-computed total $0.17725 matched computeMonthCost() to 1e-9, exercising the 5m/1h cache split, month filtering (previous-month entry excluded), dated model-id suffix resolution, unknown/<synthetic> model skipped, no-usage line skipped, and cross-file de-duplication.
  • End-to-end CLI: --refresh-month-cost writes the cache; %Y then renders ∑ month: $0.18; %X %Y render side by side; empty logs suppress %Y.
  • Real data: rendered $1223.47 for the current month against the local transcript archive; confirmed the render reads the cache (fast) rather than scanning.

Docs

%Y, its data source, caching behavior, and --month-cost-ttl are documented in docs/usage-tool.md; the statusline argument hint lists %Y.

🤖 Generated with Claude Code

Comment thread docs/usage-tool.md
directory and recomputed at most once per *--month-cost-ttl* window
by a detached background process, so a render never blocks on the
transcript scan; missing or empty logs simply suppress the
placeholder. When run inside a *tmux* pane, the resolved task id is also

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here need a blank line, I think. It is very confusing that the tmux stuff is at the end of the %Y stuff.

"claude-opus-4-6": { input: 5.00, output: 25.00 },
"claude-sonnet-4-6": { input: 3.00, output: 15.00 },
"claude-haiku-4-5": { input: 1.00, output: 5.00 }
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This both can become a maintenance issue (when new models occur or prices change) and does not support any models in Github Copilot.

catch (_e) {
user = process.env.USER ?? "default"
}
return path.join(os.tmpdir(), `ase-statusline-month-cost-${user}.json`)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really under the OS temp directory like /tmp or $TMPDIR? I would have expected ~/.ase/xxx or something like this but not the temp directory.

emit(`${prefix("$", "cost")}${c.bold(formatCostUsd(sessCost))}`)
},
Y: () => {
const monthCost = monthCostForRender(new Date(), opts.monthCostTtl)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least here we would have to check the "agent harness": "claude" vs. "copilot". For copilot this at least should expand to the empty string or perhaps even "N.A." or something like this.

@Brusdeylins Brusdeylins force-pushed the feat/statusline-monthly-cost branch 3 times, most recently from 94364e5 to fc0f453 Compare June 11, 2026 01:36
@Brusdeylins Brusdeylins force-pushed the feat/statusline-monthly-cost branch from b55504d to 5d92e26 Compare June 22, 2026 11:09
Brusdeylins and others added 3 commits July 3, 2026 21:00
Add a new %Y statusline placeholder that shows the cumulative cost (USD)
across ALL Claude Code sessions of the current calendar month, complementing
%X which only reflects the current session.

- new tool/src/ase-statusline-cost.ts: scans ~/.claude/projects/**/*.jsonl
  (honoring CLAUDE_CONFIG_DIR), sums each entry's message.usage tokens times
  the per-model price, filtered to the current month (local time). Pricing is
  cross-checked against the LiteLLM price table (the same source ccusage uses):
  input/output plus cache-read (0.1x), 5-minute cache-write (1.25x) and
  1-hour cache-write (2x) derived from each model's input price. Duplicate
  messages (same id+requestId across session logs) are de-duplicated.
- performance: the transcript scan never runs in the render path. The result
  is cached in the temp directory and, when stale/missing, refreshed by a
  detached background process (ase statusline --refresh-month-cost); the render
  serves the last cached value immediately. TTL is configurable via
  --month-cost-ttl (default 300s).
- robustness: missing/empty logs or unknown models simply suppress the
  placeholder; no errors.
- docs: document %Y, its data source, caching, and --month-cost-ttl in
  docs/usage-tool.md and extend the statusline argument hint.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add Claude Fable 5 (model id `claude-fable-5`, $10/MTok input, $50/MTok
output) to the `%Y` cumulative-month-cost price table. Cache-token costs
derive from the standard prompt-caching multipliers as for the other
models (read 0.1x, 5-min write 1.25x, 1-hour write 2x), which match the
published Fable 5 cache rates. Prices verified against the official
Anthropic pricing and models-overview documentation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onth cost

Claude Code rewrites a message's log entry with progressively growing
usage counts while the response streams. The previous first-wins
de-duplication kept whichever snapshot was encountered first, often a
partial one, systematically undercounting the month total by ~0.5%.
Now the most expensive snapshot per message-id/request-id key is kept,
which reflects the final billed state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Brusdeylins Brusdeylins force-pushed the feat/statusline-monthly-cost branch from 5d92e26 to cd440d6 Compare July 3, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants