package parsers import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ollama/ollama/api" ) func TestNemotron3NanoParser(t *testing.T) { tests := []struct { name string input string thinkValue *api.ThinkValue expectedContent string expectedThinking string expectedCalls []api.ToolCall }{ { name: "simple content - no thinking", input: "Hello, how can I help you?", thinkValue: nil, expectedContent: "Hello, how can I help you?", }, { name: "simple content - thinking disabled", input: "Hello, how can I help you?", thinkValue: &api.ThinkValue{Value: false}, expectedContent: "Hello, how can I help you?", }, { name: "thinking then content", input: "Let me think about this...\nHere is my answer.", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Let me think about this...", expectedContent: "Here is my answer.", }, { name: "thinking with newlines", input: "Step 1: Analyze\nStep 2: Process\nStep 3: Conclude\nThe answer is 42.", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Step 1: Analyze\nStep 2: Process\nStep 3: Conclude", expectedContent: "The answer is 42.", }, { name: "simple tool call", input: "\n\n\nParis\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, }, }, { name: "content then tool call", input: "Let me check the weather.\n\n\n\nNYC\n\n\n", thinkValue: nil, expectedContent: "Let me check the weather.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "NYC"}, }, }, }, }, { name: "tool call with multiple parameters", input: "\n\n\nSFO\n\n\nNYC\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "book_flight", Arguments: map[string]any{ "from": "SFO", "to": "NYC", }, }, }, }, }, { name: "multiple tool calls", input: "\n\n\nSan Francisco\n\n\n\n" + "\n\n\nNew York\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "San Francisco"}, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "New York"}, }, }, }, }, { name: "thinking then tool call", input: "I should check the weather...\n\n\n\nParis\n\n\n", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "I should check the weather...", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, }, }, { name: "thinking content then tool call", input: "Let me think...\nI'll check for you.\n\n\n\ntest\n\n\n", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Let me think...", expectedContent: "I'll check for you.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "search", Arguments: map[string]any{"query": "test"}, }, }, }, }, { name: "tool call with multiline parameter value", input: "\n\n\nLine 1\nLine 2\nLine 3\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "create_note", Arguments: map[string]any{"content": "Line 1\nLine 2\nLine 3"}, }, }, }, }, { name: "empty thinking block - immediate close", input: "\nHere is my answer.", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "", expectedContent: "Here is my answer.", }, { name: "thinking disabled but model outputs think close anyway", input: "\nSome content after spurious tag.", thinkValue: &api.ThinkValue{Value: false}, expectedContent: "\nSome content after spurious tag.", }, { name: "tool call with no function name - returns empty tool call", input: "\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{{Function: api.ToolCallFunction{Name: "", Arguments: nil}}}, }, { name: "content with newlines preserved", input: "Line 1\n\nLine 2\n\n\nLine 3", thinkValue: nil, expectedContent: "Line 1\n\nLine 2\n\n\nLine 3", }, { name: "thinking with only whitespace after close tag", input: "My thoughts... \n\t\n Content here.", thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "My thoughts...", expectedContent: "Content here.", }, { name: "unicode content", input: "Hello 世界! 🌍 Ñoño", thinkValue: nil, expectedContent: "Hello 世界! 🌍 Ñoño", }, { name: "tool call with numeric parameter", input: "\n\n\n42\n\n\n", thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "set_temp", Arguments: map[string]any{"value": "42"}, }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p := &Nemotron3NanoParser{} p.Init(nil, nil, tt.thinkValue) content, thinking, calls, err := p.Add(tt.input, false) if err != nil { t.Fatalf("unexpected error: %v", err) } // Drain remaining content finalContent, finalThinking, finalCalls, err := p.Add("", true) if err != nil { t.Fatalf("unexpected error on done: %v", err) } content += finalContent thinking += finalThinking calls = append(calls, finalCalls...) if diff := cmp.Diff(content, tt.expectedContent); diff != "" { t.Errorf("content mismatch (-got +want):\n%s", diff) } if diff := cmp.Diff(thinking, tt.expectedThinking); diff != "" { t.Errorf("thinking mismatch (-got +want):\n%s", diff) } if diff := cmp.Diff(calls, tt.expectedCalls); diff != "" { t.Errorf("calls mismatch (-got +want):\n%s", diff) } }) } } func TestNemotron3NanoParser_Streaming(t *testing.T) { tests := []struct { name string chunks []string thinkValue *api.ThinkValue expectedContent string expectedThinking string expectedCalls []api.ToolCall }{ { name: "streaming content character by character", chunks: []string{"H", "e", "l", "l", "o", ",", " ", "w", "o", "r", "l", "d", "!"}, thinkValue: nil, expectedContent: "Hello, world!", }, { name: "streaming content small tokens", chunks: []string{"Hel", "lo", ", ", "how ", "can", " I", " help", " you", " today", "?"}, thinkValue: nil, expectedContent: "Hello, how can I help you today?", }, { name: "streaming thinking then content - granular", chunks: []string{"Let", " me", " th", "ink", " about", " this", "...", "<", "/", "think", ">", "\n", "Here", " is", " my", " answer", "."}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Let me think about this...", expectedContent: "Here is my answer.", }, { name: "streaming thinking with newlines - granular", chunks: []string{"Step", " 1", ":", " Ana", "lyze\n", "Step", " 2", ":", " Pro", "cess", "", "\n", "The", " ans", "wer."}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Step 1: Analyze\nStep 2: Process", expectedContent: "The answer.", }, { name: "streaming tool call - highly granular", chunks: []string{"<", "tool", "_", "call", ">", "\n", "<", "func", "tion", "=", "get", "_", "weather", ">", "\n", "<", "param", "eter", "=", "city", ">", "\n", "Par", "is", "\n", "", "\n", "", "\n", ""}, thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, }, }, { name: "streaming content then tool call - granular", chunks: []string{"Let", " me", " check", " the", " weather", ".", "\n<", "tool_call", ">", "\n", "", "\n", "", "\n", "NYC", "\n", "", "\n", "", "\n", ""}, thinkValue: nil, expectedContent: "Let me check the weather.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "NYC"}, }, }, }, }, { name: "tool call tag split character by character", chunks: []string{"<", "t", "o", "o", "l", "_", "c", "a", "l", "l", ">", "\n", "<", "f", "u", "n", "c", "t", "i", "o", "n", "=", "t", "e", "s", "t", ">", "\n", "<", "/", "f", "u", "n", "c", "t", "i", "o", "n", ">", "\n", "<", "/", "t", "o", "o", "l", "_", "c", "a", "l", "l", ">"}, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "test", Arguments: map[string]any{}, }, }, }, }, { name: "thinking close tag split character by character", chunks: []string{"I", "'", "m", " ", "t", "h", "i", "n", "k", "i", "n", "g", ".", ".", ".", "<", "/", "t", "h", "i", "n", "k", ">", "\n", "D", "o", "n", "e", "!"}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "I'm thinking...", expectedContent: "Done!", }, { name: "multiple whitespace after think tag - separate chunks", chunks: []string{"Thinking...", "", "\n", "\n", " ", "Content here."}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Thinking...", expectedContent: "Content here.", }, { name: "tool call with multiple parameters - streaming", chunks: []string{"\n", "", "\n\n", "SFO\n", "", "\n\nNYC", "\n", "\n\n", ""}, thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "book_flight", Arguments: map[string]any{ "from": "SFO", "to": "NYC", }, }, }, }, }, { name: "thinking then content then tool call - streaming", chunks: []string{"Ana", "lyzing", " your", " request", "...", "\n", "I'll", " check", " that", " for", " you", ".", "\n", "\n", "\n", "\n", "test", " query", "\n\n", "\n", ""}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Analyzing your request...", expectedContent: "I'll check that for you.", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "search", Arguments: map[string]any{"query": "test query"}, }, }, }, }, { name: "multiple tool calls - streaming", chunks: []string{ "", "\n", "", "\n", "\n", "San Fran", "cisco\n", "", "\n", "", "\n", "", "\n", "\n", "\n", "\nNew", " York\n", "\n", "\n", "", }, thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "San Francisco"}, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "New York"}, }, }, }, }, { name: "tool call with multiline parameter - streaming", chunks: []string{"\n", "\n", "\n", "Line 1", "\nLine", " 2\n", "Line 3", "\n\n", "\n", ""}, thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "create_note", Arguments: map[string]any{"content": "Line 1\nLine 2\nLine 3"}, }, }, }, }, { name: "empty thinking block", chunks: []string{"", "\n", "Just content."}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "", expectedContent: "Just content.", }, { name: "empty input chunks interspersed", chunks: []string{"Hello", "", " ", "", "world", "", "!"}, thinkValue: nil, expectedContent: "Hello world!", }, { name: "tool call immediately after think close - no content", chunks: []string{"Analyzing...", "", "\n", "", "\n\n\n", ""}, thinkValue: &api.ThinkValue{Value: true}, expectedThinking: "Analyzing...", expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "test", Arguments: map[string]any{}, }, }, }, }, { name: "tool call with empty parameter value", chunks: []string{"\n\n\n", "\n\n\n"}, thinkValue: nil, expectedCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "test", Arguments: map[string]any{"name": ""}, }, }, }, }, { name: "partial tool call tag at end - buffered", chunks: []string{"Here's some content", "