[BUG] result.modelUsage reports cumulative session totals, not per-turn deltas — breaks usage tracking
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
result.modelUsage in SDKResultMessage returns cumulative session totals, not per-turn usage. This is not documented anywhere, and the field name (modelUsage) strongly implies per-result values. Any application that sums modelUsage across multiple query() calls or session.send() turns will dramatically overcount token usage.
Reproduction
import { unstable_v2_createSession } from "@anthropic-ai/claude-agent-sdk";
const session = await unstable_v2_createSession({ model: "claude-sonnet-4-6" });
// Turn 1
const r1 = await session.send("What is 2+2?");
console.log(r1.modelUsage);
// { "claude-sonnet-4-6": { inputTokens: 100, outputTokens: 20 } }
// Turn 2
const r2 = await session.send("What is 3+3?");
console.log(r2.modelUsage);
// { "claude-sonnet-4-6": { inputTokens: 250, outputTokens: 45 } }
// ^^^^^^^^^^^^^^^^
// This is the SESSION TOTAL, not turn 2's usage.
// Turn 2 actually used ~150 input + ~25 output.
If you naively sum them:
total = r1.inputTokens + r2.inputTokens = 100 + 250 = 350 // WRONG (2.4x overcount)
actual = 250 // The cumulative total IS the correct total
What Should Happen?
One of:
- Document the cumulative semantics — Add a clear note in the SDK types/docs that
modelUsageis a running session total, not a per-turn delta. This is the minimum fix.
- Add a per-turn field (preferred) — Expose
turnUsageordeltaUsagealongside the cumulativemodelUsage, so applications can choose which they need without manual bookkeeping.
- Fix the semantics (breaking) — Make
modelUsagereport per-turn values. This would be a breaking change for anyone already working around the cumulative behavior.
Impact
Any SDK consumer tracking context window occupancy, cost estimation, or token budgets will miscalculate if they treat modelUsage as per-turn. This is especially dangerous for:
- Context window management — Overcounting input tokens leads to premature compaction or session restarts
- Cost tracking — Summing cumulative values inflates reported costs by 2-5x depending on session length
- Rate limit estimation — Overcounting tokens makes rate limit budgeting inaccurate
We discovered this while building a ContextManager that tracks context window occupancy. Our watermark estimates were consistently 2-3x higher than actual context size, triggering unnecessary compactions. The fix was to diff consecutive modelUsage snapshots:
function diffCumulativeModelUsage(
current: Record<string, ModelUsage>,
previous: Record<string, ModelUsage>,
): { deltaUsage: Record<string, ModelUsage>; resetDetected: boolean } {
// If total decreased, session was reset (compaction/restart)
const resetDetected = sumTokens(current) < sumTokens(previous);
const delta: Record<string, ModelUsage> = {};
for (const [model, usage] of Object.entries(current)) {
const prev = previous[model];
if (resetDetected || !prev) {
delta[model] = { ...usage };
} else {
delta[model] = {
inputTokens: Math.max(0, usage.inputTokens - prev.inputTokens),
outputTokens: Math.max(0, usage.outputTokens - prev.outputTokens),
cacheReadInputTokens: Math.max(0, usage.cacheReadInputTokens - prev.cacheReadInputTokens),
cacheCreationInputTokens: Math.max(0, usage.cacheCreationInputTokens - prev.cacheCreationInputTokens),
// ... other fields
};
}
}
return { deltaUsage: delta, resetDetected };
}
Additional Notes
result.usage(the per-request API usage) has the same cumulative semantics- Session resets (compaction, restart) cause the cumulative total to drop — applications must detect this edge case or they'll compute negative deltas
- Related: #35214 reports
contextWindowbeing wrong inmodelUsage— a separate field-level bug in the same structure
Environment
@anthropic-ai/claude-agent-sdk: 0.2.77- Claude Code CLI: 2.1.77
- Runtime: Bun 1.3.10
- OS: macOS (Darwin 25.2.0, arm64)
This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