package renderers import ( "testing" "github.com/google/go-cmp/cmp" "github.com/ollama/ollama/api" ) func TestNemotron3NanoRenderer(t *testing.T) { tests := []struct { name string msgs []api.Message tools []api.Tool thinkValue *api.ThinkValue isThinking bool expected string }{ { name: "basic user message - thinking mode", msgs: []api.Message{ {Role: "user", Content: "Hello!"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHello!<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "basic user message - no thinking", msgs: []api.Message{ {Role: "user", Content: "Hello!"}, }, isThinking: false, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHello!<|im_end|>\n" + "<|im_start|>assistant\n", }, { name: "with system message", msgs: []api.Message{ {Role: "system", Content: "You are a helpful assistant."}, {Role: "user", Content: "Hello!"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\nYou are a helpful assistant.<|im_end|>\n" + "<|im_start|>user\nHello!<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "multi-turn conversation", msgs: []api.Message{ {Role: "user", Content: "Hi"}, {Role: "assistant", Content: "Hello! How can I help?"}, {Role: "user", Content: "Tell me a joke"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHi<|im_end|>\n" + "<|im_start|>assistant\nHello! How can I help?<|im_end|>\n" + "<|im_start|>user\nTell me a joke<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "with tools", msgs: []api.Message{ {Role: "user", Content: "What's the weather in Paris?"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get the current weather", Parameters: api.ToolFunctionParameters{ Type: "object", Required: []string{"city"}, Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "The city name"}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_weather\n" + "Get the current weather\n" + "\n" + "\ncity\nstring\nThe city name\n\n" + "[\"city\"]\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nWhat's the weather in Paris?<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "tool call with response", msgs: []api.Message{ {Role: "user", Content: "What's the weather in Paris?"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, }, }, {Role: "tool", Content: "Sunny, 72F"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Description: "Get the current weather", Parameters: api.ToolFunctionParameters{ Type: "object", Required: []string{"city"}, Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}, Description: "The city name"}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_weather\n" + "Get the current weather\n" + "\n" + "\ncity\nstring\nThe city name\n\n" + "[\"city\"]\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nWhat's the weather in Paris?<|im_end|>\n" + "<|im_start|>assistant\n\n" + "\n\n\nParis\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nSunny, 72F\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "assistant with content and tool call", msgs: []api.Message{ {Role: "user", Content: "What's the weather?"}, { Role: "assistant", Content: "Let me check that for you.", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_weather\n" + "\n" + "\ncity\nstring\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nWhat's the weather?<|im_end|>\n" + "<|im_start|>assistant\nLet me check that for you.\n" + "\n\n\nParis\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nSunny\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "thinking in history is truncated", msgs: []api.Message{ {Role: "user", Content: "Hi"}, {Role: "assistant", Content: "Hello!", Thinking: "Let me think about this..."}, {Role: "user", Content: "How are you?"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHi<|im_end|>\n" + "<|im_start|>assistant\nHello!<|im_end|>\n" + "<|im_start|>user\nHow are you?<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "parallel tool calls", msgs: []api.Message{ {Role: "user", Content: "Weather in Paris and London?"}, { Role: "assistant", ToolCalls: []api.ToolCall{ { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "Paris"}, }, }, { Function: api.ToolCallFunction{ Name: "get_weather", Arguments: map[string]any{"city": "London"}, }, }, }, }, {Role: "tool", Content: "Sunny"}, {Role: "tool", Content: "Rainy"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_weather\n" + "\n" + "\ncity\nstring\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nWeather in Paris and London?<|im_end|>\n" + "<|im_start|>assistant\n\n" + "\n\n\nParis\n\n\n\n" + "\n\n\nLondon\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nSunny\n\n\nRainy\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "thinking disabled even when model supports it", msgs: []api.Message{ {Role: "user", Content: "Hello!"}, }, isThinking: true, // model supports thinking thinkValue: nil, // but user didn't request it expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHello!<|im_end|>\n" + "<|im_start|>assistant\n", }, { name: "complex message history with thinking, tools, tool calls, tool results and content", msgs: []api.Message{ {Role: "user", Content: "What's the weather in Paris and London? Also, what's 2+2?"}, {Role: "assistant", Content: "", Thinking: "I need to check the weather for both cities and calculate 2+2. Let me start with the weather calls.", ToolCalls: []api.ToolCall{ {Function: api.ToolCallFunction{Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "Paris"}}}, {Function: api.ToolCallFunction{Name: "get_weather", Arguments: api.ToolCallFunctionArguments{"city": "London"}}}, }}, {Role: "tool", Content: "Sunny, 22°C", ToolCallID: "call1"}, {Role: "tool", Content: "Rainy, 15°C", ToolCallID: "call2"}, {Role: "assistant", Content: "", Thinking: "Now I have the weather data. Let me calculate 2+2.", ToolCalls: []api.ToolCall{ {Function: api.ToolCallFunction{Name: "calculate", Arguments: api.ToolCallFunctionArguments{"expression": "2+2"}}}, }}, {Role: "tool", Content: "4", ToolCallID: "call3"}, {Role: "assistant", Content: "Based on the weather data, Paris is sunny at 22°C and London is rainy at 15°C. Also, 2+2 equals 4.", Thinking: "Perfect! I have all the information needed to provide a complete answer."}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_weather", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "city": {Type: api.PropertyType{"string"}}, }, }, }, }, { Type: "function", Function: api.ToolFunction{ Name: "calculate", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "expression": {Type: api.PropertyType{"string"}}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_weather\n" + "\n" + "\ncity\nstring\n\n" + "\n\n" + "\ncalculate\n" + "\n" + "\nexpression\nstring\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nWhat's the weather in Paris and London? Also, what's 2+2?<|im_end|>\n" + "<|im_start|>assistant\n" + "\nI need to check the weather for both cities and calculate 2+2. Let me start with the weather calls.\n\n" + "\n\n\nParis\n\n\n\n" + "\n\n\nLondon\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nSunny, 22°C\n\n\nRainy, 15°C\n\n<|im_end|>\n" + "<|im_start|>assistant\n" + "\nNow I have the weather data. Let me calculate 2+2.\n\n" + "\n\n\n2+2\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\n4\n\n<|im_end|>\n" + "<|im_start|>assistant\n" + "\nPerfect! I have all the information needed to provide a complete answer.\n\n" + "Based on the weather data, Paris is sunny at 22°C and London is rainy at 15°C. Also, 2+2 equals 4.<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "empty messages list", msgs: []api.Message{}, isThinking: false, expected: "<|im_start|>system\n<|im_end|>\n<|im_start|>assistant\n", }, { name: "tool result with JSON content", msgs: []api.Message{ {Role: "user", Content: "Get user info"}, { Role: "assistant", ToolCalls: []api.ToolCall{ {Function: api.ToolCallFunction{Name: "get_user", Arguments: map[string]any{"id": "123"}}}, }, }, {Role: "tool", Content: `{"name": "John", "age": 30, "active": true}`}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "get_user", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{"id": {Type: api.PropertyType{"string"}}}, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\nget_user\n\n" + "\nid\nstring\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nGet user info<|im_end|>\n" + "<|im_start|>assistant\n\n" + "\n\n\n123\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\n{\"name\": \"John\", \"age\": 30, \"active\": true}\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "assistant message with only thinking no content", msgs: []api.Message{ {Role: "user", Content: "Think about this"}, {Role: "assistant", Thinking: "Deep thoughts here...", Content: ""}, {Role: "user", Content: "What did you think?"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nThink about this<|im_end|>\n" + "<|im_start|>assistant\n<|im_end|>\n" + "<|im_start|>user\nWhat did you think?<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "tool call with complex nested argument", msgs: []api.Message{ {Role: "user", Content: "Create data"}, { Role: "assistant", ToolCalls: []api.ToolCall{ {Function: api.ToolCallFunction{ Name: "create", Arguments: map[string]any{ "data": map[string]any{"nested": "value", "count": 42}, }, }}, }, }, {Role: "tool", Content: "Created"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "create", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{"data": {Type: api.PropertyType{"object"}}}, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\ncreate\n\n" + "\ndata\nobject\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nCreate data<|im_end|>\n" + "<|im_start|>assistant\n\n" + "\n\n\n{\"count\":42,\"nested\":\"value\"}\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nCreated\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "content explaining the format itself", msgs: []api.Message{ {Role: "user", Content: "How do I format a tool call?"}, {Role: "assistant", Content: "To call a tool, use tags with inside."}, {Role: "user", Content: "Thanks!"}, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n<|im_end|>\n" + "<|im_start|>user\nHow do I format a tool call?<|im_end|>\n" + "<|im_start|>assistant\nTo call a tool, use tags with inside.<|im_end|>\n" + "<|im_start|>user\nThanks!<|im_end|>\n" + "<|im_start|>assistant\n\n", }, { name: "unicode in content and tool args", msgs: []api.Message{ {Role: "user", Content: "Translate 你好"}, { Role: "assistant", ToolCalls: []api.ToolCall{ {Function: api.ToolCallFunction{Name: "translate", Arguments: map[string]any{"text": "你好"}}}, }, }, {Role: "tool", Content: "Hello"}, }, tools: []api.Tool{ { Type: "function", Function: api.ToolFunction{ Name: "translate", Parameters: api.ToolFunctionParameters{ Type: "object", Properties: map[string]api.ToolProperty{ "text": {Type: api.PropertyType{"string"}}, }, }, }, }, }, isThinking: true, thinkValue: &api.ThinkValue{Value: true}, expected: "<|im_start|>system\n" + "# Tools\n\nYou have access to the following functions:\n\n\n" + "\ntranslate\n\n" + "\ntext\nstring\n\n" + "\n\n\n\n" + "If you choose to call a function ONLY reply in the following format with NO suffix:\n\n" + "\n\n\nvalue_1\n\n" + "\nThis is the value for the second parameter\nthat can span\nmultiple lines\n" + "\n\n\n\n\nReminder:\n" + "- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n" + "- Required parameters MUST be specified\n" + "- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n" + "- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n" + "<|im_end|>\n" + "<|im_start|>user\nTranslate 你好<|im_end|>\n" + "<|im_start|>assistant\n\n" + "\n\n\n你好\n\n\n\n<|im_end|>\n" + "<|im_start|>user\n\nHello\n\n<|im_end|>\n" + "<|im_start|>assistant\n\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { renderer := &Nemotron3NanoRenderer{IsThinking: tt.isThinking} rendered, err := renderer.Render(tt.msgs, tt.tools, tt.thinkValue) if err != nil { t.Fatal(err) } if diff := cmp.Diff(rendered, tt.expected); diff != "" { t.Errorf("mismatch (-got +want):\n%s", diff) } }) } }