Add MiniMax-M2 tool call support (add `PARSER minimaxm2` in Modelfile to enable)
This commit is contained in:
parent
18fdcc94e5
commit
a3bcb60738
|
|
@ -0,0 +1,530 @@
|
|||
package parsers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"github.com/ollama/ollama/api"
|
||||
"github.com/ollama/ollama/logutil"
|
||||
)
|
||||
|
||||
// MiniMaxM2Parser handles MiniMax-M2's XML-style tool call format:
|
||||
// <minimax:tool_call>
|
||||
// <invoke name="function_name">
|
||||
// <parameter name="key">value</parameter>
|
||||
// </invoke>
|
||||
// </minimax:tool_call>
|
||||
//
|
||||
// And thinking format:
|
||||
// <think>thinking content</think>
|
||||
type MiniMaxM2Parser struct {
|
||||
state MiniMaxM2ParserState
|
||||
buffer strings.Builder
|
||||
tools []api.Tool
|
||||
err error // Store critical errors (like unknown tools)
|
||||
}
|
||||
|
||||
type MiniMaxM2ParserState int
|
||||
|
||||
const (
|
||||
MiniMaxM2CollectingContent MiniMaxM2ParserState = iota
|
||||
MiniMaxM2CollectingThinking
|
||||
MiniMaxM2CollectingToolCalls
|
||||
)
|
||||
|
||||
const (
|
||||
minimaxm2ThinkingOpenTag = "<think>"
|
||||
minimaxm2ThinkingCloseTag = "</think>"
|
||||
minimaxm2ToolCallOpenTag = "<minimax:tool_call>"
|
||||
minimaxm2ToolCallCloseTag = "</minimax:tool_call>"
|
||||
minimaxm2InvokeOpenPrefix = "<invoke"
|
||||
minimaxm2InvokeCloseTag = "</invoke>"
|
||||
minimaxm2ParameterOpenPrefix = "<parameter"
|
||||
minimaxm2ParameterCloseTag = "</parameter>"
|
||||
)
|
||||
|
||||
func (p *MiniMaxM2Parser) HasToolSupport() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) HasThinkingSupport() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) setInitialState(lastMessage *api.Message, tools []api.Tool, thinkValue *api.ThinkValue) {
|
||||
prefill := lastMessage != nil && lastMessage.Role == "assistant"
|
||||
|
||||
// Check both model capability AND request preference
|
||||
thinkingEnabled := thinkValue != nil && thinkValue.Bool()
|
||||
|
||||
// If tools are present, we don't start in thinking mode
|
||||
if len(tools) > 0 {
|
||||
p.state = MiniMaxM2CollectingContent
|
||||
return
|
||||
}
|
||||
|
||||
if !thinkingEnabled {
|
||||
p.state = MiniMaxM2CollectingContent
|
||||
return
|
||||
}
|
||||
|
||||
if prefill && lastMessage.Content != "" {
|
||||
p.state = MiniMaxM2CollectingContent
|
||||
return
|
||||
}
|
||||
|
||||
p.state = MiniMaxM2CollectingThinking
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) Init(tools []api.Tool, lastMessage *api.Message, thinkValue *api.ThinkValue) []api.Tool {
|
||||
p.tools = tools
|
||||
p.err = nil
|
||||
p.setInitialState(lastMessage, tools, thinkValue)
|
||||
return tools
|
||||
}
|
||||
|
||||
// Event types
|
||||
type minimaxm2Event interface {
|
||||
isMiniMaxM2Event()
|
||||
}
|
||||
|
||||
type minimaxm2EventContent struct {
|
||||
content string
|
||||
}
|
||||
|
||||
type minimaxm2EventThinkingContent struct {
|
||||
content string
|
||||
}
|
||||
|
||||
type minimaxm2EventToolCall struct {
|
||||
toolCall api.ToolCall
|
||||
}
|
||||
|
||||
func (minimaxm2EventContent) isMiniMaxM2Event() {}
|
||||
func (minimaxm2EventThinkingContent) isMiniMaxM2Event() {}
|
||||
func (minimaxm2EventToolCall) isMiniMaxM2Event() {}
|
||||
|
||||
func (p *MiniMaxM2Parser) Add(s string, done bool) (content string, thinking string, calls []api.ToolCall, err error) {
|
||||
p.buffer.WriteString(s)
|
||||
events := p.parseEvents()
|
||||
|
||||
// Check for critical errors
|
||||
if p.err != nil {
|
||||
return "", "", nil, p.err
|
||||
}
|
||||
|
||||
var toolCalls []api.ToolCall
|
||||
var contentSb strings.Builder
|
||||
var thinkingSb strings.Builder
|
||||
for _, event := range events {
|
||||
switch event := event.(type) {
|
||||
case minimaxm2EventToolCall:
|
||||
toolCalls = append(toolCalls, event.toolCall)
|
||||
case minimaxm2EventThinkingContent:
|
||||
thinkingSb.WriteString(event.content)
|
||||
case minimaxm2EventContent:
|
||||
contentSb.WriteString(event.content)
|
||||
}
|
||||
}
|
||||
|
||||
return contentSb.String(), thinkingSb.String(), toolCalls, nil
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) parseEvents() []minimaxm2Event {
|
||||
var all []minimaxm2Event
|
||||
|
||||
keepLooping := true
|
||||
for keepLooping && p.err == nil {
|
||||
var events []minimaxm2Event
|
||||
events, keepLooping = p.eat()
|
||||
if len(events) > 0 {
|
||||
all = append(all, events...)
|
||||
}
|
||||
}
|
||||
|
||||
return all
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) eat() ([]minimaxm2Event, bool) {
|
||||
var events []minimaxm2Event
|
||||
bufStr := p.buffer.String()
|
||||
if bufStr == "" {
|
||||
return events, false
|
||||
}
|
||||
|
||||
switch p.state {
|
||||
case MiniMaxM2CollectingThinking:
|
||||
if strings.Contains(bufStr, minimaxm2ThinkingCloseTag) {
|
||||
// thinking[</think>] -> content
|
||||
split := strings.SplitN(bufStr, minimaxm2ThinkingCloseTag, 2)
|
||||
thinking := split[0]
|
||||
thinking = strings.TrimRightFunc(thinking, unicode.IsSpace)
|
||||
|
||||
remaining := split[1]
|
||||
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(remaining)
|
||||
p.state = MiniMaxM2CollectingContent
|
||||
|
||||
if len(thinking) > 0 {
|
||||
events = append(events, minimaxm2EventThinkingContent{content: thinking})
|
||||
}
|
||||
return events, true
|
||||
} else if overlapLen := overlap(bufStr, minimaxm2ThinkingCloseTag); overlapLen > 0 {
|
||||
// partial </think>
|
||||
beforePartialTag := bufStr[:len(bufStr)-overlapLen]
|
||||
trailingLen := trailingWhitespaceLen(beforePartialTag)
|
||||
ambiguousStart := len(beforePartialTag) - trailingLen
|
||||
|
||||
unambiguous := bufStr[:ambiguousStart]
|
||||
ambiguous := bufStr[ambiguousStart:]
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(ambiguous)
|
||||
if len(unambiguous) > 0 {
|
||||
events = append(events, minimaxm2EventThinkingContent{content: unambiguous})
|
||||
}
|
||||
return events, false
|
||||
} else {
|
||||
// otherwise it's thinking content
|
||||
whitespaceLen := trailingWhitespaceLen(bufStr)
|
||||
ambiguousStart := len(bufStr) - whitespaceLen
|
||||
|
||||
unambiguous := bufStr[:ambiguousStart]
|
||||
ambiguous := bufStr[ambiguousStart:]
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(ambiguous)
|
||||
if len(unambiguous) > 0 {
|
||||
events = append(events, minimaxm2EventThinkingContent{content: unambiguous})
|
||||
}
|
||||
return events, false
|
||||
}
|
||||
|
||||
case MiniMaxM2CollectingContent:
|
||||
// Check which tag appears first
|
||||
toolCallIdx := strings.Index(bufStr, minimaxm2ToolCallOpenTag)
|
||||
thinkIdx := strings.Index(bufStr, minimaxm2ThinkingOpenTag)
|
||||
|
||||
// Determine which tag comes first
|
||||
var tagIdx int
|
||||
var tagName string
|
||||
var nextState MiniMaxM2ParserState
|
||||
|
||||
if toolCallIdx >= 0 && (thinkIdx < 0 || toolCallIdx < thinkIdx) {
|
||||
tagIdx = toolCallIdx
|
||||
tagName = minimaxm2ToolCallOpenTag
|
||||
nextState = MiniMaxM2CollectingToolCalls
|
||||
} else if thinkIdx >= 0 {
|
||||
tagIdx = thinkIdx
|
||||
tagName = minimaxm2ThinkingOpenTag
|
||||
nextState = MiniMaxM2CollectingThinking
|
||||
} else {
|
||||
tagIdx = -1
|
||||
}
|
||||
|
||||
if tagIdx >= 0 {
|
||||
// Found a tag - emit content before it
|
||||
before := bufStr[:tagIdx]
|
||||
if before != "" {
|
||||
events = append(events, minimaxm2EventContent{content: before})
|
||||
}
|
||||
|
||||
// Move past the tag
|
||||
remaining := bufStr[tagIdx+len(tagName):]
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(remaining)
|
||||
p.state = nextState
|
||||
|
||||
logutil.Trace("minimaxm2: found tag", "tag", tagName, "before", before)
|
||||
return events, true
|
||||
}
|
||||
|
||||
// No complete tag found - check for partial tags at end
|
||||
toolCallOverlap := overlap(bufStr, minimaxm2ToolCallOpenTag)
|
||||
thinkingOverlap := overlap(bufStr, minimaxm2ThinkingOpenTag)
|
||||
maxOverlap := max(toolCallOverlap, thinkingOverlap)
|
||||
|
||||
if maxOverlap > 0 {
|
||||
// Hold back potential partial tag
|
||||
content := bufStr[:len(bufStr)-maxOverlap]
|
||||
if content != "" {
|
||||
events = append(events, minimaxm2EventContent{content: content})
|
||||
}
|
||||
// Keep the potential partial tag in buffer
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(bufStr[len(bufStr)-maxOverlap:])
|
||||
logutil.Trace("minimaxm2: holding potential partial tag", "overlap", maxOverlap)
|
||||
return events, false
|
||||
}
|
||||
|
||||
// No partial tag - emit everything
|
||||
if bufStr != "" {
|
||||
events = append(events, minimaxm2EventContent{content: bufStr})
|
||||
p.buffer.Reset()
|
||||
}
|
||||
return events, false
|
||||
|
||||
case MiniMaxM2CollectingToolCalls:
|
||||
if strings.Contains(bufStr, minimaxm2ToolCallCloseTag) {
|
||||
// Found closing tag - parse all tool calls in the block
|
||||
split := strings.SplitN(bufStr, minimaxm2ToolCallCloseTag, 2)
|
||||
toolCallBlock := split[0]
|
||||
remaining := split[1]
|
||||
remaining = strings.TrimLeftFunc(remaining, unicode.IsSpace)
|
||||
|
||||
p.buffer.Reset()
|
||||
p.buffer.WriteString(remaining)
|
||||
p.state = MiniMaxM2CollectingContent
|
||||
|
||||
// Parse all <invoke> blocks within this tool call block
|
||||
toolCalls, errs := p.parseToolCallBlock(toolCallBlock)
|
||||
// If there were critical errors (unknown tools), don't emit any events
|
||||
// This allows errors to propagate properly
|
||||
if len(errs) == 0 {
|
||||
for _, tc := range toolCalls {
|
||||
events = append(events, minimaxm2EventToolCall{toolCall: tc})
|
||||
}
|
||||
} else {
|
||||
// Log warnings for non-critical errors, but still emit successful tool calls
|
||||
for _, err := range errs {
|
||||
slog.Warn("minimaxm2 tool call parsing failed", "error", err)
|
||||
}
|
||||
// Only emit successfully parsed tool calls
|
||||
for _, tc := range toolCalls {
|
||||
events = append(events, minimaxm2EventToolCall{toolCall: tc})
|
||||
}
|
||||
}
|
||||
|
||||
return events, true
|
||||
}
|
||||
|
||||
// Check for partial closing tag
|
||||
if overlapLen := overlap(bufStr, minimaxm2ToolCallCloseTag); overlapLen > 0 {
|
||||
// Hold back potential partial closing tag
|
||||
return events, false
|
||||
}
|
||||
|
||||
// Still collecting tool call content
|
||||
return events, false
|
||||
}
|
||||
|
||||
return events, false
|
||||
}
|
||||
|
||||
// parseToolCallBlock extracts all <invoke> blocks from a <minimax:tool_call> content
|
||||
func (p *MiniMaxM2Parser) parseToolCallBlock(content string) ([]api.ToolCall, []error) {
|
||||
var toolCalls []api.ToolCall
|
||||
var errors []error
|
||||
|
||||
remaining := content
|
||||
for {
|
||||
// Find next <invoke> tag
|
||||
invokeStartIdx := strings.Index(remaining, minimaxm2InvokeOpenPrefix)
|
||||
if invokeStartIdx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Find the end of the opening <invoke> tag (the '>')
|
||||
tagEndIdx := strings.Index(remaining[invokeStartIdx:], ">")
|
||||
if tagEndIdx == -1 {
|
||||
logutil.Trace("minimaxm2: incomplete invoke opening tag")
|
||||
break
|
||||
}
|
||||
tagEndIdx += invokeStartIdx
|
||||
|
||||
// Extract the opening tag to get the name attribute
|
||||
openingTag := remaining[invokeStartIdx : tagEndIdx+1]
|
||||
functionName := extractNameAttribute(openingTag)
|
||||
if functionName == "" {
|
||||
err := fmt.Errorf("invoke tag missing name attribute: %s", openingTag)
|
||||
slog.Warn("minimaxm2: invoke tag missing name attribute", "tag", openingTag)
|
||||
errors = append(errors, err)
|
||||
remaining = remaining[tagEndIdx+1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the closing </invoke> tag
|
||||
invokeEndIdx := strings.Index(remaining[tagEndIdx+1:], minimaxm2InvokeCloseTag)
|
||||
if invokeEndIdx == -1 {
|
||||
// Missing closing tag - conservative recovery
|
||||
slog.Warn("minimaxm2: missing </invoke> tag, recovering", "function", functionName)
|
||||
// Try to parse what we have up to the end
|
||||
invokeContent := remaining[tagEndIdx+1:]
|
||||
toolCall, err := p.parseInvoke(functionName, invokeContent)
|
||||
if err != nil {
|
||||
errors = append(errors, err)
|
||||
} else {
|
||||
toolCalls = append(toolCalls, toolCall)
|
||||
}
|
||||
break
|
||||
}
|
||||
invokeEndIdx += tagEndIdx + 1
|
||||
|
||||
// Extract the content between <invoke> and </invoke>
|
||||
invokeContent := remaining[tagEndIdx+1 : invokeEndIdx]
|
||||
|
||||
// Parse this invoke
|
||||
toolCall, err := p.parseInvoke(functionName, invokeContent)
|
||||
if err != nil {
|
||||
slog.Warn("minimaxm2: failed to parse invoke", "function", functionName, "error", err)
|
||||
errors = append(errors, err)
|
||||
} else {
|
||||
toolCalls = append(toolCalls, toolCall)
|
||||
logutil.Trace("minimaxm2: parsed tool call", "name", functionName, "args", toolCall.Function.Arguments)
|
||||
}
|
||||
|
||||
// Move past this invoke
|
||||
remaining = remaining[invokeEndIdx+len(minimaxm2InvokeCloseTag):]
|
||||
}
|
||||
|
||||
return toolCalls, errors
|
||||
}
|
||||
|
||||
// parseInvoke extracts the function name and parameters from an <invoke> block
|
||||
func (p *MiniMaxM2Parser) parseInvoke(functionName string, content string) (api.ToolCall, error) {
|
||||
// Validate tool exists
|
||||
tool := p.findToolByName(functionName)
|
||||
if tool == nil {
|
||||
availableTools := make([]string, len(p.tools))
|
||||
for i, t := range p.tools {
|
||||
availableTools[i] = t.Function.Name
|
||||
}
|
||||
// Store critical error to halt processing
|
||||
p.err = fmt.Errorf("model called unknown tool %q - available tools: %v (ensure tools are provided in API request)", functionName, availableTools)
|
||||
slog.Error("MiniMaxM2 model attempted to call unregistered tool",
|
||||
"tool", functionName,
|
||||
"available_tools", availableTools,
|
||||
"recommendation", "ensure tools array includes this tool in API request")
|
||||
return api.ToolCall{}, p.err
|
||||
}
|
||||
|
||||
// Extract all <parameter> tags
|
||||
params := make(map[string]any)
|
||||
remaining := content
|
||||
|
||||
for {
|
||||
// Find next <parameter> tag
|
||||
paramStartIdx := strings.Index(remaining, minimaxm2ParameterOpenPrefix)
|
||||
if paramStartIdx == -1 {
|
||||
break
|
||||
}
|
||||
|
||||
// Find the end of the opening <parameter> tag (the '>')
|
||||
tagEndIdx := strings.Index(remaining[paramStartIdx:], ">")
|
||||
if tagEndIdx == -1 {
|
||||
logutil.Trace("minimaxm2: incomplete parameter opening tag")
|
||||
break
|
||||
}
|
||||
tagEndIdx += paramStartIdx
|
||||
|
||||
// Extract the opening tag to get the name attribute
|
||||
openingTag := remaining[paramStartIdx : tagEndIdx+1]
|
||||
paramName := extractNameAttribute(openingTag)
|
||||
if paramName == "" {
|
||||
slog.Warn("minimaxm2: parameter tag missing name attribute", "tag", openingTag)
|
||||
remaining = remaining[tagEndIdx+1:]
|
||||
continue
|
||||
}
|
||||
|
||||
// Find the closing </parameter> tag
|
||||
paramEndIdx := strings.Index(remaining[tagEndIdx+1:], minimaxm2ParameterCloseTag)
|
||||
if paramEndIdx == -1 {
|
||||
// Missing closing tag - conservative recovery
|
||||
slog.Warn("minimaxm2: missing </parameter> tag for parameter, recovering", "parameter", paramName)
|
||||
// Take everything until the next parameter or end of content
|
||||
nextParamIdx := strings.Index(remaining[tagEndIdx+1:], minimaxm2ParameterOpenPrefix)
|
||||
if nextParamIdx == -1 {
|
||||
// No more parameters, take rest of content
|
||||
paramValue := strings.TrimSpace(remaining[tagEndIdx+1:])
|
||||
params[paramName] = parseParameterValue(paramValue)
|
||||
break
|
||||
} else {
|
||||
// Take up to next parameter
|
||||
paramValue := strings.TrimSpace(remaining[tagEndIdx+1 : tagEndIdx+1+nextParamIdx])
|
||||
params[paramName] = parseParameterValue(paramValue)
|
||||
remaining = remaining[tagEndIdx+1+nextParamIdx:]
|
||||
continue
|
||||
}
|
||||
}
|
||||
paramEndIdx += tagEndIdx + 1
|
||||
|
||||
// Extract the parameter value
|
||||
paramValue := strings.TrimSpace(remaining[tagEndIdx+1 : paramEndIdx])
|
||||
params[paramName] = parseParameterValue(paramValue)
|
||||
|
||||
logutil.Trace("minimaxm2: parsed parameter", "name", paramName, "value", params[paramName])
|
||||
|
||||
// Move past this parameter
|
||||
remaining = remaining[paramEndIdx+len(minimaxm2ParameterCloseTag):]
|
||||
}
|
||||
|
||||
return api.ToolCall{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: tool.Function.Name,
|
||||
Arguments: params,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// extractNameAttribute extracts the value of name="..." from an opening tag
|
||||
// Handles: <invoke name="function_name"> or <parameter name="param_name">
|
||||
func extractNameAttribute(tag string) string {
|
||||
// Look for name="..."
|
||||
nameAttrIdx := strings.Index(tag, `name="`)
|
||||
if nameAttrIdx == -1 {
|
||||
// Try with single quotes: name='...'
|
||||
nameAttrIdx = strings.Index(tag, `name='`)
|
||||
if nameAttrIdx == -1 {
|
||||
return ""
|
||||
}
|
||||
// Extract with single quotes
|
||||
start := nameAttrIdx + len(`name='`)
|
||||
end := strings.Index(tag[start:], `'`)
|
||||
if end == -1 {
|
||||
return ""
|
||||
}
|
||||
return tag[start : start+end]
|
||||
}
|
||||
|
||||
// Extract with double quotes
|
||||
start := nameAttrIdx + len(`name="`)
|
||||
end := strings.Index(tag[start:], `"`)
|
||||
if end == -1 {
|
||||
return ""
|
||||
}
|
||||
return tag[start : start+end]
|
||||
}
|
||||
|
||||
// parseParameterValue attempts to parse value as JSON, falls back to string
|
||||
func parseParameterValue(value string) any {
|
||||
// Remove leading and trailing newlines
|
||||
value = strings.TrimPrefix(value, "\n")
|
||||
value = strings.TrimSuffix(value, "\n")
|
||||
|
||||
// Check for null
|
||||
if strings.ToLower(value) == "null" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Try to parse as JSON (handles objects, arrays, numbers, booleans)
|
||||
var jsonValue any
|
||||
if err := json.Unmarshal([]byte(value), &jsonValue); err == nil {
|
||||
return jsonValue
|
||||
}
|
||||
|
||||
// Fallback to string
|
||||
return value
|
||||
}
|
||||
|
||||
func (p *MiniMaxM2Parser) findToolByName(name string) *api.Tool {
|
||||
name = strings.TrimSpace(name)
|
||||
for i := range p.tools {
|
||||
if p.tools[i].Function.Name == name {
|
||||
return &p.tools[i]
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,758 @@
|
|||
package parsers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/ollama/ollama/api"
|
||||
)
|
||||
|
||||
func TestMiniMaxM2Parser(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "get_weather",
|
||||
Description: "Get the weather for a location",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{
|
||||
"location": {
|
||||
Type: api.PropertyType{"string"},
|
||||
},
|
||||
"unit": {
|
||||
Type: api.PropertyType{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "search_web",
|
||||
Description: "Search the web",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{
|
||||
"query": {
|
||||
Type: api.PropertyType{"string"},
|
||||
},
|
||||
"limit": {
|
||||
Type: api.PropertyType{"integer"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "calculate",
|
||||
Description: "Perform a calculation",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{
|
||||
"expression": {
|
||||
Type: api.PropertyType{"string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantContent string
|
||||
wantThinking string
|
||||
wantCalls []api.ToolCall
|
||||
wantError bool
|
||||
}{
|
||||
// Basic functionality
|
||||
{
|
||||
name: "simple content",
|
||||
input: "Hello world",
|
||||
wantContent: "Hello world",
|
||||
wantThinking: "",
|
||||
wantCalls: nil,
|
||||
},
|
||||
{
|
||||
name: "single invoke with single parameter",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">Tokyo</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single invoke with multiple parameters",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">San Francisco</parameter>
|
||||
<parameter name="unit">celsius</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "San Francisco",
|
||||
"unit": "celsius",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "single invoke with no parameters",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="calculate">
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "calculate",
|
||||
Arguments: map[string]any{},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple invokes in one tool_call block",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">Tokyo</parameter>
|
||||
</invoke>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">London</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "London",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parameter with JSON object value",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="search_web">
|
||||
<parameter name="query">{"keywords": ["AI", "news"]}</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "search_web",
|
||||
Arguments: map[string]any{
|
||||
"query": map[string]any{
|
||||
"keywords": []any{"AI", "news"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parameter with JSON array value",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="search_web">
|
||||
<parameter name="query">["AI", "machine learning"]</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "search_web",
|
||||
Arguments: map[string]any{
|
||||
"query": []any{"AI", "machine learning"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parameter with integer value",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="search_web">
|
||||
<parameter name="query">AI news</parameter>
|
||||
<parameter name="limit">10</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "search_web",
|
||||
Arguments: map[string]any{
|
||||
"query": "AI news",
|
||||
"limit": float64(10), // JSON unmarshals numbers as float64
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Content mixing
|
||||
{
|
||||
name: "content before tool call",
|
||||
input: "Let me check the weather. <minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Tokyo</parameter>\n</invoke>\n</minimax:tool_call>",
|
||||
wantContent: "Let me check the weather. ",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content after tool call",
|
||||
input: "<minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Tokyo</parameter>\n</invoke>\n</minimax:tool_call>\nHere is the weather.",
|
||||
wantContent: "Here is the weather.",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "content before and after tool call",
|
||||
input: "Let me check. <minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Tokyo</parameter>\n</invoke>\n</minimax:tool_call>\nDone checking.",
|
||||
wantContent: "Let me check. Done checking.",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "thinking content only",
|
||||
input: "<think>I need to analyze this problem first.</think>",
|
||||
wantContent: "",
|
||||
wantThinking: "I need to analyze this problem first.",
|
||||
wantCalls: nil,
|
||||
},
|
||||
{
|
||||
name: "thinking then tool call",
|
||||
input: "<think>I should check the weather.</think>\n<minimax:tool_call>\n<invoke name=\"get_weather\">\n<parameter name=\"location\">Tokyo</parameter>\n</invoke>\n</minimax:tool_call>",
|
||||
wantContent: "",
|
||||
wantThinking: "I should check the weather.",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "thinking with content",
|
||||
input: "Let me think. <think>This is complex.</think> Okay, done.",
|
||||
wantContent: "Let me think. Okay, done.",
|
||||
wantThinking: "This is complex.",
|
||||
wantCalls: nil,
|
||||
},
|
||||
|
||||
// Edge cases
|
||||
{
|
||||
name: "whitespace in parameter values",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location"> San Francisco </parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "San Francisco",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty parameter value",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location"></parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "special characters in values",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="search_web">
|
||||
<parameter name="query">AI & Machine Learning</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "search_web",
|
||||
Arguments: map[string]any{
|
||||
"query": "AI & Machine Learning",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "tool call inside think tag (should treat as thinking)",
|
||||
input: "<think>Maybe I should call <minimax:tool_call><invoke name=\"get_weather\"><parameter name=\"location\">Tokyo</parameter></invoke></minimax:tool_call></think>",
|
||||
wantContent: "",
|
||||
wantThinking: "Maybe I should call <minimax:tool_call><invoke name=\"get_weather\"><parameter name=\"location\">Tokyo</parameter></invoke></minimax:tool_call>",
|
||||
wantCalls: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple thinking blocks",
|
||||
input: "<think>First thought</think> Some content. <think>Second thought</think>",
|
||||
wantContent: "Some content. ",
|
||||
wantThinking: "First thoughtSecond thought",
|
||||
wantCalls: nil,
|
||||
},
|
||||
{
|
||||
name: "multiple tool call blocks",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">Tokyo</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>
|
||||
Some text.
|
||||
<minimax:tool_call>
|
||||
<invoke name="search_web">
|
||||
<parameter name="query">news</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "Some text.\n",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "search_web",
|
||||
Arguments: map[string]any{
|
||||
"query": "news",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
// Recovery scenarios
|
||||
{
|
||||
name: "missing closing parameter tag",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">Tokyo</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "missing closing invoke tag",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">Tokyo</parameter>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parameter with newlines (should trim)",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="get_weather">
|
||||
<parameter name="location">
|
||||
Tokyo
|
||||
</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{
|
||||
"location": "Tokyo",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := &MiniMaxM2Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
gotContent, gotThinking, gotCalls, err := parser.Add(tt.input, true)
|
||||
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Add() error = %v, wantError %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
if gotContent != tt.wantContent {
|
||||
t.Errorf("Add() content = %q, want %q", gotContent, tt.wantContent)
|
||||
}
|
||||
|
||||
if gotThinking != tt.wantThinking {
|
||||
t.Errorf("Add() thinking = %q, want %q", gotThinking, tt.wantThinking)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.wantCalls, gotCalls); diff != "" {
|
||||
t.Errorf("Add() calls mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxM2ParserStreaming(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "get_weather",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{
|
||||
"location": {Type: api.PropertyType{"string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
chunks []string
|
||||
wantContent string
|
||||
wantThinking string
|
||||
wantCalls []api.ToolCall
|
||||
}{
|
||||
{
|
||||
name: "tool call tag split across chunks",
|
||||
chunks: []string{
|
||||
"Let me check. <minimax:tool",
|
||||
"_call>\n<invoke name=\"get_weather\">\n",
|
||||
"<parameter name=\"location\">Tokyo</parameter>\n",
|
||||
"</invoke>\n</minimax:tool_call>",
|
||||
},
|
||||
wantContent: "Let me check. ",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{"location": "Tokyo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invoke tag split across chunks",
|
||||
chunks: []string{
|
||||
"<minimax:tool_call>\n<inv",
|
||||
"oke name=\"get_weather\">\n<parameter name=\"location\">",
|
||||
"Tokyo</parameter>\n</invoke>\n</minimax:tool_call>",
|
||||
},
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{"location": "Tokyo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "parameter value split across chunks",
|
||||
chunks: []string{
|
||||
"<minimax:tool_call>\n<invoke name=\"get_weather\">\n",
|
||||
"<parameter name=\"location\">San Fran",
|
||||
"cisco</parameter>\n</invoke>\n</minimax:tool_call>",
|
||||
},
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{"location": "San Francisco"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "closing tag split across chunks",
|
||||
chunks: []string{
|
||||
"<minimax:tool_call>\n<invoke name=\"get_weather\">\n",
|
||||
"<parameter name=\"location\">Tokyo</parameter>\n</inv",
|
||||
"oke>\n</minimax:tool_call>",
|
||||
},
|
||||
wantContent: "",
|
||||
wantThinking: "",
|
||||
wantCalls: []api.ToolCall{
|
||||
{
|
||||
Function: api.ToolCallFunction{
|
||||
Name: "get_weather",
|
||||
Arguments: map[string]any{"location": "Tokyo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "thinking tag split across chunks",
|
||||
chunks: []string{
|
||||
"Let me think. <thi",
|
||||
"nk>This is complex.</th",
|
||||
"ink> Done.",
|
||||
},
|
||||
wantContent: "Let me think. Done.",
|
||||
wantThinking: "This is complex.",
|
||||
wantCalls: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := &MiniMaxM2Parser{}
|
||||
parser.Init(tools, nil, nil)
|
||||
|
||||
var allContent, allThinking string
|
||||
var allCalls []api.ToolCall
|
||||
|
||||
for i, chunk := range tt.chunks {
|
||||
isDone := i == len(tt.chunks)-1
|
||||
content, thinking, calls, err := parser.Add(chunk, isDone)
|
||||
if err != nil {
|
||||
t.Fatalf("Add() error = %v", err)
|
||||
}
|
||||
allContent += content
|
||||
allThinking += thinking
|
||||
allCalls = append(allCalls, calls...)
|
||||
}
|
||||
|
||||
if allContent != tt.wantContent {
|
||||
t.Errorf("Streaming content = %q, want %q", allContent, tt.wantContent)
|
||||
}
|
||||
|
||||
if allThinking != tt.wantThinking {
|
||||
t.Errorf("Streaming thinking = %q, want %q", allThinking, tt.wantThinking)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(tt.wantCalls, allCalls); diff != "" {
|
||||
t.Errorf("Streaming calls mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxM2ParserErrors(t *testing.T) {
|
||||
tools := []api.Tool{
|
||||
{
|
||||
Type: "function",
|
||||
Function: api.ToolFunction{
|
||||
Name: "get_weather",
|
||||
Parameters: api.ToolFunctionParameters{
|
||||
Type: "object",
|
||||
Properties: map[string]api.ToolProperty{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "unknown tool",
|
||||
input: `<minimax:tool_call>
|
||||
<invoke name="unknown_function">
|
||||
<parameter name="param">value</parameter>
|
||||
</invoke>
|
||||
</minimax:tool_call>`,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "no tools provided but model makes tool call",
|
||||
input: `<minimax:tool_call>\n<invoke name="get_weather">\n<parameter name="location">Tokyo</parameter>\n</invoke>\n</minimax:tool_call>`,
|
||||
wantError: true, // Should error when tool is not in registry
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
parser := &MiniMaxM2Parser{}
|
||||
if tt.name == "no tools provided but model makes tool call" {
|
||||
parser.Init(nil, nil, nil) // No tools
|
||||
} else {
|
||||
parser.Init(tools, nil, nil)
|
||||
}
|
||||
|
||||
_, _, _, err := parser.Add(tt.input, true)
|
||||
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("Add() error = %v, wantError %v", err, tt.wantError)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxM2ParserAttributeExtraction(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
tag string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "double quotes",
|
||||
tag: `<invoke name="get_weather">`,
|
||||
want: "get_weather",
|
||||
},
|
||||
{
|
||||
name: "single quotes",
|
||||
tag: `<invoke name='get_weather'>`,
|
||||
want: "get_weather",
|
||||
},
|
||||
{
|
||||
name: "with extra attributes",
|
||||
tag: `<invoke name="get_weather" id="123">`,
|
||||
want: "get_weather",
|
||||
},
|
||||
{
|
||||
name: "parameter tag",
|
||||
tag: `<parameter name="location">`,
|
||||
want: "location",
|
||||
},
|
||||
{
|
||||
name: "no name attribute",
|
||||
tag: `<invoke>`,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "malformed quote",
|
||||
tag: `<invoke name="get_weather>`,
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := extractNameAttribute(tt.tag)
|
||||
if got != tt.want {
|
||||
t.Errorf("extractNameAttribute() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -68,6 +68,8 @@ func ParserForName(name string) Parser {
|
|||
return &Nemotron3NanoParser{}
|
||||
case "functiongemma":
|
||||
return &FunctionGemmaParser{}
|
||||
case "minimaxm2":
|
||||
return &MiniMaxM2Parser{}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue