Skip to content

feat(statusline): add current-month total cost placeholder across all agent tools - #55

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

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

Conversation

@Brusdeylins

@Brusdeylins Brusdeylins commented Jun 6, 2026

Copy link
Copy Markdown
Collaborator

Summary

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

Example: ∑ month: $791.20

Data sources

The figure is computed locally, without any network access, from the session logs of every agent tool ASE supports:

Tool Log location Override
Anthropic Claude Code CLI ~/.claude/projects/**/*.jsonl CLAUDE_CONFIG_DIR
OpenAI Codex CLI ~/.codex/{sessions,archived_sessions}/**/rollout-*.jsonl CODEX_HOME
GitHub Copilot CLI ~/.copilot/session-state/**/*.jsonl COPILOT_CONFIG_DIR

Each tool records usage differently, so each is normalized before pricing:

  • Claude Code logs one message.usage per API call, with cache writes split into ephemeral_5m / ephemeral_1h.
  • Codex emits a dedicated event_msg / token_count record whose last_token_usage is the delta of that call; its input_tokens includes the cached tokens, so the uncached remainder is recovered, and reasoning_output_tokens are folded into the output.
  • Copilot aggregates per model over the whole session in modelMetrics; its inputTokens is cache-inclusive as well, and because the aggregate is cumulative, all records of one session and model share a key so only the final one is billed.

Pricing

Prices come from a snapshot of the LiteLLM price database (model_prices_and_context_window.json — the same source ccusage and codeburn use), reduced to the chat/responses models of the vendors reachable through the supported agent tools (Anthropic, OpenAI, Gemini, xAI, DeepSeek, Mistral) — 273 models.

  • The snapshot is generated by tool/etc/litellm-prices.mjs into the checked-in source module tool/src/ase-statusline-prices.ts via the new build target npm start prices-update. Only that target needs network; build and rendering stay fully offline.
  • Per model, the real per-token rates for input, output, cache-read, 5-minute cache-write and 1-hour cache-write are used as published — no longer derived from the input price by fixed 0.1× / 1.25× / 2× factors, which silently misprices any model that deviates.
  • Model ids resolve through a widening chain: bare id → dot-normalized id (Copilot renders claude-sonnet-4.5) → id without vendor prefix (anthropic/claude-opus-5) → longest matching id prefix, which maps dated snapshots such as claude-opus-5-20260401 onto their base entry.
  • A model absent from the snapshot contributes nothing, as it cannot be priced reliably.

Why this replaces the previous hand-maintained table: that table silently returned $0 for any model it did not list. claude-opus-5 was missing and claude-sonnet-5 was carried at $3/$15 instead of $2/$10 — on the author's archive the month figure came out as $355.03 instead of ~$790, i.e. wrong by more than half, with no error and no warning.

Month boundary

