[BUG] JSONL session transcripts missing final message_stop event, output token counts unreliable
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?
Session JSONL files (~/.claude/projects/<project>/<session-id>.jsonl) log every content block during streaming but never log
the final message_stop event.
output_tokensin theusagefield is a mid-stream snapshot, not the final countstop_reasonis alwaysnull, no line ever records"end_turn"- Output tokens are undercounted by ~2x compared to actual content
- Input/cache token counts are accurate (set at request start)
### Evidence
Single API call producing thinking + text + 3 tool_use blocks:
```text
Line 41: thinking output_tokens=8 stop_reason=null
Line 42: text output_tokens=8 stop_reason=null
Line 43: tool_use output_tokens=8 stop_reason=null
Line 45: tool_use output_tokens=8 stop_reason=null
Line 46: tool_use output_tokens=310 stop_reason=null ← partial update, still not final
Full session comparison (128 API calls, Opus 4.6, 49 min):
┌───────────────────────────────────────────┬───────────────┐
│ Method │ Output Tokens │
├───────────────────────────────────────────┼───────────────┤
│ JSONL output_tokens summed │ 23,725 │
├───────────────────────────────────────────┼───────────────┤
│ tiktoken cl100k_base on extracted content │ 45,050 │
├───────────────────────────────────────────┼───────────────┤
│ Undercount │ ~1.9x │
└───────────────────────────────────────────┴───────────────┘
Impact
- Cost tracking tools (ccusage, cccost, custom scripts) that parse JSONL get wrong output counts
- No way to reconcile per-session cost against Anthropic billing dashboard
- Users building usage reports or budgeting around JSONL data are underestimating spend
What Should Happen?
Each API call should have a final JSONL line with:
stop_reason: "end_turn"(or"tool_use")- Accurate final
output_tokenscount matching what Anthropic actually bills
This is one additional JSONL line per API call , the final streaming event that currently gets discarded.
Error Messages/Logs
{
"type": "assistant",
"requestId": "req_011CYLnnca3yt6Axy4nsUrRM",
"message": {
"role": "assistant",
"stop_reason": null,
"usage": {
"input_tokens": 1,
"cache_creation_input_tokens": 510,
"cache_read_input_tokens": 40716,
"output_tokens": 310
}
}
}
No line in the entire 1043-line JSONL transcript has stop_reason set to anything other than null.
Steps to Reproduce
- Run any Claude Code session with multiple tool calls
- After session ends, open the JSONL transcript at
~/.claude/projects/<project>/<session-id>.jsonl - Search for
stop_reason, every line showsnull - Sum
output_tokensfrom the last JSONL line per uniquerequestId - Compare against actual output content (extract all thinking/text/tool_use blocks, tokenize with tiktoken)
- Observe ~2x undercount
Claude Model
Opus
Is this a regression?
No, this never worked
Last Working Version
_No response_
Claude Code Version
2.1.50
Platform
Anthropic API
Operating System
macOS
Terminal/Shell
Terminal.app (macOS)
Additional Information
Session analyzed: 128 API calls, 49 minutes, ~11.3M total input tokens (mostly cached), heavy MCP tool usage. The undercount is consistent across the entire session, not isolated to specific request types.
This issue has 5 comments on GitHub. Read the full discussion on GitHub ↗