@@ -178,24 +178,44 @@ func (a *anthropicToAnthropicTranslator) reflectStreamingEvent(eventUnion *anthr
178178 }
179179 case eventUnion .MessageDelta != nil :
180180 u := eventUnion .MessageDelta .Usage
181- // message_delta events provide final counts for specific token types.
182- // Standard Anthropic only reports output_tokens here, but some Anthropic-compatible
183- // backends report the final input/cache token counts on message_delta instead of
184- // message_start. When those fields are present, merge them so they are not dropped.
185- // They are only merged when non-zero to avoid clobbering message_start values with a
186- // delta that carries output tokens only. See https://github.com/envoyproxy/ai-gateway/issues/2290.
187- if u .InputTokens > 0 || u .CacheReadInputTokens > 0 || u .CacheCreationInputTokens > 0 {
188- messageDeltaUsage := metrics .ExtractTokenUsageFromExplicitCaching (
189- int64 (u .InputTokens ),
190- int64 (u .OutputTokens ),
191- ptr .To (int64 (u .CacheReadInputTokens )),
192- ptr .To (int64 (u .CacheCreationInputTokens )),
193- )
194- a .streamingTokenUsage .Override (messageDeltaUsage )
195- } else if u .OutputTokens >= 0 {
196- // Update output tokens from message_delta (final count).
181+ // message_delta carries the final counts. Standard Anthropic only reports output_tokens
182+ // here, but some Anthropic-compatible backends report the final input/cache counts on
183+ // message_delta instead of message_start. See https://github.com/envoyproxy/ai-gateway/issues/2290.
184+ //
185+ // output_tokens is always the final value on message_delta, so take it unconditionally.
186+ if u .OutputTokens >= 0 {
197187 a .streamingTokenUsage .SetOutputTokens (uint32 (u .OutputTokens )) //nolint:gosec
198188 }
189+ // Merge the input/cache counts per field rather than replacing the whole usage snapshot:
190+ // a delta may report only the fields that apply (the rest arrive as zero), so overwriting
191+ // every field would clobber values already set on message_start. We can only treat a field
192+ // as "present" when it is non-zero, since the upstream usage fields are not pointers.
193+ if u .InputTokens > 0 || u .CacheReadInputTokens > 0 || u .CacheCreationInputTokens > 0 {
194+ // The unified input_tokens is the sum of raw input + cache-read + cache-creation, so
195+ // recover the latest known value of each component and overwrite only the ones present
196+ // on this delta before recomputing the sum.
197+ cacheRead , _ := a .streamingTokenUsage .CachedInputTokens ()
198+ cacheCreation , _ := a .streamingTokenUsage .CacheCreationInputTokens ()
199+ unifiedInput , _ := a .streamingTokenUsage .InputTokens ()
200+ var rawInput uint32
201+ if unifiedInput >= cacheRead + cacheCreation {
202+ rawInput = unifiedInput - cacheRead - cacheCreation
203+ }
204+
205+ if u .InputTokens > 0 {
206+ rawInput = uint32 (u .InputTokens ) //nolint:gosec
207+ }
208+ if u .CacheReadInputTokens > 0 {
209+ cacheRead = uint32 (u .CacheReadInputTokens ) //nolint:gosec
210+ }
211+ if u .CacheCreationInputTokens > 0 {
212+ cacheCreation = uint32 (u .CacheCreationInputTokens ) //nolint:gosec
213+ }
214+
215+ a .streamingTokenUsage .SetCachedInputTokens (cacheRead )
216+ a .streamingTokenUsage .SetCacheCreationInputTokens (cacheCreation )
217+ a .streamingTokenUsage .SetInputTokens (rawInput + cacheRead + cacheCreation )
218+ }
199219 }
200220}
201221
0 commit comments