Entries are bucketed on UTC months, matching the day on which the model vendors bill and reset their usage windows. (A local-time boundary attributes late-evening calls to the wrong month; for the author's archive that is a ~$125 difference.)

De-duplication

A call logged more than once — while its response streams, or after a session was resumed or forked — is billed once, keeping the most expensive snapshot, since usage counts grow monotonically while a response streams.

Performance (non-blocking)

The statusline renders very frequently and a full scan takes ~1.5–3 s, so the scan never runs in the render path:

  • The result is cached in the temp directory (MonthCostCache: scheme version / 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.
  • TTL is configurable via --month-cost-ttl <seconds> (default 300).
  • Files not modified within the month are stat-skipped, keeping the scan off the archive.
  • The cache carries a scheme version, so a value written by an older ASE is discarded rather than rendered as if it were current.

Robustness

Missing or empty logs, unreadable files, truncated trailing JSON lines of a live session, and unknown models all degrade gracefully — the placeholder simply renders nothing, never an error.

Verification

  • npm --prefix tool start buildlint + tsc green (against ASE 0.9.52).
  • Fixture test, Codex + Copilot: synthetic logs in a temp CODEX_HOME / COPILOT_CONFIG_DIR (with an empty CLAUDE_CONFIG_DIR); hand-computed total $0.045500 (Codex $0.006350 + Copilot $0.039150) matched computeMonthCost() exactly. The fixture exercises the cached-input subtraction, reasoning tokens folded into output, the dot-normalized model id, and the cache-write rate. A deliberately planted previous-month record of 10M tokens was correctly excluded — without the month filter the result would be off by orders of magnitude.
  • Copilot double-counting: appending a second cumulative session.shutdown record leaves the total unchanged at $0.045500.
  • Real data: the author's archive yields $787–791 for the current month (previously $355.03), scan ~3 s.
  • Cross-check against codeburn: at an identical (local-time) month definition this implementation reports $913.86 over 2763 calls versus codeburn's $844.84 over 2573 calls. The ~190 additional calls map 1:1 onto distinct requestIds, i.e. they are real API requests codeburn drops; codeburn's own figure also moves between $822.54 (warm cache) and $834.63 (cold cache).
  • End-to-end CLI: --refresh-month-cost writes the cache, %X %Y then renders cost: $1.23 month: $791.20; empty logs suppress %Y.

Docs

%Y, its data sources per agent tool, the UTC boundary, the price snapshot, the caching behavior, and --month-cost-ttl are documented in docs/usage-tool.md; the new prices-update build target is listed in AGENTS.md.

Caveat

The Codex and Copilot parsers are Vibe-Coded: their log layouts and field names were taken from the provider implementations of codeburn (MIT) and are verified only against the fixtures described above — neither tool has produced logs on the author's machine, so they have not been validated against real-world session data. The Claude Code path is verified against a real archive.

🤖 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.

Comment thread tool/src/ase-statusline-cost.ts Outdated
"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.

Comment thread tool/src/ase-statusline-cost.ts Outdated
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
Brusdeylins force-pushed the feat/statusline-monthly-cost branch from 5d92e26 to cd440d6 Compare July 3, 2026 19:11
@Brusdeylins
Brusdeylins force-pushed the feat/statusline-monthly-cost branch 2 times, most recently from 5aaf7fa to 9eae350 Compare July 16, 2026 00:06
@Brusdeylins
Brusdeylins force-pushed the feat/statusline-monthly-cost branch from 9eae350 to 8c2d82f Compare July 24, 2026 13:30
Brusdeylins and others added 5 commits August 2, 2026 02:39
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>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… all agent tools

The %Y current-month cost so far relied on a hand-maintained price table
covering seven Claude models and scanned Anthropic Claude Code logs only.
Both limits produced silently wrong figures: an unlisted model contributed
nothing at all (claude-opus-5 was missing, understating the month by more
than half), a stale entry mispriced its model (claude-sonnet-5 was carried
at 3/15 instead of 2/10 USD per million tokens), and users of the other
agent tools got no figure at all.

Prices now come from a snapshot of the LiteLLM price database, reduced to
the chat models of the vendors reachable through the supported agent tools
and generated by "npm start prices-update" into a checked-in source module,
so build and rendering stay offline. Model ids resolve through the bare id,
a dot-normalized id, the id without its vendor prefix, and finally the
longest matching id prefix, which maps dated snapshots onto their base
entry.

The scan covers Anthropic Claude Code, OpenAI Codex CLI, and GitHub Copilot
CLI, each with its own log layout, usage field names, and identity for
de-duplication; Codex reports its cached tokens inside the input total and
Copilot aggregates cumulatively per session, both of which are normalized
before pricing. The month boundary moved to UTC, matching the day on which
the vendors bill and reset their usage windows, and the on-disk cache
carries a scheme version so a value written by an older ASE is discarded
instead of being rendered as if it were current.
@Brusdeylins
Brusdeylins force-pushed the feat/statusline-monthly-cost branch from 8c2d82f to 6ef49b7 Compare August 2, 2026 00:40
@Brusdeylins Brusdeylins changed the title feat(statusline): add current-month total cost placeholder feat(statusline): add current-month total cost placeholder across all agent tools Aug 2, 2026
@Brusdeylins

Copy link
Copy Markdown
Collaborator Author

Update: month cost is now priced from a LiteLLM snapshot and covers all agent tools

The branch was rebased onto ASE 0.9.52 and extended substantially. The PR title and description have been rewritten accordingly; this comment summarizes what changed since the previous review state.

Why: the previous revision priced models from a hand-maintained table of seven Claude models, and a model missing from that table contributed nothing at all — no error, no warning. On my archive claude-opus-5 was missing and claude-sonnet-5 was priced at $3/$15 instead of $2/$10, so %Y reported $355.03 where the true figure is ~$790. Maintaining that table by hand is the actual defect; two of the earlier commits on this branch were pure price-table catch-up.

What changed:

  1. Prices come from a LiteLLM snapshot. tool/etc/litellm-prices.mjs reduces LiteLLM's price database to the 273 chat models of the vendors reachable through the supported agent tools and writes the checked-in module tool/src/ase-statusline-prices.ts. New build target npm start prices-update; only that target touches the network, build and rendering stay offline. Cache-read and 5-minute / 1-hour cache-write rates are now taken per model as published, instead of being derived from the input price by fixed factors.

  2. All three agent tools are scanned, not just Claude Code: Anthropic Claude Code (~/.claude/projects), OpenAI Codex CLI (~/.codex/{sessions,archived_sessions}, rollout-*.jsonl), and GitHub Copilot CLI (~/.copilot/session-state), each honoring its own environment override. Codex's cache-inclusive input and reasoning tokens, and Copilot's cumulative per-model aggregates, are normalized before pricing.

  3. The month boundary moved to UTC, matching the day on which the vendors bill and reset usage windows. On my archive that is a ~$125 difference against a local-time boundary.

  4. The on-disk cache carries a scheme version, so a value written by an older ASE is discarded instead of being rendered as if it were current.

Verification: lint + tsc green; a hand-computed fixture total of $0.045500 for Codex + Copilot matched exactly (including a planted previous-month record that must be excluded, and a duplicated cumulative Copilot record that must not double-count); real archive renders month: $791.20; cross-checked against codeburn, where the additional calls this implementation counts map 1:1 onto distinct requestIds.

Caveat — Vibe-Coded: the Codex and Copilot parsers follow the field names of codeburn's provider implementations (MIT) and are verified only against fixtures. Neither tool has produced logs on my machine, so those two paths have not been exercised against real-world session data. The Claude Code path is verified against a real archive.

🤖 Generated with Claude Code

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