fix(claude-agent-sdk): stop assigning query usage to the final call - #2348
Open
Abhijeet Prasad (AbhiPrasad) wants to merge 1 commit into
Open
fix(claude-agent-sdk): stop assigning query usage to the final call#2348Abhijeet Prasad (AbhiPrasad) wants to merge 1 commit into
Abhijeet Prasad (AbhiPrasad) wants to merge 1 commit into
Conversation
`ClaudeAgentSDKPlugin` copied session usage from the terminal result event to the final assistant
message in each `query()`. The final `anthropic.messages.create` span then combined request input with
cache totals for the full session.
```text
expected final call:
prompt = 100 input + 65,000 cache read + 2,000 cache creation = 67,100
completion = 1,000
previous final span:
prompt = request input + session cache read and creation = 242,100
completion = session output - earlier output snapshots = 3,497
```
Assistant messages contain the initial `message_start` output count, not the final output count. The
plugin subtracted these initial counts from the session output. This calculation assigned all remaining
output to the final request. It did not calculate the output for each request. The plugin also changed an
SDK message that the caller could read.
This defect caused incorrect prompt tokens, completion tokens, and token-based cost. Prompt caching is
enabled by default, so the defect affects all JavaScript Claude Agent SDK users. Both
`wrapClaudeAgentSDK` and automatic instrumentation use the same plugin. The defect was present from
version 3.21.0 through the previous main branch. It is the JavaScript equivalent of Python issue SDK-52.
The caller's `includePartialMessages` option now determines the source of final per-request usage.
Braintrust keeps this option unchanged because enabling it would add public stream events.
```text
includePartialMessages: true
message_start -> request ID and per-request prompt/cache usage
message_delta -> final per-request completion usage
result -> query metadata only
includePartialMessages: false or omitted (Claude SDK default)
assistant -> request ID and per-request prompt/cache usage
transcript -> final completion usage, when an exact match exists
no match -> omit completion_tokens and tokens
result -> query metadata only
```
When the option is `true`, the caller still receives all original partial events. The plugin merges each
final stream update with the assistant usage for the same message ID. A partial update cannot remove
valid prompt or cache usage.
When the option is `false` or omitted, the caller does not receive partial events. The plugin ignores the
initial output snapshot. Prompt and cache metrics remain exact because the assistant message reports
them for one request. Completion and total metrics are exact only when transcript recovery succeeds.
An unavailable transcript causes missing completion metrics, not zero or estimated metrics.
The plugin gets transcript paths from passive SDK hooks. It keeps the root path separate from each
subagent path.
```text
SessionStart ------\
UserPromptSubmit ---+--> root transcript path ------> [root, message ID]
SessionEnd --------/
SubagentStop(tool A) ---> transcript path A --------> [tool A, message ID]
SubagentStop(tool B) ---> transcript path B --------> [tool B, message ID]
```
The agent key and message ID form one lookup key. This key prevents a root message from matching a
subagent row. It also keeps two subagents separate when their streams run at the same time.
```text
Claude SDK Plugin Transcript
assistant(id=msg_1) --------> save [root, msg_1] and span
query stream ends ----------> read root path -------------> search backward for msg_1
<----------------------------- newest valid usage row
update span [root, msg_1]
missing row ----------------> wait 25 ms and read again maximum: 3 reads
```
The reader searches only for pending message IDs. It selects the newest valid row because a transcript
can contain multiple snapshots for one message. It does not parse every row in a large resumed session.
Transcript errors do not affect the query. The retries can add at most 50 ms after stream completion.
The plugin does not store transcript content or transcript paths in span data.
The plugin does not record terminal usage on the root task span. Braintrust summaries already add the
metrics from model-call spans. A second copy on the root span would count the same tokens and cost twice.
The Anthropic instrumentation now reads the 5-minute and 1-hour cache-write fields. It uses the object
returned by `finalizeAnthropicTokens`. Therefore, each span contains the TTL fields or the legacy
aggregate field, but not both. Prompt totals use the same cache representation that the span stores.
Unit and end-to-end tests compare each model-call span with stream or transcript usage. Tests also cover
both partial-message settings, missing transcripts, unchanged SDK messages, null usage fields, wrappers,
and automatic instrumentation.
Abhijeet Prasad (AbhiPrasad)
force-pushed
the
abhi-fix-anthropic-token-accounting
branch
from
August 10, 2026 14:39
5e6c3ce to
8e1069e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
resolves https://linear.app/braintrustdata/issue/SDK-208/js-claude-agent-sdk-instrumentation-stamps-session-cumulative-result
AI Summary
ClaudeAgentSDKPlugincopied session usage from the terminal result event to the final assistantmessage in each
query(). The finalanthropic.messages.createspan then combined request input withcache totals for the full session.
Assistant messages contain the initial
message_startoutput count, not the final output count. Theplugin subtracted these initial counts from the session output. This calculation assigned all remaining
output to the final request. It did not calculate the output for each request. The plugin also changed an
SDK message that the caller could read.
This defect caused incorrect prompt tokens, completion tokens, and token-based cost. Prompt caching is
enabled by default, so the defect affects all JavaScript Claude Agent SDK users. Both
wrapClaudeAgentSDKand automatic instrumentation use the same plugin. The defect was present fromversion 3.21.0 through the previous main branch. It is the JavaScript equivalent of Python issue SDK-52.
The caller's
includePartialMessagesoption now determines the source of final per-request usage.Braintrust keeps this option unchanged because enabling it would add public stream events.
When the option is
true, the caller still receives all original partial events. The plugin merges eachfinal stream update with the assistant usage for the same message ID. A partial update cannot remove
valid prompt or cache usage.
When the option is
falseor omitted, the caller does not receive partial events. The plugin ignores theinitial output snapshot. Prompt and cache metrics remain exact because the assistant message reports
them for one request. Completion and total metrics are exact only when transcript recovery succeeds.
An unavailable transcript causes missing completion metrics, not zero or estimated metrics.
The plugin gets transcript paths from passive SDK hooks. It keeps the root path separate from each
subagent path.
The agent key and message ID form one lookup key. This key prevents a root message from matching a
subagent row. It also keeps two subagents separate when their streams run at the same time.
The reader searches only for pending message IDs. It selects the newest valid row because a transcript
can contain multiple snapshots for one message. It does not parse every row in a large resumed session.
Transcript errors do not affect the query. The retries can add at most 50 ms after stream completion.
The plugin does not store transcript content or transcript paths in span data.
The plugin does not record terminal usage on the root task span. Braintrust summaries already add the
metrics from model-call spans. A second copy on the root span would count the same tokens and cost twice.
The Anthropic instrumentation now reads the 5-minute and 1-hour cache-write fields. It uses the object
returned by
finalizeAnthropicTokens. Therefore, each span contains the TTL fields or the legacyaggregate field, but not both. Prompt totals use the same cache representation that the span stores.
The Anthropic instrumentation now reads the 5-minute and 1-hour cache-write fields. It uses the object
returned by
finalizeAnthropicTokens. Therefore, each span contains the TTL fields or the legacyaggregate field, but not both. Prompt totals use the same cache representation that the span stores. This matches
the spec from https://github.com/braintrustdata/braintrust-spec/blob/main/skills/instrumentation-spec/references/features/prompt-cache.md