Address PR review: fenced-stream guard and multi ANTML calls

This commit is contained in:
CJACK.
2026-03-07 17:45:43 +08:00
parent 25e40cc3a6
commit 0ce3fd22a7
4 changed files with 71 additions and 4 deletions

View File

@@ -357,3 +357,36 @@ func TestHandleClaudeStreamRealtimeToolSafetyAcrossStructuredFormats(t *testing.
})
}
}
func TestHandleClaudeStreamRealtimeDoesNotStopOnUnclosedFencedToolExample(t *testing.T) {
h := &Handler{}
resp := makeClaudeSSEHTTPResponse(
"data: {\"p\":\"response/content\",\"v\":\"Here is an example:\\n```json\\n{\\\"tool_calls\\\":[{\\\"name\\\":\\\"Bash\\\",\\\"input\\\":{\\\"command\\\":\\\"pwd\\\"}}]}\"}",
"data: {\"p\":\"response/content\",\"v\":\"\\n```\\nDo not execute it.\"}",
`data: [DONE]`,
)
rec := httptest.NewRecorder()
req := httptest.NewRequest(http.MethodPost, "/anthropic/v1/messages", nil)
h.handleClaudeStreamRealtime(rec, req, resp, "claude-sonnet-4-5", []any{map[string]any{"role": "user", "content": "show example only"}}, false, false, []string{"Bash"})
frames := parseClaudeFrames(t, rec.Body.String())
for _, f := range findClaudeFrames(frames, "content_block_start") {
contentBlock, _ := f.Payload["content_block"].(map[string]any)
if contentBlock["type"] == "tool_use" {
t.Fatalf("unexpected tool_use for fenced example, body=%s", rec.Body.String())
}
}
foundEndTurn := false
for _, f := range findClaudeFrames(frames, "message_delta") {
delta, _ := f.Payload["delta"].(map[string]any)
if delta["stop_reason"] == "end_turn" {
foundEndTurn = true
break
}
}
if !foundEndTurn {
t.Fatalf("expected stop_reason=end_turn, body=%s", rec.Body.String())
}
}

View File

@@ -117,6 +117,9 @@ func (s *claudeStreamRuntime) onParsed(parsed sse.LineResult) streamengine.Parse
s.text.WriteString(p.Text)
if s.bufferToolContent {
if hasUnclosedCodeFence(s.text.String()) {
continue
}
detected := util.ParseToolCalls(s.text.String(), s.toolNames)
if len(detected) > 0 {
s.finalize("tool_use")
@@ -154,3 +157,7 @@ func (s *claudeStreamRuntime) onParsed(parsed sse.LineResult) streamengine.Parse
return streamengine.ParsedDecision{ContentSeen: contentSeen}
}
func hasUnclosedCodeFence(text string) bool {
return strings.Count(text, "```")%2 == 1
}

View File

@@ -32,8 +32,8 @@ func parseXMLToolCalls(text string) []ParsedToolCall {
if call, ok := parseFunctionCallTagStyle(text); ok {
return []ParsedToolCall{call}
}
if call, ok := parseAntmlFunctionCallStyle(text); ok {
return []ParsedToolCall{call}
if calls := parseAntmlFunctionCallStyles(text); len(calls) > 0 {
return calls
}
if call, ok := parseInvokeFunctionCallStyle(text); ok {
return []ParsedToolCall{call}
@@ -140,8 +140,24 @@ func parseFunctionCallTagStyle(text string) (ParsedToolCall, bool) {
return ParsedToolCall{Name: name, Input: input}, true
}
func parseAntmlFunctionCallStyle(text string) (ParsedToolCall, bool) {
m := antmlFunctionCallPattern.FindStringSubmatch(text)
func parseAntmlFunctionCallStyles(text string) []ParsedToolCall {
matches := antmlFunctionCallPattern.FindAllStringSubmatch(text, -1)
if len(matches) == 0 {
return nil
}
out := make([]ParsedToolCall, 0, len(matches))
for _, m := range matches {
if call, ok := parseSingleAntmlFunctionCallMatch(m); ok {
out = append(out, call)
}
}
if len(out) == 0 {
return nil
}
return out
}
func parseSingleAntmlFunctionCallMatch(m []string) (ParsedToolCall, bool) {
if len(m) < 3 {
return ParsedToolCall{}, false
}

View File

@@ -260,3 +260,14 @@ func TestParseToolCallsSupportsAntmlFunctionAttributeWithParametersTag(t *testin
t.Fatalf("expected command argument, got %#v", calls[0].Input)
}
}
func TestParseToolCallsSupportsMultipleAntmlFunctionCalls(t *testing.T) {
text := `<antml:function_calls><antml:function_call id="1" function="Bash"><antml:parameters>{"command":"pwd"}</antml:parameters></antml:function_call><antml:function_call id="2" function="Read"><antml:parameters>{"file_path":"README.md"}</antml:parameters></antml:function_call></antml:function_calls>`
calls := ParseToolCalls(text, []string{"bash", "read"})
if len(calls) != 2 {
t.Fatalf("expected 2 calls, got %#v", calls)
}
if calls[0].Name != "bash" || calls[1].Name != "read" {
t.Fatalf("expected canonical names [bash read], got %#v", calls)
}
}