package parsers import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ollama/ollama/api" ) func TestDeepSeekParser(t *testing.T) { tests := []struct { name string input string expectedContent string expectedThinking string expectedCalls []api.ToolCall hasThinking bool }{ { name: "simple_content", input: "Hello, how are you?", expectedContent: "Hello, how are you?", hasThinking: false, }, { name: "thinking_content", input: "I need to think about this...The answer is 42.", expectedThinking: "I need to think about this...", expectedContent: "The answer is 42.", hasThinking: true, }, { name: "no_thinking_simple", input: "Just a regular response.", expectedContent: "Just a regular response.", hasThinking: false, }, { name: "thinking_with_newlines", input: "Let me think:\n- Point 1\n- Point 2\n\nHere's my answer.", expectedThinking: "Let me think:\n- Point 1\n- Point 2", expectedContent: "Here's my answer.", hasThinking: true, }, { name: "tool_call_simple", input: "I'll check the weather.<|tool▁calls▁begin|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"Paris\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "I'll check the weather.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Paris", }, }, }, }, hasThinking: false, }, { name: "multiple_tool_calls", input: "Getting weather for both cities.<|tool▁calls▁begin|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"Paris\"}<|tool▁call▁end|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"London\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Getting weather for both cities.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Paris", }, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "London", }, }, }, }, hasThinking: false, }, { name: "tool_output", input: "Here's the weather: <|tool▁output▁begin|>Temperature: 22°C, Sunny<|tool▁output▁end|> Hope that helps!", expectedContent: "Here's the weather: Temperature: 22°C, Sunny Hope that helps!", hasThinking: false, }, { name: "complex_tool_arguments", input: "Processing data.<|tool▁calls▁begin|><|tool▁call▁begin|>process_data<|tool▁sep|>{\"items\":[\"item1\",\"item2\"],\"config\":{\"enabled\":true,\"threshold\":0.95}}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Processing data.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "process_data", Arguments: api.ToolCallFunctionArguments{ "items": []interface{}{"item1", "item2"}, "config": map[string]interface{}{"enabled": true, "threshold": 0.95}, }, }, }, }, hasThinking: false, }, { name: "thinking_with_tool_call", // technically this can't happen, but the parser can handle it input: "Let me check the weather...I'll get that for you.<|tool▁calls▁begin|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"Paris\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedThinking: "Let me check the weather...", expectedContent: "I'll get that for you.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Paris", }, }, }, }, hasThinking: true, }, { name: "empty_content", input: "", expectedContent: "", hasThinking: false, }, { name: "only_thinking", input: "Just thinking content", expectedThinking: "Just thinking content", expectedContent: "", hasThinking: true, }, { name: "multiple_tool_outputs", input: "Results: <|tool▁output▁begin|>Paris: 22°C<|tool▁output▁end|> and <|tool▁output▁begin|>London: 18°C<|tool▁output▁end|>", expectedContent: "Results: Paris: 22°C and London: 18°C", hasThinking: false, }, { name: "unicode_content", input: "مرحبا بالعالم! 你好世界! 🌍", expectedContent: "مرحبا بالعالم! 你好世界! 🌍", hasThinking: false, }, { name: "emoji_passthrough", input: "Task completed ✅ 🎉", expectedContent: "Task completed ✅ 🎉", hasThinking: false, }, { name: "emoji_after_tool_call", input: "I'll help you.<|tool▁calls▁begin|><|tool▁call▁begin|>get_weather<|tool▁sep|>{\"location\":\"Tokyo\"}<|tool▁call▁end|><|tool▁calls▁end|>完成 ✅", expectedContent: "I'll help you.完成 ✅", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Tokyo", }, }, }, }, hasThinking: false, }, { name: "newlines_and_whitespace", input: "Line 1\n\nLine 3\t\tTabbed content", expectedContent: "Line 1\n\nLine 3\t\tTabbed content", hasThinking: false, }, { name: "thinking_with_unicode", input: "我在思考这个问题...答案是42。", expectedThinking: "我在思考这个问题...", expectedContent: "答案是42。", hasThinking: true, }, { name: "tool_call_with_unicode_args", input: "Searching for information.<|tool▁calls▁begin|><|tool▁call▁begin|>search<|tool▁sep|>{\"query\":\"北京天气\",\"language\":\"中文\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Searching for information.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "search", Arguments: api.ToolCallFunctionArguments{ "query": "北京天气", "language": "中文", }, }, }, }, hasThinking: false, }, { name: "tool_output_with_unicode", input: "天气信息: <|tool▁output▁begin|>北京: 25°C, 晴天<|tool▁output▁end|> 希望对您有帮助!", expectedContent: "天气信息: 北京: 25°C, 晴天 希望对您有帮助!", hasThinking: false, }, { name: "mixed_content_with_special_chars", input: "Price: $100 & tax @ 10% = $110 <|tool▁output▁begin|>Total: $110<|tool▁output▁end|> (final)", expectedContent: "Price: $100 & tax @ 10% = $110 Total: $110 (final)", hasThinking: false, }, { name: "tool_call_with_special_chars", input: "Processing data.<|tool▁calls▁begin|><|tool▁call▁begin|>execute_command<|tool▁sep|>{\"command\":\"ls && echo \\\"done\\\"\",\"path\":\"/home/user\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Processing data.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "execute_command", Arguments: api.ToolCallFunctionArguments{ "command": "ls && echo \"done\"", "path": "/home/user", }, }, }, }, hasThinking: false, }, { name: "thinking_with_special_chars", input: "Let me calculate: 2+2=4 & 3*3=9...The results are correct!", expectedThinking: "Let me calculate: 2+2=4 & 3*3=9...", expectedContent: "The results are correct!", hasThinking: true, }, { name: "empty_tool_call_args", input: "Pinging server.<|tool▁calls▁begin|><|tool▁call▁begin|>ping<|tool▁sep|>{}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Pinging server.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "ping", Arguments: api.ToolCallFunctionArguments{}, }, }, }, hasThinking: false, }, { name: "empty_tool_output", input: "Checking status: <|tool▁output▁begin|><|tool▁output▁end|> No output received.", expectedContent: "Checking status: No output received.", hasThinking: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parser := &DeepSeek3Parser{hasThinkingSupport: tt.hasThinking} parser.Init([]api.Tool{}, nil, &api.ThinkValue{Value: tt.hasThinking}) content, thinking, calls, err := parser.Add(tt.input, true) if err != nil { t.Fatalf("Add() error = %v", err) } if diff := cmp.Diff(tt.expectedContent, content); diff != "" { t.Errorf("Content mismatch (-want +got):\n%s", diff) } if diff := cmp.Diff(tt.expectedThinking, thinking); diff != "" { t.Errorf("Thinking mismatch (-want +got):\n%s", diff) } if diff := cmp.Diff(tt.expectedCalls, calls); diff != "" { t.Errorf("Tool calls mismatch (-want +got):\n%s", diff) } }) } } func TestDeepSeekParser_Streaming(t *testing.T) { tests := []struct { name string chunks []string expectedContent string expectedThinking string expectedCalls []api.ToolCall hasThinking bool }{ { name: "streaming_simple_content", chunks: []string{"Hello, ", "how are ", "you?"}, expectedContent: "Hello, how are you?", hasThinking: false, }, { name: "streaming_thinking", chunks: []string{"I need to ", "think about this", "...", "The answer is 42."}, expectedThinking: "I need to think about this...", expectedContent: "The answer is 42.", hasThinking: true, }, { name: "streaming_tool_call", chunks: []string{"I'll check weather.", "<|tool▁calls▁begin|>", "<|tool▁call▁begin|>get_weather", "<|tool▁sep|>{\"location\":\"Paris\"}", "<|tool▁call▁end|><|tool▁calls▁end|>"}, expectedContent: "I'll check weather.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Paris", }, }, }, }, hasThinking: false, }, { name: "streaming_thinking_with_partial_tag", chunks: []string{"Thinking about this", "...", "Done thinking."}, expectedThinking: "Thinking about this...", expectedContent: "Done thinking.", hasThinking: true, }, { name: "streaming_tool_output", chunks: []string{"Weather info: ", "<|tool▁output▁begin|>", "25°C, Sunny", "<|tool▁output▁end|>", " Enjoy!"}, expectedContent: "Weather info: 25°C, Sunny Enjoy!", hasThinking: false, }, { name: "streaming_with_split_tags", chunks: []string{"Content before ", "<|tool▁calls▁begin|><|tool▁call▁begin|>test", "<|tool▁sep|>{}", "<|tool▁call▁end|><|tool▁calls▁end|>", " after"}, expectedContent: "Content before after", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "test", Arguments: api.ToolCallFunctionArguments{}, }, }, }, hasThinking: false, }, { name: "streaming_thinking_with_split_end_tag", chunks: []string{"Thinking content", "", "Regular content"}, expectedThinking: "Thinking content", expectedContent: "Regular content", hasThinking: true, }, { name: "streaming_unicode_content", chunks: []string{"مرحبا ", "بالعالم! ", "你好", "世界!"}, expectedContent: "مرحبا بالعالم! 你好世界!", hasThinking: false, }, { name: "streaming_multiple_tool_outputs", chunks: []string{"Results: ", "<|tool▁output▁begin|>", "Paris: 22°C", "<|tool▁output▁end|>", " and ", "<|tool▁output▁begin|>", "London: 18°C", "<|tool▁output▁end|>"}, expectedContent: "Results: Paris: 22°C and London: 18°C", hasThinking: false, }, { name: "streaming_tool_call_with_split_json", chunks: []string{"Processing.", "<|tool▁calls▁begin|><|tool▁call▁begin|>calc<|tool▁sep|>{\"x\":", "42,\"y\":", "24}<|tool▁call▁end|><|tool▁calls▁end|>"}, expectedContent: "Processing.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "calc", Arguments: api.ToolCallFunctionArguments{ "x": float64(42), "y": float64(24), }, }, }, }, hasThinking: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parser := &DeepSeek3Parser{hasThinkingSupport: tt.hasThinking} parser.Init([]api.Tool{}, nil, &api.ThinkValue{Value: tt.hasThinking}) var allContent, allThinking string var allCalls []api.ToolCall for i, chunk := range tt.chunks { done := i == len(tt.chunks)-1 content, thinking, calls, err := parser.Add(chunk, done) if err != nil { t.Fatalf("Add() error = %v", err) } allContent += content allThinking += thinking allCalls = append(allCalls, calls...) } if diff := cmp.Diff(tt.expectedContent, allContent); diff != "" { t.Errorf("Content mismatch (-want +got):\n%s", diff) } if diff := cmp.Diff(tt.expectedThinking, allThinking); diff != "" { t.Errorf("Thinking mismatch (-want +got):\n%s", diff) } if diff := cmp.Diff(tt.expectedCalls, allCalls); diff != "" { t.Errorf("Tool calls mismatch (-want +got):\n%s", diff) } }) } } func TestDeepSeekParser_HasThinkingSupport(t *testing.T) { tests := []struct { name string hasThinking bool expectedSupport bool }{ { name: "thinking_enabled", hasThinking: true, expectedSupport: true, }, { name: "thinking_disabled", hasThinking: false, expectedSupport: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parser := &DeepSeek3Parser{hasThinkingSupport: tt.hasThinking} if got := parser.HasThinkingSupport(); got != tt.expectedSupport { t.Errorf("HasThinkingSupport() = %v, want %v", got, tt.expectedSupport) } }) } } func TestDeepSeekParser_HasToolSupport(t *testing.T) { parser := &DeepSeek3Parser{} if !parser.HasToolSupport() { t.Error("HasToolSupport() should return true") } } func TestDeepSeekParser_Init(t *testing.T) { parser := &DeepSeek3Parser{hasThinkingSupport: true} tools := []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "test_tool", }, }, } returnedTools := parser.Init(tools, nil, &api.ThinkValue{Value: true}) if diff := cmp.Diff(tools, returnedTools); diff != "" { t.Errorf("Init() returned tools mismatch (-want +got):\n%s", diff) } // Test initial state is set to thinking when enabled if parser.state != DeepSeekCollectingThinking { t.Errorf("Expected initial state to be DeepSeekCollectingThinking, got %v", parser.state) } } func TestDeepSeek3Parser_parseToolCallContent(t *testing.T) { tests := []struct { name string content string expected api.ToolCall expectError bool }{ { name: "valid_tool_call", content: "get_weather<|tool▁sep|>{\"location\":\"Paris\"}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "get_weather", Arguments: api.ToolCallFunctionArguments{ "location": "Paris", }, }, }, }, { name: "complex_arguments", content: "process_data<|tool▁sep|>{\"items\":[\"a\",\"b\"],\"config\":{\"enabled\":true}}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "process_data", Arguments: api.ToolCallFunctionArguments{ "items": []interface{}{"a", "b"}, "config": map[string]interface{}{"enabled": true}, }, }, }, }, { name: "empty_arguments", content: "ping<|tool▁sep|>{}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "ping", Arguments: api.ToolCallFunctionArguments{}, }, }, }, { name: "unicode_in_tool_name", content: "获取天气<|tool▁sep|>{\"城市\":\"北京\"}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "获取天气", Arguments: api.ToolCallFunctionArguments{ "城市": "北京", }, }, }, }, { name: "special_chars_in_arguments", content: "execute<|tool▁sep|>{\"command\":\"ls && echo \\\"done\\\"\",\"path\":\"/home/user\"}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "execute", Arguments: api.ToolCallFunctionArguments{ "command": "ls && echo \"done\"", "path": "/home/user", }, }, }, }, { name: "numeric_arguments", content: "calculate<|tool▁sep|>{\"x\":3.14,\"y\":42,\"enabled\":true}", expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "calculate", Arguments: api.ToolCallFunctionArguments{ "x": 3.14, "y": float64(42), "enabled": true, }, }, }, }, { name: "invalid_format_no_separator", content: "get_weather{\"location\":\"Paris\"}", expectError: true, }, { name: "invalid_json", content: "get_weather<|tool▁sep|>{invalid json}", expectError: true, }, { name: "empty_tool_name", content: "<|tool▁sep|>{\"arg\":\"value\"}", expectError: false, // This should work, just empty name expected: api.ToolCall{ Function: api.ToolCallFunction{ Name: "", Arguments: api.ToolCallFunctionArguments{ "arg": "value", }, }, }, }, { name: "missing_json_part", content: "tool_name<|tool▁sep|>", expectError: true, }, } parser := &DeepSeek3Parser{} for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := parser.parseToolCallContent(tt.content) if tt.expectError { if err == nil { t.Error("Expected error but got none") } return } if err != nil { t.Fatalf("Unexpected error: %v", err) } if diff := cmp.Diff(tt.expected, result); diff != "" { t.Errorf("parseToolCallContent() mismatch (-want +got):\n%s", diff) } }) } } func TestDeepSeekParser_EdgeCases(t *testing.T) { tests := []struct { name string input string expectedContent string expectedThinking string hasThinking bool }{ { name: "nested_think_tags_in_thinking", input: "Outer thinking inner contentFinal content", expectedThinking: "Outer thinking inner", expectedContent: "contentFinal content", hasThinking: true, }, { name: "multiple_think_close_tags", input: "First thoughtSecond thoughtFinal content", expectedThinking: "First thought", expectedContent: "Second thoughtFinal content", hasThinking: true, }, { name: "empty_thinking_content", input: "Just content", expectedThinking: "", expectedContent: "Just content", hasThinking: true, }, { name: "thinking_disabled_with_think_tags", input: "Some contentMore content", expectedContent: "Some contentMore content", hasThinking: false, }, { name: "malformed_tool_call_missing_sep", input: "Testing.<|tool▁calls▁begin|><|tool▁call▁begin|>bad_tool{\"arg\":\"value\"}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Testing.", hasThinking: false, }, { name: "malformed_tool_call_invalid_json", input: "Testing.<|tool▁calls▁begin|><|tool▁call▁begin|>bad_tool<|tool▁sep|>{invalid json}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "Testing.", hasThinking: false, }, { name: "partial_tool_tag_at_end", input: "Content with partial <|tool▁calls▁", expectedContent: "Content with partial <|tool▁calls▁", hasThinking: false, }, { name: "partial_think_tag_at_end", input: "Thinking contentLine 1\nLine 2\nLine 3<|tool▁output▁end|>\nDone.", expectedContent: "Output:\nLine 1\nLine 2\nLine 3\nDone.", hasThinking: false, }, { name: "consecutive_tool_calls", input: "First.<|tool▁calls▁begin|><|tool▁call▁begin|>tool1<|tool▁sep|>{}<|tool▁call▁end|><|tool▁calls▁end|>Second.<|tool▁calls▁begin|><|tool▁call▁begin|>tool2<|tool▁sep|>{}<|tool▁call▁end|><|tool▁calls▁end|>", expectedContent: "First.", hasThinking: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { parser := &DeepSeek3Parser{hasThinkingSupport: tt.hasThinking} parser.Init([]api.Tool{}, nil, &api.ThinkValue{Value: tt.hasThinking}) content, thinking, _, err := parser.Add(tt.input, true) if err != nil { t.Fatalf("Add() error = %v", err) } if diff := cmp.Diff(tt.expectedContent, content); diff != "" { t.Errorf("Content mismatch (-want +got):\n%s", diff) } if diff := cmp.Diff(tt.expectedThinking, thinking); diff != "" { t.Errorf("Thinking mismatch (-want +got):\n%s", diff) } }) } }