Skip to content
This repository was archived by the owner on Jul 1, 2026. It is now read-only.

Add claude-cost-export: local-session cost → Fleet dashboard - #62

Merged
cpitzi merged 1 commit into
mainfrom
feat/claude-cost-export
Jun 16, 2026
Merged

Add claude-cost-export: local-session cost → Fleet dashboard#62
cpitzi merged 1 commit into
mainfrom
feat/claude-cost-export

Conversation

@cpitzi

@cpitzi cpitzi commented Jun 16, 2026

Copy link
Copy Markdown
Collaborator

Origin

Chris asked whether the cost metrics the bullpen emits could also be emitted from Claude Code directly, so his local interactive sessions land on the same "Claude Runner Fleet" Grafana dashboard. We compared native OpenTelemetry vs a hook-based approach; he preferred the hook path and specifically asked how reliably a SessionEnd hook fires and whether sessions could be buffered locally while off-LAN. This implements the design that answers both, and folds it into the bootstrap scripts as a standing workstation standard.

What this does

Reconstructs the cost of local, interactive Claude Code sessions from the on-disk transcript and ships one session_complete event per finished session to the homelab Alloy Loki receiver (:3100 on LXC 105) → Grafana Cloud. A workstation then appears on the Fleet dashboard under a new Local sessions row (job=claude_local, source=local, worker=<hostname>) without touching the fleet's own panels.

The bullpen gets total_cost_usd for free from headless claude -p --output-format json; interactive sessions have no such result blob, so cost is computed from token counts × pricing.json (cache reads 0.1×, 5m writes 1.25×, 1h writes 2×, Opus fast-mode 2×) — the same API-list-price basis the fleet reports.

Design (answers the two questions)

  • SessionEnd is best-effort. It fires on graceful exit but a hard kill (window close, SSH drop, crash) skips it. So the transcript is the source of truth; the hook just drops a done/<id> marker for promptness, and a systemd --user timer sweeps every 5 min as the backstop (a session idle > COST_IDLE_MIN is treated as finished). Nothing is lost to a missed hook.
  • Off-LAN buffering falls out of decoupling capture from shipping: finished sessions go to a local spool.ndjson; the shipper drains it to Loki when reachable; off-LAN POSTs fail and stay queued until the next on-LAN tick.
  • Exactly-once + no races: emitted.json guard + flock serialization. Install seeds the guard (claims existing sessions without shipping) so only post-install sessions ship — no backfill spike.

Token accounting reuses the empirically-derived dedup rules from the session-report skill (global uuid dedup for resumed sessions; per-requestId dedup keeping max output_tokens).

Files

claude-cost-export/cost-export.mjs (sweep+ship), pricing.json, cost-hook.sh (SessionEnd), claude-cost-export.{service,timer}, idempotent install.sh, README.md. All four setup-*.sh call the installer after node+jq+repos are present (non-fatal).

Verified

  • Cost math checked against hand-computation (synthetic Opus session = $0.07 exact).
  • requestId dedup keeps max output (810→800); idempotent re-run spools 0; flock serializes concurrent runs.
  • Round-trip confirmed: event accepted by Loki (HTTP 204) and queried back via Grafana Cloud.
  • shellcheck clean (setup scripts + bundle); node --check passes.

Companion PR

Dashboard row to display this data: PitziLabs/homelab-observability (Local sessions row on claude-runner-fleet.json).

Note

Pricing.json is a static snapshot of platform.claude.com pricing — update when Anthropic changes prices (raw tokens are shipped alongside cost_usd so totals stay recomputable).

🤖 Generated with Claude Code

Capture the cost of local, interactive Claude Code sessions and ship one
event per finished session to the homelab Alloy Loki receiver, so a
workstation appears on the "Claude Runner Fleet" Grafana dashboard next to
the bullpen agents (job=claude_local, source=local, worker=<hostname>).

The bullpen gets total_cost_usd for free from headless `claude -p`;
interactive sessions have no result blob, so cost is reconstructed from the
transcript (dedupe by uuid + requestId-keeping-max-output, sum tokens per
model) × pricing.json. The transcript is the source of truth; capture and
shipping are decoupled via a spool so a hard kill (skipping the SessionEnd
hook) is caught by the timer's idle backstop, and off-LAN sessions queue and
backfill when the LAN returns. Runs are flock-serialized and emit-once
guarded; install seeds the guard so only post-install sessions ship.

Wired into all four setup-*.sh (non-fatal, after node+jq+repos are present).

Prompt-Origin: Chris asked whether the cost metrics the bullpen emits could
also be emitted from Claude Code directly, to capture his local sessions in
the same dashboard. He preferred a SessionEnd-hook approach and asked about
hook reliability and off-LAN buffering; this implements the decoupled
transcript-as-truth + spool design that answers both.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cpitzi
cpitzi merged commit 4a70391 into main Jun 16, 2026
2 checks passed
@cpitzi
cpitzi deleted the feat/claude-cost-export branch June 16, 2026 17:25
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant