fix(providers): strip XML tool call artifacts from response content

Minimax (and potentially other providers) embed raw XML like
<minimax:toolcall> in response Content alongside structured tool_calls.
Strip these before returning to prevent leaking internal data to users.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-20 16:03:48 +09:00
parent e67c4c3f27
commit c8d79c2355
2 changed files with 37 additions and 1 deletions

View file

@ -23,7 +23,14 @@ func NewHTTPProvider(apiKey, apiBase, proxy string) *HTTPProvider {
}
func (p *HTTPProvider) Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error) {
return p.delegate.Chat(ctx, messages, tools, model, options)
resp, err := p.delegate.Chat(ctx, messages, tools, model, options)
if err != nil {
return nil, err
}
// Strip provider-specific XML tool call artifacts (e.g. minimax)
// that leak into Content alongside structured tool_calls.
resp.Content = stripXMLToolCalls(resp.Content)
return resp, nil
}
func (p *HTTPProvider) GetDefaultModel() string {

View file

@ -56,6 +56,35 @@ func extractToolCallsFromText(text string) []ToolCall {
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.
func stripXMLToolCalls(text string) string {
// Match <vendor:toolcall>...</vendor:toolcall> blocks (any namespace prefix)
idx := strings.Index(text, ":toolcall>")
if idx == -1 {
return text
}
// Find the opening tag start: scan backwards for '<'
tagStart := strings.LastIndex(text[:idx], "<")
if tagStart == -1 {
return text
}
// Extract namespace (e.g. "minimax" from "<minimax:toolcall>")
ns := text[tagStart+1 : idx]
closeTag := "</" + ns + ":toolcall>"
closeIdx := strings.Index(text, closeTag)
if closeIdx == -1 {
return text
}
cleaned := text[:tagStart] + text[closeIdx+len(closeTag):]
// Recursively strip if there are more blocks
if strings.Contains(cleaned, ":toolcall>") {
cleaned = stripXMLToolCalls(cleaned)
}
return strings.TrimSpace(cleaned)
}
// stripToolCallsFromText removes tool call JSON from response text.
func stripToolCallsFromText(text string) string {
start := strings.Index(text, `{"tool_calls"`)