From f04605d84fcc4975c03ca51db90181e3fb004868 Mon Sep 17 00:00:00 2001 From: Luis Pater Date: Sat, 25 Jul 2026 14:35:27 +0800 Subject: [PATCH] fix(translator): prevent duplicate message delta processing in Claude response handler Closes: #4544 --- .../openai/claude/openai_claude_response.go | 2 +- .../claude/openai_claude_response_test.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/translator/openai/claude/openai_claude_response.go b/internal/translator/openai/claude/openai_claude_response.go index 47f3f389..bdd0dfc6 100644 --- a/internal/translator/openai/claude/openai_claude_response.go +++ b/internal/translator/openai/claude/openai_claude_response.go @@ -336,7 +336,7 @@ func convertOpenAIStreamingChunkToAnthropic(rawJSON []byte, param *ConvertOpenAI // Handle usage information separately (this comes in a later chunk) // Only process if usage has actual values (not null) - if param.FinishReason != "" { + if param.FinishReason != "" && !param.MessageDeltaSent { usage := root.Get("usage") var inputTokens, outputTokens, cachedTokens int64 if usage.Exists() && usage.Type != gjson.Null { diff --git a/internal/translator/openai/claude/openai_claude_response_test.go b/internal/translator/openai/claude/openai_claude_response_test.go index 35aa36f3..f5b063b2 100644 --- a/internal/translator/openai/claude/openai_claude_response_test.go +++ b/internal/translator/openai/claude/openai_claude_response_test.go @@ -103,6 +103,25 @@ func lastStopReason(events []sseEvent) string { const streamReq = `{"stream":true}` +func TestStreaming_LateUsageOnlyDoesNotEmitAfterMessageStop(t *testing.T) { + events := runStream(t, streamReq, + `{"id":"c1","model":"m","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}`, + `{"id":"c1","model":"m","choices":[{"index":0,"delta":{"content":"hello"},"finish_reason":null}]}`, + `{"id":"c1","model":"m","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":1}}`, + `{"id":"c1","model":"m","choices":[],"usage":{"prompt_tokens":1,"completion_tokens":1}}`, + ) + + if got := countByType(events, "message_delta"); got != 1 { + t.Fatalf("expected exactly one message_delta, got %d (events=%+v)", got, events) + } + if got := countByType(events, "message_stop"); got != 1 { + t.Fatalf("expected exactly one message_stop, got %d (events=%+v)", got, events) + } + if len(events) == 0 || events[len(events)-1].Type != "message_stop" { + t.Fatalf("message_stop must be the last semantic event (events=%+v)", events) + } +} + func TestConvertOpenAIResponseToClaude_StreamIgnoresNullToolNameDelta(t *testing.T) { originalRequest := []byte(streamReq) var param any -- 2.51.2