Merge pull request #1970 from deqiying/fix-1754-claude-openai-cache-usage
fix(anthropic): 修正缓存 token 的 Anthropic 用量语义
This commit is contained in:
@@ -181,6 +181,55 @@ func TestResponsesToAnthropic_TextOnly(t *testing.T) {
|
|||||||
assert.Equal(t, 5, anth.Usage.OutputTokens)
|
assert.Equal(t, 5, anth.Usage.OutputTokens)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestResponsesToAnthropic_CachedTokensUseAnthropicInputSemantics(t *testing.T) {
|
||||||
|
resp := &ResponsesResponse{
|
||||||
|
ID: "resp_cached",
|
||||||
|
Model: "gpt-5.2",
|
||||||
|
Status: "completed",
|
||||||
|
Output: []ResponsesOutput{
|
||||||
|
{
|
||||||
|
Type: "message",
|
||||||
|
Content: []ResponsesContentPart{
|
||||||
|
{Type: "output_text", Text: "Cached response"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Usage: &ResponsesUsage{
|
||||||
|
InputTokens: 54006,
|
||||||
|
OutputTokens: 123,
|
||||||
|
TotalTokens: 54129,
|
||||||
|
InputTokensDetails: &ResponsesInputTokensDetails{
|
||||||
|
CachedTokens: 50688,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
|
||||||
|
assert.Equal(t, 3318, anth.Usage.InputTokens)
|
||||||
|
assert.Equal(t, 50688, anth.Usage.CacheReadInputTokens)
|
||||||
|
assert.Equal(t, 123, anth.Usage.OutputTokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResponsesToAnthropic_CachedTokensClampInputTokens(t *testing.T) {
|
||||||
|
resp := &ResponsesResponse{
|
||||||
|
ID: "resp_cached_clamp",
|
||||||
|
Model: "gpt-5.2",
|
||||||
|
Status: "completed",
|
||||||
|
Usage: &ResponsesUsage{
|
||||||
|
InputTokens: 100,
|
||||||
|
OutputTokens: 5,
|
||||||
|
InputTokensDetails: &ResponsesInputTokensDetails{
|
||||||
|
CachedTokens: 150,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
anth := ResponsesToAnthropic(resp, "claude-sonnet-4-5-20250929")
|
||||||
|
assert.Equal(t, 0, anth.Usage.InputTokens)
|
||||||
|
assert.Equal(t, 150, anth.Usage.CacheReadInputTokens)
|
||||||
|
assert.Equal(t, 5, anth.Usage.OutputTokens)
|
||||||
|
}
|
||||||
|
|
||||||
func TestResponsesToAnthropic_ToolUse(t *testing.T) {
|
func TestResponsesToAnthropic_ToolUse(t *testing.T) {
|
||||||
resp := &ResponsesResponse{
|
resp := &ResponsesResponse{
|
||||||
ID: "resp_456",
|
ID: "resp_456",
|
||||||
@@ -343,6 +392,36 @@ func TestStreamingTextOnly(t *testing.T) {
|
|||||||
assert.Equal(t, "message_stop", events[1].Type)
|
assert.Equal(t, "message_stop", events[1].Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStreamingCachedTokensUseAnthropicInputSemantics(t *testing.T) {
|
||||||
|
state := NewResponsesEventToAnthropicState()
|
||||||
|
ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
|
||||||
|
Type: "response.created",
|
||||||
|
Response: &ResponsesResponse{ID: "resp_cached_stream", Model: "gpt-5.2"},
|
||||||
|
}, state)
|
||||||
|
|
||||||
|
events := ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{
|
||||||
|
Type: "response.completed",
|
||||||
|
Response: &ResponsesResponse{
|
||||||
|
Status: "completed",
|
||||||
|
Usage: &ResponsesUsage{
|
||||||
|
InputTokens: 54006,
|
||||||
|
OutputTokens: 123,
|
||||||
|
TotalTokens: 54129,
|
||||||
|
InputTokensDetails: &ResponsesInputTokensDetails{
|
||||||
|
CachedTokens: 50688,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, state)
|
||||||
|
|
||||||
|
require.Len(t, events, 2)
|
||||||
|
assert.Equal(t, "message_delta", events[0].Type)
|
||||||
|
assert.Equal(t, 3318, events[0].Usage.InputTokens)
|
||||||
|
assert.Equal(t, 50688, events[0].Usage.CacheReadInputTokens)
|
||||||
|
assert.Equal(t, 123, events[0].Usage.OutputTokens)
|
||||||
|
assert.Equal(t, "message_stop", events[1].Type)
|
||||||
|
}
|
||||||
|
|
||||||
func TestStreamingToolCall(t *testing.T) {
|
func TestStreamingToolCall(t *testing.T) {
|
||||||
state := NewResponsesEventToAnthropicState()
|
state := NewResponsesEventToAnthropicState()
|
||||||
|
|
||||||
|
|||||||
@@ -84,18 +84,34 @@ func ResponsesToAnthropic(resp *ResponsesResponse, model string) *AnthropicRespo
|
|||||||
out.StopReason = responsesStatusToAnthropicStopReason(resp.Status, resp.IncompleteDetails, blocks)
|
out.StopReason = responsesStatusToAnthropicStopReason(resp.Status, resp.IncompleteDetails, blocks)
|
||||||
|
|
||||||
if resp.Usage != nil {
|
if resp.Usage != nil {
|
||||||
out.Usage = AnthropicUsage{
|
out.Usage = anthropicUsageFromResponsesUsage(resp.Usage)
|
||||||
InputTokens: resp.Usage.InputTokens,
|
|
||||||
OutputTokens: resp.Usage.OutputTokens,
|
|
||||||
}
|
|
||||||
if resp.Usage.InputTokensDetails != nil {
|
|
||||||
out.Usage.CacheReadInputTokens = resp.Usage.InputTokensDetails.CachedTokens
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func anthropicUsageFromResponsesUsage(usage *ResponsesUsage) AnthropicUsage {
|
||||||
|
if usage == nil {
|
||||||
|
return AnthropicUsage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
cachedTokens := 0
|
||||||
|
if usage.InputTokensDetails != nil {
|
||||||
|
cachedTokens = usage.InputTokensDetails.CachedTokens
|
||||||
|
}
|
||||||
|
|
||||||
|
inputTokens := usage.InputTokens - cachedTokens
|
||||||
|
if inputTokens < 0 {
|
||||||
|
inputTokens = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return AnthropicUsage{
|
||||||
|
InputTokens: inputTokens,
|
||||||
|
OutputTokens: usage.OutputTokens,
|
||||||
|
CacheReadInputTokens: cachedTokens,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func responsesStatusToAnthropicStopReason(status string, details *ResponsesIncompleteDetails, blocks []AnthropicContentBlock) string {
|
func responsesStatusToAnthropicStopReason(status string, details *ResponsesIncompleteDetails, blocks []AnthropicContentBlock) string {
|
||||||
switch status {
|
switch status {
|
||||||
case "incomplete":
|
case "incomplete":
|
||||||
@@ -466,11 +482,10 @@ func resToAnthHandleCompleted(evt *ResponsesStreamEvent, state *ResponsesEventTo
|
|||||||
stopReason := "end_turn"
|
stopReason := "end_turn"
|
||||||
if evt.Response != nil {
|
if evt.Response != nil {
|
||||||
if evt.Response.Usage != nil {
|
if evt.Response.Usage != nil {
|
||||||
state.InputTokens = evt.Response.Usage.InputTokens
|
usage := anthropicUsageFromResponsesUsage(evt.Response.Usage)
|
||||||
state.OutputTokens = evt.Response.Usage.OutputTokens
|
state.InputTokens = usage.InputTokens
|
||||||
if evt.Response.Usage.InputTokensDetails != nil {
|
state.OutputTokens = usage.OutputTokens
|
||||||
state.CacheReadInputTokens = evt.Response.Usage.InputTokensDetails.CachedTokens
|
state.CacheReadInputTokens = usage.CacheReadInputTokens
|
||||||
}
|
|
||||||
}
|
}
|
||||||
switch evt.Response.Status {
|
switch evt.Response.Status {
|
||||||
case "incomplete":
|
case "incomplete":
|
||||||
|
|||||||
Reference in New Issue
Block a user