Skip to content

Add burn waste: per-tool-call & per-file cost attribution#50

Open
willwashburn wants to merge 1 commit intomainfrom
feat/burn-waste
Open

Add burn waste: per-tool-call & per-file cost attribution#50
willwashburn wants to merge 1 commit intomainfrom
feat/burn-waste

Conversation

@willwashburn
Copy link
Copy Markdown
Member

@willwashburn willwashburn commented Apr 23, 2026

Summary

  • New burn waste command: ranks tool calls, files, Bash commands, and subagent calls by their attributed cost
  • Splits each tool_use's cost into initial (tokens that enter context at the next turn as fresh input/cacheCreate) and persistence (the same tokens riding along in cacheRead for every subsequent turn until eviction)
  • Sized attribution when the content sidecar is enabled (uses tool_result text length / 4 as a token estimate); even-split fallback (initial only) when it isn't, with a printed note

Closes #3.

Output shape

turns analyzed: 12,345
session grand total: $4.21
attributed to tool calls: $3.18  /  unattributed (output, system overhead, untracked): $1.03

Top files by cumulative cost
path                  firstTurn  initial(tok)  persist(tok)  rideTurns  cost      %attr
/src/big.ts           2          8,123         162,460        20         $0.0696   2.2%
…

Top Bash commands by cost
command          calls  initial(tok)  persist(tok)  cost
ls -la /repo     11     45,012        90,103        $0.041
…

Top subagent calls by cost
subagent          calls  initial(tok)  persist(tok)  cost
general-purpose   3      24,000        72,500        $0.0234
…

--all shows the full lists; --json emits the raw aggregations for downstream tooling.

Attribution math

  • Initial cost (turn N+1): a tool_result of size S tokens contributes S × ((next.input × inputPrice + next.cacheCreate × cacheWritePrice) / (next.input + next.cacheCreate)) — i.e. the model-weighted blend of fresh-input and cacheCreate rates that turn N+1 actually paid.
  • Persistence (turns N+2 … M): each subsequent turn adds S × cacheReadPrice / 1M. Eviction is approximated by stopping when cacheRead < S (the tool_result no longer fits in cache, so it must have aged out or been compacted away).
  • Aggregations: Read/Edit/Write/NotebookEdit group by target path; Bash groups by argsHash; Agent/Task group by subagent_type (or target as a fallback label).

Test plan

  • pnpm run build — clean
  • pnpm run test — 188/188 passing (six new attribution tests)
  • Synthetic-session acceptance test (one 8k Read + 20 ride-along turns) — within ±10% of hand-computed truth
  • attributedTotal + unattributedTotal == grandTotal invariant test
  • Even-split fallback test for sessions with no content sidecar
  • burn help lists waste and example invocation

Notes / follow-ups

🤖 Generated with Claude Code


Open in Devin Review

Computes initial cost (the turn after a tool call, where the tool_result
enters context) and persistence cost (subsequent turns where the result
rides along in cacheRead) for every tool_use in a session. Aggregates
into three rankings: top files (Read/Edit/Write/NotebookEdit by target
path), top Bash commands (grouped by argsHash so repeated invocations
collapse), and top subagent calls (Agent/Task by subagent_type).

Sized attribution uses the content sidecar to estimate each tool_result's
token count from its text length (chars/4); when the sidecar is unavail-
able the command falls back to even-split across the prior turn's tool
calls (initial cost only) and emits a note. Tool results are treated as
evicted from cache once a turn's cacheRead drops below their estimated
size, capping persistence reasonably without modeling the 5m/1h tier
boundary explicitly.

Closes #3

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 5 additional findings in Devin Review.

Open in Devin Review

Comment on lines +271 to +272
const turnRate = t.model === emitModel ? rate : null;
const cacheReadPrice = turnRate ? turnRate.cacheRead : rate.cacheRead;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Cross-model persistence pricing is a no-op — always uses emit model's rate

In computePersistenceCostSized, lines 271-272 attempt to differentiate pricing when a tail turn uses a different model than the emit turn, but the logic is a no-op. When t.model === emitModel, turnRate = rate so cacheReadPrice = rate.cacheRead. When t.model !== emitModel, turnRate = null so the fallback is still rate.cacheRead. Both branches produce the identical result. The pricing table (available in the caller at packages/analyze/src/waste.ts:130) is never passed through to this function, so it cannot look up the tail turn's actual model rate. In sessions that switch models (e.g. Sonnet → Haiku, where cacheRead rates differ ~10×), persistence costs will be priced at the wrong model's rate.

Prompt for agents
The function computePersistenceCostSized at packages/analyze/src/waste.ts:254 receives only the emit model's ModelCost (rate), not the full PricingTable. Lines 271-272 attempt cross-model rate differentiation but are a no-op because the fallback always uses rate.cacheRead.

To fix: pass the PricingTable through the call chain from attributeSession (line 130) → attributeToolCallsSized (line 164) → computePersistenceCostSized (line 198). Then inside the loop at line 271, call lookupRate(t.model, pricing) to get the tail turn's actual rate and use its cacheRead price. The emitModel parameter and the existing conditional can then be replaced with a proper pricing table lookup.

Affected files: packages/analyze/src/waste.ts (attributeSession, attributeToolCallsSized, computePersistenceCostSized function signatures and bodies).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

burn waste: per-tool-call and per-file spend attribution

1 participant