fix: handle mismatched XML tool call close tags from MiniMax

MiniMax returns <minimax:toolcall> but closes with </minimax:tool_call>
(underscore variant). The XML parser constructed the close tag from the
opening tag and failed to match. Now tries both :toolcall> and :tool_call>
variants as fallback.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-21 02:39:26 +09:00
parent db8bc53265
commit 62cb5c1d15
2 changed files with 47 additions and 0 deletions

View file

@ -1058,6 +1058,43 @@ Done.`
} }
} }
func TestExtractXMLToolCalls_MismatchedCloseTag(t *testing.T) {
// MiniMax uses <minimax:toolcall> but closes with </minimax:tool_call> (underscore)
text := `<minimax:toolcall>
<invoke name="readfile">
<parameter name="path">/home/user/project/pyproject.toml</parameter>
</invoke>
</minimax:tool_call>`
calls := extractXMLToolCalls(text)
if len(calls) != 1 {
t.Fatalf("expected 1 tool call, got %d", len(calls))
}
if calls[0].Name != "readfile" {
t.Errorf("Name = %q, want %q", calls[0].Name, "readfile")
}
if calls[0].Arguments["path"] != "/home/user/project/pyproject.toml" {
t.Errorf("Arguments[path] = %v, want pyproject.toml path", calls[0].Arguments["path"])
}
}
func TestStripXMLToolCalls_MismatchedCloseTag(t *testing.T) {
text := `今テスト走らせるね
<minimax:toolcall>
<invoke name="exec">
<parameter name="command">cd /home/user && pytest</parameter>
</invoke>
</minimax:tool_call>`
got := stripXMLToolCalls(text)
if strings.Contains(got, "toolcall") || strings.Contains(got, "tool_call") {
t.Errorf("should remove XML block, got %q", got)
}
if !strings.Contains(got, "今テスト走らせるね。") {
t.Errorf("should keep text before, got %q", got)
}
}
func TestStripXMLToolCalls_NoXML(t *testing.T) { func TestStripXMLToolCalls_NoXML(t *testing.T) {
text := "Just regular text." text := "Just regular text."
got := stripXMLToolCalls(text) got := stripXMLToolCalls(text)

View file

@ -86,6 +86,11 @@ func extractXMLToolCalls(text string) []ToolCall {
ns := remaining[tagStart+1 : idx] ns := remaining[tagStart+1 : idx]
closeTag := "</" + ns + ":toolcall>" closeTag := "</" + ns + ":toolcall>"
closeIdx := strings.Index(remaining, closeTag) closeIdx := strings.Index(remaining, closeTag)
// Fallback: some models use inconsistent close tags (e.g. tool_call vs toolcall)
if closeIdx == -1 {
closeTag = "</" + ns + ":tool_call>"
closeIdx = strings.Index(remaining, closeTag)
}
if closeIdx == -1 { if closeIdx == -1 {
break break
} }
@ -191,6 +196,11 @@ func stripXMLToolCalls(text string) string {
ns := text[tagStart+1 : idx] ns := text[tagStart+1 : idx]
closeTag := "</" + ns + ":toolcall>" closeTag := "</" + ns + ":toolcall>"
closeIdx := strings.Index(text, closeTag) closeIdx := strings.Index(text, closeTag)
// Fallback: some models use inconsistent close tags (e.g. tool_call vs toolcall)
if closeIdx == -1 {
closeTag = "</" + ns + ":tool_call>"
closeIdx = strings.Index(text, closeTag)
}
if closeIdx == -1 { if closeIdx == -1 {
return text return text
} }