fix: messages 流式缓存场景 token 记账错乱
qwen/dashscope 等上游 messages 流式的 usage 语义: - message_start.usage.input_tokens = 总输入 - message_delta.usage.input_tokens = 非缓存输入(缓存部分单列 cache_read/cache_creation 字段),是最终计费口径 原 usageSink 字段级合并中 delta 的 input 覆盖 start 的 input, 总输入丢失(31790 → 8);缓存写也未参与计费。 - push:带 cache_* 字段的 usage 视为最终口径,整体替换 sink - finishUsage:缓存写按 1.25× 输入价计费(Anthropic 5m 口径); 落库 input_tokens 存总量(含缓存读/写)便于对账 - 估算兜底条件排除已有缓存计数的请求 - 回归测试:缓存写/缓存命中/chat 末块合并不回归
This commit is contained in:
@@ -46,3 +46,56 @@ func TestSSEDataPayloadSpacedAndMultiLine(t *testing.T) {
|
||||
t.Fatalf("multiline block: got %q, want yo", got)
|
||||
}
|
||||
}
|
||||
|
||||
// 复现线上 qwen(dashscope)messages 流式缓存场景:
|
||||
// message_start.usage.input_tokens 是总输入,message_delta.usage.input_tokens 是非缓存输入
|
||||
// 且带 cache_read/cache_creation,是最终计费口径。合并后:
|
||||
// in(落库)=input+cache_read+cache_creation,计价 in 只算非缓存部分。
|
||||
// 此前 delta 的 input 覆盖 start 的 input 导致总输入丢失(31790 → 8)。
|
||||
func TestUsageSinkMessageDeltaAuthoritative(t *testing.T) {
|
||||
sink := &usageSink{}
|
||||
// message_start:总输入 31790
|
||||
start := json.RawMessage(`{"input_tokens":31790,"output_tokens":0}`)
|
||||
sink.push(start)
|
||||
if got := sink.us.InputTokens; got != 31790 {
|
||||
t.Fatalf("after start: input=%d, want 31790", got)
|
||||
}
|
||||
// message_delta:非缓存输入 8 + 缓存写 33229(最终口径,整体替换)
|
||||
delta := json.RawMessage(`{"output_tokens":8,"cache_creation_input_tokens":33229,"input_tokens":8,"cache_read_input_tokens":0}`)
|
||||
sink.push(delta)
|
||||
s := sink.Shape()
|
||||
if s.InputTokens != 8 || s.CacheCreationInputTokens != 33229 || s.OutputTokens != 8 {
|
||||
t.Fatalf("after delta: %+v, want input=8 cache_create=33229 output=8", s)
|
||||
}
|
||||
// finishUsage 口径:落库 input = 8 + 0 + 33229 = 33237(总量),计价 in=8、cacheCreate=33229
|
||||
in := s.InputTokens + s.CacheReadInputTokens + s.CacheCreationInputTokens
|
||||
if in != 33237 {
|
||||
t.Fatalf("total input=%d, want 33237", in)
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存命中场景(id=55):delta input=76 非缓存 + cache_read=33229 + cache_creation=17。
|
||||
func TestUsageSinkCacheHitMerge(t *testing.T) {
|
||||
sink := &usageSink{}
|
||||
sink.push(json.RawMessage(`{"input_tokens":31862,"output_tokens":0}`))
|
||||
sink.push(json.RawMessage(`{"output_tokens":32,"cache_creation_input_tokens":17,"input_tokens":76,"cache_read_input_tokens":33229}`))
|
||||
s := sink.Shape()
|
||||
total := s.InputTokens + s.CacheReadInputTokens + s.CacheCreationInputTokens
|
||||
if total != 33322 {
|
||||
t.Fatalf("total input=%d, want 33322 (76+33229+17)", total)
|
||||
}
|
||||
if s.OutputTokens != 32 {
|
||||
t.Fatalf("output=%d, want 32", s.OutputTokens)
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAI chat 末块(无 cache 字段)仍走零值不覆盖合并,不受整体替换影响。
|
||||
func TestUsageSinkChatLastChunkStillMerges(t *testing.T) {
|
||||
sink := &usageSink{}
|
||||
sink.push(json.RawMessage(`{"prompt_tokens":65,"completion_tokens":0}`))
|
||||
sink.push(json.RawMessage(`{"prompt_tokens":65,"completion_tokens":82}`))
|
||||
s := sink.Shape()
|
||||
if s.PromptTokens != 65 || s.CompletionTokens != 82 {
|
||||
t.Fatalf("chat merge broken: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user