package parsers import ( "reflect" "testing" "github.com/ollama/ollama/api" ) func TestIntellect3ParserThinkingOnly(t *testing.T) { cases := []struct { desc string chunks []string wantText string wantThink string }{ { desc: "simple thinking content", chunks: []string{"I need to analyze thisHere is my response"}, wantText: "Here is my response", wantThink: "I need to analyze this", }, { desc: "thinking with whitespace", chunks: []string{"\n Some thoughts \n\n\nContent"}, wantText: "Content", wantThink: "Some thoughts \n", // Thinking parser preserves internal whitespace }, { desc: "thinking only", chunks: []string{"Just thinking"}, wantText: "", wantThink: "Just thinking", }, { desc: "no thinking tags", chunks: []string{"Just regular content"}, wantText: "Just regular content", wantThink: "", }, { desc: "streaming thinking content", chunks: []string{"Fir", "st part", " second partContent"}, wantText: "Content", wantThink: "First part second part", }, { desc: "partial opening tag", chunks: []string{"ThinkingContent"}, wantText: "Content", wantThink: "Thinking", }, { desc: "partial closing tag", chunks: []string{"ThinkingContent"}, wantText: "Content", wantThink: "Thinking", }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { parser := Intellect3Parser{} parser.Init(nil, nil, nil) var gotText, gotThink string for i, chunk := range tc.chunks { isLast := i == len(tc.chunks)-1 text, think, calls, err := parser.Add(chunk, isLast) if err != nil { t.Fatalf("unexpected error: %v", err) } gotText += text gotThink += think if len(calls) > 0 { t.Fatalf("expected no tool calls, got %v", calls) } } if gotText != tc.wantText { t.Errorf("content: got %q, want %q", gotText, tc.wantText) } if gotThink != tc.wantThink { t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink) } }) } } func TestIntellect3ParserToolCallsOnly(t *testing.T) { tools := []api.Tool{ tool("get_weather", map[string]api.ToolProperty{ "location": {Type: api.PropertyType{"string"}}, "unit": {Type: api.PropertyType{"string"}}, }), } cases := []struct { desc string chunks []string wantText string wantCalls []api.ToolCall }{ { desc: "simple tool call", chunks: []string{ "Let me check the weather\n\nSan Francisco\n\n\ncelsius\n\n", }, wantText: "Let me check the weather", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "San Francisco", "unit": "celsius", }, }, }, }, }, { desc: "tool call streaming", chunks: []string{ "Checking\n\nNew York\n\n\nfahrenheit\n\nDone", }, wantText: "CheckingDone", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "New York", "unit": "fahrenheit", }, }, }, }, }, { desc: "multiple tool calls", chunks: []string{ "\n\nBoston\n\n\ncelsius\n\n", "\n\nSeattle\n\n\nfahrenheit\n\n", }, wantText: "", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Boston", "unit": "celsius", }, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Seattle", "unit": "fahrenheit", }, }, }, }, }, { desc: "no tool calls", chunks: []string{"Just regular content"}, wantText: "Just regular content", wantCalls: nil, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { parser := Intellect3Parser{} parser.Init(tools, nil, nil) var gotText string var gotCalls []api.ToolCall for i, chunk := range tc.chunks { isLast := i == len(tc.chunks)-1 text, think, calls, err := parser.Add(chunk, isLast) if err != nil { t.Fatalf("unexpected error: %v", err) } gotText += text gotCalls = append(gotCalls, calls...) if think != "" { t.Fatalf("expected no thinking, got %q", think) } } if gotText != tc.wantText { t.Errorf("content: got %q, want %q", gotText, tc.wantText) } if !reflect.DeepEqual(gotCalls, tc.wantCalls) { t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls) } }) } } func TestIntellect3ParserCombined(t *testing.T) { tools := []api.Tool{ tool("get_weather", map[string]api.ToolProperty{ "location": {Type: api.PropertyType{"string"}}, "unit": {Type: api.PropertyType{"string"}}, }), } cases := []struct { desc string chunks []string wantText string wantThink string wantCalls []api.ToolCall }{ { desc: "thinking then tool call", chunks: []string{ "Need to get weather dataLet me check\n\nParis\n\n\ncelsius\n\n", }, wantText: "Let me check", wantThink: "Need to get weather data", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Paris", "unit": "celsius", }, }, }, }, }, { desc: "thinking, tool call, and final content", chunks: []string{ "User wants weather infoChecking weather\n\nTokyo\n\n\ncelsius\n\nDone!", }, wantText: "Checking weatherDone!", wantThink: "User wants weather info", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Tokyo", "unit": "celsius", }, }, }, }, }, { desc: "streaming combined content", chunks: []string{ "Analyzing", " the request", "Let me help", "\n\nLondon", "\n\n\ncelsius\n\n", "There you go!", }, wantText: "Let me helpThere you go!", wantThink: "Analyzing the request", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "London", "unit": "celsius", }, }, }, }, }, { desc: "multiple tool calls with thinking", chunks: []string{ "Need multiple locations", "\n\nBoston\n\n\ncelsius\n\n", "and\n\nBerlin\n\n\ncelsius\n\n", }, wantText: "and", wantThink: "Need multiple locations", wantCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Boston", "unit": "celsius", }, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{ "location": "Berlin", "unit": "celsius", }, }, }, }, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { parser := Intellect3Parser{} parser.Init(tools, nil, nil) var gotText, gotThink string var gotCalls []api.ToolCall for i, chunk := range tc.chunks { isLast := i == len(tc.chunks)-1 text, think, calls, err := parser.Add(chunk, isLast) if err != nil { t.Fatalf("unexpected error: %v", err) } gotText += text gotThink += think gotCalls = append(gotCalls, calls...) } if gotText != tc.wantText { t.Errorf("content: got %q, want %q", gotText, tc.wantText) } if gotThink != tc.wantThink { t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink) } if !reflect.DeepEqual(gotCalls, tc.wantCalls) { t.Errorf("tool calls: got %#v, want %#v", gotCalls, tc.wantCalls) } }) } } func TestIntellect3ParserEdgeCases(t *testing.T) { tools := []api.Tool{ tool("test_func", map[string]api.ToolProperty{ "param": {Type: api.PropertyType{"string"}}, }), } cases := []struct { desc string chunks []string wantText string wantThink string wantCalls int }{ { desc: "empty input", chunks: []string{""}, wantText: "", wantThink: "", wantCalls: 0, }, { desc: "only whitespace", chunks: []string{" \n \t "}, wantText: "", wantThink: "", wantCalls: 0, }, { desc: "unclosed thinking tag", chunks: []string{"Never closes"}, wantText: "", wantThink: "Never closes", wantCalls: 0, }, { desc: "unclosed tool call tag", chunks: []string{"\n\nvalue\n\n"}, wantText: "", // Qwen3CoderParser waits for closing tag, doesn't emit partial tool calls wantThink: "", wantCalls: 0, // Won't be parsed until is seen }, { desc: "unicode in thinking", chunks: []string{"思考中 πŸ€”η­”ζ‘ˆζ˜― 42"}, wantText: "η­”ζ‘ˆζ˜― 42", wantThink: "思考中 πŸ€”", wantCalls: 0, }, { desc: "fake thinking tag", chunks: []string{"This is not the right tagContent"}, wantText: "This is not the right tagContent", wantThink: "", wantCalls: 0, }, { desc: "fake tool call tag", chunks: []string{"Not a tool call"}, wantText: "Not a tool call", wantThink: "", wantCalls: 0, }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { parser := Intellect3Parser{} parser.Init(tools, nil, nil) var gotText, gotThink string var gotCalls []api.ToolCall for i, chunk := range tc.chunks { isLast := i == len(tc.chunks)-1 text, think, calls, err := parser.Add(chunk, isLast) if err != nil { t.Fatalf("unexpected error: %v", err) } gotText += text gotThink += think gotCalls = append(gotCalls, calls...) } if gotText != tc.wantText { t.Errorf("content: got %q, want %q", gotText, tc.wantText) } if gotThink != tc.wantThink { t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink) } if len(gotCalls) != tc.wantCalls { t.Errorf("tool calls count: got %d, want %d", len(gotCalls), tc.wantCalls) } }) } } func TestIntellect3ParserCapabilities(t *testing.T) { parser := Intellect3Parser{} if !parser.HasToolSupport() { t.Error("Intellect3Parser should have tool support") } if !parser.HasThinkingSupport() { t.Error("Intellect3Parser should have thinking support") } } func TestIntellect3ParserInit(t *testing.T) { parser := Intellect3Parser{} tools := []api.Tool{ tool("test", map[string]api.ToolProperty{ "param": {Type: api.PropertyType{"string"}}, }), } returnedTools := parser.Init(tools, nil, nil) // Should return tools unchanged (delegated to Qwen3CoderParser) if !reflect.DeepEqual(returnedTools, tools) { t.Errorf("Init should return tools unchanged") } } func TestIntellect3ParserWhitespaceHandling(t *testing.T) { tools := []api.Tool{ tool("test", map[string]api.ToolProperty{ "param": {Type: api.PropertyType{"string"}}, }), } cases := []struct { desc string chunks []string wantText string wantThink string }{ { desc: "whitespace between thinking and content", chunks: []string{"Thinking\n\n\nContent"}, wantText: "Content", wantThink: "Thinking", }, { desc: "whitespace inside thinking tags", chunks: []string{" \n Thinking \n Content"}, wantText: "Content", wantThink: "Thinking \n ", // Thinking parser preserves internal whitespace }, { desc: "leading whitespace before thinking", chunks: []string{" ThinkingContent"}, wantText: "Content", wantThink: "Thinking", }, { desc: "whitespace before tool call", chunks: []string{"Text \n\nvalue\n\n"}, wantText: "Text", wantThink: "", }, { desc: "whitespace after tool call", chunks: []string{"\n\nvalue\n\n Text"}, wantText: "Text", wantThink: "", }, } for _, tc := range cases { t.Run(tc.desc, func(t *testing.T) { parser := Intellect3Parser{} parser.Init(tools, nil, nil) var gotText, gotThink string for i, chunk := range tc.chunks { isLast := i == len(tc.chunks)-1 text, think, _, err := parser.Add(chunk, isLast) if err != nil { t.Fatalf("unexpected error: %v", err) } gotText += text gotThink += think } if gotText != tc.wantText { t.Errorf("content: got %q, want %q", gotText, tc.wantText) } if gotThink != tc.wantThink { t.Errorf("thinking: got %q, want %q", gotThink, tc.wantThink) } }) } }