Merge pull request #4138 from fengshao1227/fix/read-tool-args-streaming

fix(apicompat): Read 工具参数实时流式发送,不再依赖 .done 事件
This commit is contained in:
Wesley Liddick
2026-07-13 14:14:08 +08:00
committed by GitHub
3 changed files with 90 additions and 11 deletions
@@ -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) {
@@ -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
}
@@ -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)
}