[BUG] result.modelUsage reports cumulative session totals, not per-turn deltas — breaks usage tracking

Resolved 💬 4 comments Opened Mar 27, 2026 by miyago9267 Closed Apr 26, 2026

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:

  1. Document the cumulative semantics — Add a clear note in the SDK types/docs that modelUsage is a running session total, not a per-turn delta. This is the minimum fix.
  1. Add a per-turn field (preferred) — Expose turnUsage or deltaUsage alongside the cumulative modelUsage, so applications can choose which they need without manual bookkeeping.
  1. Fix the semantics (breaking) — Make modelUsage report 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 contextWindow being wrong in modelUsage — 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)

View original on GitHub ↗

This issue has 4 comments on GitHub. Read the full discussion on GitHub ↗