Skip to content

Commit 527375a

Browse files
committed
fix(translator): merge Anthropic message_delta usage at field level
Signed-off-by: pjdurden <prajjwalchittori1@gmail.com>
1 parent 896e782 commit 527375a

2 files changed

Lines changed: 60 additions & 16 deletions

File tree

internal/translator/anthropic_anthropic.go

Lines changed: 36 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -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

internal/translator/anthropic_anthropic_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,30 @@ data: {"type":"message_stop"}`
238238
// Total input = input_tokens(4522) + cache_read_input_tokens(4511) = 9033; total = 9033 + 5 = 9038.
239239
require.Equal(t, tokenUsageFrom(9033, 4511, 0, 5, 9038, -1), tokenUsage)
240240
})
241+
242+
t.Run("partial fields on message_delta do not clobber message_start", func(t *testing.T) {
243+
// A backend may set input_tokens on message_start and only report the final cache_read on
244+
// message_delta. Merging must be per field: the delta omits input_tokens (reported as 0),
245+
// which must not zero out the value already recorded from message_start.
246+
translator := NewAnthropicToAnthropicTranslator("", "")
247+
require.NotNil(t, translator)
248+
translator.(*anthropicToAnthropicTranslator).stream = true
249+
250+
const responseBody = `event: message_start
251+
data: {"type":"message_start","message":{"model":"claude-sonnet-4-5-20250929","id":"msg_x","type":"message","role":"assistant","content":[],"stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":1000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0}}}
252+
253+
event: message_delta
254+
data: {"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"output_tokens":10,"cache_read_input_tokens":500}}
255+
256+
event: message_stop
257+
data: {"type":"message_stop"}`
258+
259+
_, _, tokenUsage, _, err := translator.ResponseBody(nil, strings.NewReader(responseBody), false, nil)
260+
require.NoError(t, err)
261+
// message_start raw input(1000) is preserved; delta adds cache_read(500).
262+
// Total input = 1000 + 500 = 1500; total = 1500 + 10 = 1510.
263+
require.Equal(t, tokenUsageFrom(1500, 500, 0, 10, 1510, -1), tokenUsage)
264+
})
241265
}
242266

243267
func TestAnthropicToAnthropic_ResponseError(t *testing.T) {

0 commit comments

Comments
 (0)