-
Notifications
You must be signed in to change notification settings - Fork 40
Fix: Read prompt tokens from message_delta usage on the beta Messages path #811
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -177,9 +177,10 @@ func parseAnthropicJSON(body []byte, ext *pipeline.InferenceExtension) { | |||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // anthropicStreamEvent is one SSE event's data payload. The Messages stream is | ||||||||||||||||||||||||||||||||||||||||||
| // a sequence of typed events (vs OpenAI's uniform chat.completion.chunk): | ||||||||||||||||||||||||||||||||||||||||||
| // message_start (carries usage.input_tokens), content_block_delta (text_delta / | ||||||||||||||||||||||||||||||||||||||||||
| // message_start (carries usage — but see below), content_block_delta (text_delta / | ||||||||||||||||||||||||||||||||||||||||||
| // input_json_delta / thinking_delta), message_delta (delta.stop_reason + | ||||||||||||||||||||||||||||||||||||||||||
| // cumulative usage.output_tokens), message_stop, plus ping/content_block_*. | ||||||||||||||||||||||||||||||||||||||||||
| // cumulative usage.output_tokens, and on the ?beta=true path the prompt-cache | ||||||||||||||||||||||||||||||||||||||||||
| // counts too), message_stop, plus ping/content_block_*. | ||||||||||||||||||||||||||||||||||||||||||
| type anthropicStreamEvent struct { | ||||||||||||||||||||||||||||||||||||||||||
| Type string `json:"type"` | ||||||||||||||||||||||||||||||||||||||||||
| Message *struct { | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -194,7 +195,9 @@ type anthropicStreamEvent struct { | |||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| // foldAnthropicFrame folds one Messages SSE event into the running stream state. | ||||||||||||||||||||||||||||||||||||||||||
| // input_tokens come from message_start; the completion accumulates from | ||||||||||||||||||||||||||||||||||||||||||
| // The prompt size is taken as the largest total seen, because different Messages | ||||||||||||||||||||||||||||||||||||||||||
| // API paths report it on different events: message_start on the plain path, | ||||||||||||||||||||||||||||||||||||||||||
| // message_delta on the ?beta=true path. The completion accumulates from | ||||||||||||||||||||||||||||||||||||||||||
| // text_delta blocks; stop_reason and the cumulative output_tokens arrive in | ||||||||||||||||||||||||||||||||||||||||||
| // message_delta. Unknown events (ping, content_block_start/stop, message_stop) | ||||||||||||||||||||||||||||||||||||||||||
| // are ignored. | ||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -216,12 +219,27 @@ func foldAnthropicFrame(frame []byte, state *inferenceStreamState, ext *pipeline | |||||||||||||||||||||||||||||||||||||||||
| if ev.Delta != nil && ev.Delta.StopReason != "" { | ||||||||||||||||||||||||||||||||||||||||||
| ext.FinishReason = ev.Delta.StopReason | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if ev.Usage != nil && ev.Usage.OutputTokens > 0 { | ||||||||||||||||||||||||||||||||||||||||||
| // usage.output_tokens in message_delta is cumulative — take the | ||||||||||||||||||||||||||||||||||||||||||
| // latest. TotalTokens must be non-zero for the shared finalize | ||||||||||||||||||||||||||||||||||||||||||
| // block to copy the counts onto the extension. | ||||||||||||||||||||||||||||||||||||||||||
| state.usage.CompletionTokens = ev.Usage.OutputTokens | ||||||||||||||||||||||||||||||||||||||||||
| state.usage.TotalTokens = state.usage.PromptTokens + ev.Usage.OutputTokens | ||||||||||||||||||||||||||||||||||||||||||
| if ev.Usage != nil { | ||||||||||||||||||||||||||||||||||||||||||
| // The prompt side can arrive here rather than in message_start. | ||||||||||||||||||||||||||||||||||||||||||
| // Clients using the ?beta=true Messages path (Claude Code sends | ||||||||||||||||||||||||||||||||||||||||||
| // anthropic-beta: claude-code-*) get a message_start carrying only | ||||||||||||||||||||||||||||||||||||||||||
| // input_tokens, with cache_creation_input_tokens and | ||||||||||||||||||||||||||||||||||||||||||
| // cache_read_input_tokens deferred to message_delta — so reading | ||||||||||||||||||||||||||||||||||||||||||
| // the prompt size from message_start alone undercounts a cached | ||||||||||||||||||||||||||||||||||||||||||
| // agent request by orders of magnitude (a 33k-token turn recorded | ||||||||||||||||||||||||||||||||||||||||||
| // as 9). Take the larger value: on the non-beta path message_delta | ||||||||||||||||||||||||||||||||||||||||||
| // carries no input counts, and assigning unconditionally would | ||||||||||||||||||||||||||||||||||||||||||
| // clobber the correct message_start total with zero. | ||||||||||||||||||||||||||||||||||||||||||
| if p := ev.Usage.promptTotal(); p > state.usage.PromptTokens { | ||||||||||||||||||||||||||||||||||||||||||
| state.usage.PromptTokens = p | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| if ev.Usage.OutputTokens > 0 { | ||||||||||||||||||||||||||||||||||||||||||
| // usage.output_tokens in message_delta is cumulative — take the | ||||||||||||||||||||||||||||||||||||||||||
| // latest. TotalTokens must be non-zero for the shared finalize | ||||||||||||||||||||||||||||||||||||||||||
| // block to copy the counts onto the extension. | ||||||||||||||||||||||||||||||||||||||||||
| state.usage.CompletionTokens = ev.Usage.OutputTokens | ||||||||||||||||||||||||||||||||||||||||||
| state.usage.TotalTokens = state.usage.PromptTokens + ev.Usage.OutputTokens | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+233
to
+242
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win Record prompt usage when the stream has zero output tokens. If Proposed fix if ev.Usage.OutputTokens > 0 {
// usage.output_tokens in message_delta is cumulative — take the
// latest. TotalTokens must be non-zero for the shared finalize
// block to copy the counts onto the extension.
state.usage.CompletionTokens = ev.Usage.OutputTokens
- state.usage.TotalTokens = state.usage.PromptTokens + ev.Usage.OutputTokens
}
+ state.usage.TotalTokens = state.usage.PromptTokens + state.usage.CompletionTokens
}📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking, and not a regression from this PR: if a terminal
message_deltaever carried the prompt-cache counts withoutput_tokens == 0,PromptTokensgets refreshed butTotalTokensstays 0, so the finalize gate (if state.usage.TotalTokens > 0) would drop the whole prompt count. A real Anthropic turn always emitsoutput_tokens > 0on the final delta, so this is theoretical. If you want to harden it, recomputeTotalTokenswheneverPromptTokenschanges rather than only inside theOutputTokens > 0arm. This is the same case CodeRabbit flagged.