feat(providers): parse XML tool calls as fallback for minimax
When the provider returns no structured tool_calls but Content contains XML blocks like <minimax:toolcall>, extract and parse them into ToolCall structs. This handles cases where minimax returns tool calls only as XML. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c8d79c2355
commit
0e8d31c0f6
3 changed files with 211 additions and 2 deletions
|
|
@ -979,3 +979,89 @@ func TestFindMatchingBrace(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- XML tool call extract/strip tests ---
|
||||
|
||||
func TestExtractXMLToolCalls_Single(t *testing.T) {
|
||||
text := `<minimax:toolcall>
|
||||
<invoke name="exec">
|
||||
<parameter name="command">echo hello</parameter>
|
||||
</invoke>
|
||||
</minimax:toolcall>`
|
||||
|
||||
calls := extractXMLToolCalls(text)
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("expected 1 tool call, got %d", len(calls))
|
||||
}
|
||||
if calls[0].Name != "exec" {
|
||||
t.Errorf("Name = %q, want %q", calls[0].Name, "exec")
|
||||
}
|
||||
if calls[0].Arguments["command"] != "echo hello" {
|
||||
t.Errorf("Arguments[command] = %v, want %q", calls[0].Arguments["command"], "echo hello")
|
||||
}
|
||||
if calls[0].Function == nil || calls[0].Function.Name != "exec" {
|
||||
t.Errorf("Function.Name should be exec")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractXMLToolCalls_Multiple(t *testing.T) {
|
||||
text := `<minimax:toolcall>
|
||||
<invoke name="web_search">
|
||||
<parameter name="query">golang testing</parameter>
|
||||
</invoke>
|
||||
<invoke name="exec">
|
||||
<parameter name="command">go test ./...</parameter>
|
||||
<parameter name="timeout">30</parameter>
|
||||
</invoke>
|
||||
</minimax:toolcall>`
|
||||
|
||||
calls := extractXMLToolCalls(text)
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("expected 2 tool calls, got %d", len(calls))
|
||||
}
|
||||
if calls[0].Name != "web_search" {
|
||||
t.Errorf("[0].Name = %q, want %q", calls[0].Name, "web_search")
|
||||
}
|
||||
if calls[1].Name != "exec" {
|
||||
t.Errorf("[1].Name = %q, want %q", calls[1].Name, "exec")
|
||||
}
|
||||
if calls[1].Arguments["timeout"] != "30" {
|
||||
t.Errorf("[1].Arguments[timeout] = %v, want %q", calls[1].Arguments["timeout"], "30")
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractXMLToolCalls_NoXML(t *testing.T) {
|
||||
calls := extractXMLToolCalls("just regular text")
|
||||
if len(calls) != 0 {
|
||||
t.Errorf("expected 0 tool calls, got %d", len(calls))
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripXMLToolCalls(t *testing.T) {
|
||||
text := `Let me run that.
|
||||
<minimax:toolcall>
|
||||
<invoke name="exec">
|
||||
<parameter name="command">echo hello</parameter>
|
||||
</invoke>
|
||||
</minimax:toolcall>
|
||||
Done.`
|
||||
|
||||
got := stripXMLToolCalls(text)
|
||||
if strings.Contains(got, "toolcall") {
|
||||
t.Errorf("should remove XML block, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Let me run that.") {
|
||||
t.Errorf("should keep text before, got %q", got)
|
||||
}
|
||||
if !strings.Contains(got, "Done.") {
|
||||
t.Errorf("should keep text after, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripXMLToolCalls_NoXML(t *testing.T) {
|
||||
text := "Just regular text."
|
||||
got := stripXMLToolCalls(text)
|
||||
if got != text {
|
||||
t.Errorf("stripXMLToolCalls() = %q, want %q", got, text)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,8 +27,14 @@ func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []Too
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Strip provider-specific XML tool call artifacts (e.g. minimax)
|
||||
// that leak into Content alongside structured tool_calls.
|
||||
// If provider returned no structured tool_calls but Content has XML
|
||||
// tool call blocks (e.g. minimax), parse them as a fallback.
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
if xmlCalls := extractXMLToolCalls(resp.Content); len(xmlCalls) > 0 {
|
||||
resp.ToolCalls = xmlCalls
|
||||
}
|
||||
}
|
||||
// Strip XML tool call artifacts from Content regardless.
|
||||
resp.Content = stripXMLToolCalls(resp.Content)
|
||||
return resp, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package providers
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
|
@ -56,6 +57,122 @@ func extractToolCallsFromText(text string) []ToolCall {
|
|||
return result
|
||||
}
|
||||
|
||||
// extractXMLToolCalls parses XML tool call blocks (e.g. <minimax:toolcall>)
|
||||
// into structured ToolCall objects. Used as a fallback when the provider returns
|
||||
// tool calls as XML in Content but not in the structured tool_calls field.
|
||||
//
|
||||
// Expected format:
|
||||
//
|
||||
// <vendor:toolcall>
|
||||
// <invoke name="tool_name">
|
||||
// <parameter name="param">value</parameter>
|
||||
// </invoke>
|
||||
// </vendor:toolcall>
|
||||
func extractXMLToolCalls(text string) []ToolCall {
|
||||
var result []ToolCall
|
||||
remaining := text
|
||||
callIdx := 0
|
||||
|
||||
for {
|
||||
// Find next :toolcall> block
|
||||
idx := strings.Index(remaining, ":toolcall>")
|
||||
if idx == -1 {
|
||||
break
|
||||
}
|
||||
tagStart := strings.LastIndex(remaining[:idx], "<")
|
||||
if tagStart == -1 {
|
||||
break
|
||||
}
|
||||
ns := remaining[tagStart+1 : idx]
|
||||
closeTag := "</" + ns + ":toolcall>"
|
||||
closeIdx := strings.Index(remaining, closeTag)
|
||||
if closeIdx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
block := remaining[idx+len(":toolcall>") : closeIdx]
|
||||
remaining = remaining[closeIdx+len(closeTag):]
|
||||
|
||||
// Parse <invoke> elements within the block
|
||||
invokeRemaining := block
|
||||
for {
|
||||
invokeStart := strings.Index(invokeRemaining, "<invoke")
|
||||
if invokeStart == -1 {
|
||||
break
|
||||
}
|
||||
invokeEnd := strings.Index(invokeRemaining[invokeStart:], "</invoke>")
|
||||
if invokeEnd == -1 {
|
||||
break
|
||||
}
|
||||
invokeBody := invokeRemaining[invokeStart : invokeStart+invokeEnd+len("</invoke>")]
|
||||
invokeRemaining = invokeRemaining[invokeStart+invokeEnd+len("</invoke>"):]
|
||||
|
||||
// Extract tool name from <invoke name="...">
|
||||
nameStart := strings.Index(invokeBody, `name="`)
|
||||
if nameStart == -1 {
|
||||
continue
|
||||
}
|
||||
nameStart += len(`name="`)
|
||||
nameEnd := strings.Index(invokeBody[nameStart:], `"`)
|
||||
if nameEnd == -1 {
|
||||
continue
|
||||
}
|
||||
toolName := invokeBody[nameStart : nameStart+nameEnd]
|
||||
|
||||
// Extract parameters
|
||||
args := make(map[string]interface{})
|
||||
paramRemaining := invokeBody
|
||||
for {
|
||||
pStart := strings.Index(paramRemaining, "<parameter")
|
||||
if pStart == -1 {
|
||||
break
|
||||
}
|
||||
pNameStart := strings.Index(paramRemaining[pStart:], `name="`)
|
||||
if pNameStart == -1 {
|
||||
break
|
||||
}
|
||||
pNameStart += pStart + len(`name="`)
|
||||
pNameEnd := strings.Index(paramRemaining[pNameStart:], `"`)
|
||||
if pNameEnd == -1 {
|
||||
break
|
||||
}
|
||||
paramName := paramRemaining[pNameStart : pNameStart+pNameEnd]
|
||||
|
||||
// Find closing > of the <parameter ...> tag
|
||||
tagClose := strings.Index(paramRemaining[pNameStart:], ">")
|
||||
if tagClose == -1 {
|
||||
break
|
||||
}
|
||||
valueStart := pNameStart + tagClose + 1
|
||||
valueEnd := strings.Index(paramRemaining[valueStart:], "</parameter>")
|
||||
if valueEnd == -1 {
|
||||
break
|
||||
}
|
||||
paramValue := paramRemaining[valueStart : valueStart+valueEnd]
|
||||
args[paramName] = paramValue
|
||||
paramRemaining = paramRemaining[valueStart+valueEnd+len("</parameter>"):]
|
||||
}
|
||||
|
||||
// Build Arguments JSON string
|
||||
argsJSON, _ := json.Marshal(args)
|
||||
|
||||
callIdx++
|
||||
result = append(result, ToolCall{
|
||||
ID: fmt.Sprintf("xmltc_%d", callIdx),
|
||||
Type: "function",
|
||||
Name: toolName,
|
||||
Arguments: args,
|
||||
Function: &FunctionCall{
|
||||
Name: toolName,
|
||||
Arguments: string(argsJSON),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// stripXMLToolCalls removes XML tool call blocks (e.g. <minimax:toolcall>...</minimax:toolcall>)
|
||||
// from response text. Some providers embed raw XML tool calls in Content alongside
|
||||
// structured tool_calls; this prevents them from leaking to users.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue