From a5d40c9845b06519c62c5d1518beac0fa3f58353 Mon Sep 17 00:00:00 2001 From: li Date: Mon, 13 Jul 2026 11:47:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(apicompat):=20Read=20=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E5=AE=9E=E6=97=B6=E6=B5=81=E5=BC=8F=E5=8F=91?= =?UTF-8?q?=E9=80=81=EF=BC=8C=E4=B8=8D=E5=86=8D=E4=BE=9D=E8=B5=96=20.done?= =?UTF-8?q?=20=E4=BA=8B=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #4114 --- .../pkg/apicompat/anthropic_responses_test.go | 13 ++- .../pkg/apicompat/responses_to_anthropic.go | 4 - .../responses_to_anthropic_read_tool_test.go | 84 +++++++++++++++++++ 3 files changed, 90 insertions(+), 11 deletions(-) create mode 100644 backend/internal/pkg/apicompat/responses_to_anthropic_read_tool_test.go diff --git a/backend/internal/pkg/apicompat/anthropic_responses_test.go b/backend/internal/pkg/apicompat/anthropic_responses_test.go index 8997835c2a..db6b49aa9b 100644 --- a/backend/internal/pkg/apicompat/anthropic_responses_test.go +++ b/backend/internal/pkg/apicompat/anthropic_responses_test.go @@ -718,7 +718,7 @@ func TestStreamingToolCallDoneWithoutDeltaEmitsArguments(t *testing.T) { assert.Equal(t, "content_block_stop", events[1].Type) } -func TestStreamingReadToolDropsEmptyPages(t *testing.T) { +func TestStreamingReadToolStreamsDeltas(t *testing.T) { state := NewResponsesEventToAnthropicState() ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{ @@ -739,18 +739,17 @@ func TestStreamingReadToolDropsEmptyPages(t *testing.T) { OutputIndex: 0, Delta: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`, }, state) - assert.Len(t, events, 0) + require.Len(t, events, 1, "Read tool deltas must be streamed like any other tool") + assert.Equal(t, "content_block_delta", events[0].Type) + assert.Equal(t, "input_json_delta", events[0].Delta.Type) events = ResponsesEventToAnthropicEvents(&ResponsesStreamEvent{ Type: "response.function_call_arguments.done", OutputIndex: 0, Arguments: `{"file_path":"/tmp/demo.py","limit":2000,"offset":0,"pages":""}`, }, state) - require.Len(t, events, 2) - assert.Equal(t, "content_block_delta", events[0].Type) - assert.Equal(t, "input_json_delta", events[0].Delta.Type) - assert.JSONEq(t, `{"file_path":"/tmp/demo.py","limit":2000,"offset":0}`, events[0].Delta.PartialJSON) - assert.Equal(t, "content_block_stop", events[1].Type) + require.Len(t, events, 1, "after streaming deltas, .done should just close the block") + assert.Equal(t, "content_block_stop", events[0].Type) } func TestStreamingReasoning(t *testing.T) { diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic.go b/backend/internal/pkg/apicompat/responses_to_anthropic.go index 9c3b85b2ef..376f0d97da 100644 --- a/backend/internal/pkg/apicompat/responses_to_anthropic.go +++ b/backend/internal/pkg/apicompat/responses_to_anthropic.go @@ -413,10 +413,6 @@ func resToAnthHandleFuncArgsDelta(evt *ResponsesStreamEvent, state *ResponsesEve return nil } - if state.CurrentBlockType == "tool_use" && state.CurrentToolName == "Read" { - state.CurrentToolArgs += evt.Delta - return nil - } if state.CurrentBlockType == "tool_use" { state.CurrentToolHadDelta = true } diff --git a/backend/internal/pkg/apicompat/responses_to_anthropic_read_tool_test.go b/backend/internal/pkg/apicompat/responses_to_anthropic_read_tool_test.go new file mode 100644 index 0000000000..72b60099fe --- /dev/null +++ b/backend/internal/pkg/apicompat/responses_to_anthropic_read_tool_test.go @@ -0,0 +1,84 @@ +package apicompat + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestResToAnthFuncArgsDelta_ReadToolStreamsDeltas(t *testing.T) { + state := NewResponsesEventToAnthropicState() + state.MessageStartSent = true + state.CurrentBlockType = "tool_use" + state.CurrentToolName = "Read" + state.OutputIndexToBlockIdx = map[int]int{0: 0} + + evt := &ResponsesStreamEvent{ + Type: "response.function_call_arguments.delta", + OutputIndex: 0, + Delta: `{"file_path":"/tmp/test.go"}`, + } + + events := ResponsesEventToAnthropicEvents(evt, state) + + require.Len(t, events, 1, "Read tool delta must produce content_block_delta") + assert.Equal(t, "content_block_delta", events[0].Type) + assert.Equal(t, "input_json_delta", events[0].Delta.Type) + assert.Equal(t, `{"file_path":"/tmp/test.go"}`, events[0].Delta.PartialJSON) + assert.True(t, state.CurrentToolHadDelta, "Read deltas should set CurrentToolHadDelta") +} + +func TestResToAnthFuncArgsDelta_ReadToolWithoutDone(t *testing.T) { + state := NewResponsesEventToAnthropicState() + state.MessageStartSent = true + state.ContentBlockIndex = 0 + state.ContentBlockOpen = true + state.CurrentBlockType = "tool_use" + state.CurrentToolName = "Read" + state.OutputIndexToBlockIdx = map[int]int{0: 0} + + delta := &ResponsesStreamEvent{ + Type: "response.function_call_arguments.delta", + OutputIndex: 0, + Delta: `{"file_path":"/tmp/test.go"}`, + } + events := ResponsesEventToAnthropicEvents(delta, state) + require.Len(t, events, 1, "delta should be streamed") + + completed := &ResponsesStreamEvent{ + Type: "response.completed", + Response: &ResponsesResponse{ + Status: "completed", + }, + } + events = ResponsesEventToAnthropicEvents(completed, state) + + hasStop := false + for _, e := range events { + if e.Type == "content_block_stop" { + hasStop = true + } + } + assert.True(t, hasStop, "block should be closed even without .done event") +} + +func TestResToAnthFuncArgsDelta_NonReadToolUnchanged(t *testing.T) { + state := NewResponsesEventToAnthropicState() + state.MessageStartSent = true + state.CurrentBlockType = "tool_use" + state.CurrentToolName = "Write" + state.OutputIndexToBlockIdx = map[int]int{0: 0} + + evt := &ResponsesStreamEvent{ + Type: "response.function_call_arguments.delta", + OutputIndex: 0, + Delta: `{"file_path":"/tmp/out.txt","content":"hello"}`, + } + + events := ResponsesEventToAnthropicEvents(evt, state) + + require.Len(t, events, 1) + assert.Equal(t, "content_block_delta", events[0].Type) + assert.True(t, state.CurrentToolHadDelta) +}