nemotron_h: support the Nemotron 3.5 prompt layout (#17672)

Select the 3.5 parser and renderer from its checkpoint template, preserve its prompt semantics, and map medium reasoning effort to the final-user annotation expected by the reference template.

Exercise parser and renderer registration, create-time metadata inference, and exact Jinja parity so created models cannot silently fall back to the Nemotron 3 renderer.
This commit is contained in:
Daniel Hiltgen
2026-08-11 06:18:51 -07:00
committed by GitHub
parent 400164d47c
commit 96fb6d2fa9
10 changed files with 412 additions and 10 deletions
+1 -1
View File
@@ -76,7 +76,7 @@ func ParserForName(name string) Parser {
return &Olmo3Parser{}
case "olmo3-think":
return &Olmo3ThinkParser{}
case "nemotron-3-nano":
case "nemotron-3-nano", "nemotron-3.5-nano":
return &Nemotron3NanoParser{}
case "functiongemma":
return &FunctionGemmaParser{}
+2
View File
@@ -67,6 +67,8 @@ func TestBuiltInParsersStillWork(t *testing.T) {
{"qwen3.5"},
{"ornith"},
{"harmony"},
{"nemotron-3-nano"},
{"nemotron-3.5-nano"},
}
for _, tt := range tests {
@@ -0,0 +1,141 @@
package renderers
import (
"os"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/ollama/ollama/api"
)
// Expectations captured from testdata/nemotron35_chat_template.jinja2, so they
// are the template's output and not the renderer's. The default run compares
// the v35 renderer against them; VERIFY_JINJA2=1 re-checks them against the
// template itself.
const nemotron35Template = "testdata/nemotron35_chat_template.jinja2"
var nemotron35Expected = map[string]string{
"medium effort": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello\n\n{reasoning effort: efficient}<|im_end|>\n<|im_start|>assistant\n<think>\n",
"no system default thinking on": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think>\n",
"no system explicit thinking off": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>",
"literal endthink does not enable thinking": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nliteral </think> only<|im_end|>\n<|im_start|>assistant\n<think></think>",
"user no think toggle": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHello /no_think<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system think toggle overrides false": "<|im_start|>system\nPolicy /think<|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>",
"later toggle wins": "<|im_start|>system\nPolicy /no_think<|im_end|>\n<|im_start|>user\nActually /think<|im_end|>\n<|im_start|>assistant\n<think></think>",
"system sanitizes toggles but preserves closing tag": "<|im_start|>system\nA /think B /no_think C </think><|im_end|>\n<|im_start|>user\nHello<|im_end|>\n<|im_start|>assistant\n<think></think>",
"assistant plain content adds empty think block": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think></think>Hello there<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant reasoning content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think>\nNeed to think</think>Answer<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant preserves existing think tags": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think>kept</think>Answer<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tools without system": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\nUse a tool<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system with tools": "<|im_start|>system\nFollow policy.\n\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\nUse a tool<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant tool call with content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nWeather?<|im_end|>\n<|im_start|>assistant\n<think></think>Checking now.\n<tool_call>\n<function=get_weather>\n<parameter=city>\nParis\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant tool call with structured arguments": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nCreate data<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=create>\n<parameter=payload>\n{\"count\": 42, \"nested\": {\"value\": \"ok\"}}\n</parameter>\n<parameter=tags>\n[\"a\", \"b\"]\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant tool call truncated with reasoning": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nWeather?<|im_end|>\n<|im_start|>assistant\n<think></think>Checking now.\n<tool_call>\n<function=get_weather>\n<parameter=city>\nParis\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\nAnd tomorrow?<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant tool call truncated open think only": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nWeather?<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=get_weather>\n<parameter=city>\nParis\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\nAnd tomorrow?<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant tool call empty content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nWeather?<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=get_weather>\n<parameter=city>\nParis\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant truncated with think pair": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think></think>Visible<|im_end|>\n<|im_start|>user\nNext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant truncated reasoning content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think></think>Visible<|im_end|>\n<|im_start|>user\nNext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant truncated plain content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think></think>Visible<|im_end|>\n<|im_start|>user\nNext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant truncated empty content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>assistant\n<think></think><|im_end|>\n<|im_start|>user\nNext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"consecutive tool messages grouped": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nDo work<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=step>\n<parameter=value>\n1\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\none\n</tool_response>\n<tool_response>\ntwo\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"fallback role": "<|im_start|>system\n<|im_end|>\n<|im_start|>developer\nCustom role content<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool without description": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>ping</name>\n<parameters>\n<parameter>\n<name>host</name>\n<type>string</type>\n<description>Host</description>\n</parameter>\n<required>[\"host\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool without parameters": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>now</name>\n<description>Current time</description>\n<parameters>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool param without type": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>q</name>\n<description>Query</description>\n<parameters>\n<parameter>\n<name>x</name>\n<description>no type</description>\n</parameter>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool param with unhandled keys": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>q</name>\n<description>Query</description>\n<parameters>\n<parameter>\n<name>x</name>\n<type>string</type>\n<description>X</description>\n<items>[\"a\", \"b\"]</items>\n</parameter>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool without required list": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>q</name>\n<description>Query</description>\n<parameters>\n<parameter>\n<name>x</name>\n<type>string</type>\n</parameter>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system message after first turn": "<|im_start|>system\nFirst system<|im_end|>\n<|im_start|>user\nHi<|im_end|>\n<|im_start|>system\nSecond system<|im_end|>\n<|im_start|>user\nAgain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool message last": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nresult\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool run then assistant": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\none\n</tool_response>\n<tool_response>\ntwo\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think></think>done<|im_end|>\n<|im_start|>user\nnext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"two user turns truncates earlier thinking": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nfirst<|im_end|>\n<|im_start|>assistant\n<think></think>answer one<|im_end|>\n<|im_start|>user\nsecond<|im_end|>\n<|im_start|>assistant\n<think>\nlate reasoning</think>answer two<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant reasoning with tool call": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\ndecide</think>calling\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system only no user": "<|im_start|>system\nOnly system<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant first no user": "<|im_start|>system\n<|im_end|>\n<|im_start|>assistant\n<think></think>unprompted<|im_end|>\n<|im_start|>user\nnow ask<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool message first": "<|im_start|>system\n<|im_end|>\n<tool_response>\norphan result\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"multiple tools": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>alpha</name>\n<description>First</description>\n<parameters>\n<parameter>\n<name>a</name>\n<type>string</type>\n</parameter>\n</parameters>\n</function>\n<function>\n<name>beta</name>\n<description>Second</description>\n<parameters>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call multiple arguments": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=one>\n1\n</parameter>\n<parameter=two>\n2\n</parameter>\n<parameter=three>\nTrue\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant fully empty": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think><|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"deep multi turn truncation": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nu1<|im_end|>\n<|im_start|>assistant\n<think></think>a1<|im_end|>\n<|im_start|>user\nu2<|im_end|>\n<|im_start|>assistant\n<think></think>a2<|im_end|>\n<|im_start|>user\nu3<|im_end|>\n<|im_start|>assistant\n<think>\nt3</think>a3<|im_end|>\n<|im_start|>assistant\n<think>\n",
"content think tags with tool call": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>inline</think>after\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool response json": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\n{\"temp\": 14, \"unit\": \"c\"}\n</tool_response>\n<|im_end|>\n<|im_start|>user\nand?<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant closing tag only": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\nstray </think> tail<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call scalar arg types": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=int>\n7\n</parameter>\n<parameter=float>\n2.5\n</parameter>\n<parameter=wholefloat>\n2\n</parameter>\n<parameter=boolfalse>\nFalse\n</parameter>\n<parameter=empty>\n\n</parameter>\n<parameter=zero>\n0\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call container arg": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=obj>\n{\"k\": true}\n</parameter>\n<parameter=arr>\n[1, \"two\", false]\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant two tool calls": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=first>\n<parameter=a>\n1\n</parameter>\n</function>\n</tool_call>\n<tool_call>\n<function=second>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call no arguments": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=bare>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"multiline content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nline one\nline two\n<|im_end|>\n<|im_start|>assistant\n<think></think>reply one\nreply two<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"unicode content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nhéllo <b>&amp;</b> 日本語 é<|im_end|>\n<|im_start|>assistant\n<think>\n",
"empty user content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system with markup": "<|im_start|>system\n<tool_call>not real</tool_call><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tools registered with history call": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=search_docs>\n<parameter=query>\napi\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nfound\n</tool_response>\n<|im_end|>\n<|im_start|>user\nthanks<|im_end|>\n<|im_start|>assistant\n<think>\n",
"think unset with history": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nu1<|im_end|>\n<|im_start|>assistant\n<think></think>a1<|im_end|>\n<|im_start|>user\nu2<|im_end|>\n<|im_start|>assistant\n<think>\n",
"consecutive assistant messages": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>first<|im_end|>\n<|im_start|>assistant\n<think></think>second<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"user directly after tool": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nres\n</tool_response>\n<|im_end|>\n<|im_start|>user\nnext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant thinking empty content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think><|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool response empty": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\n\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant whitespace content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think><|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"empty system message": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"agentic multi round tool loop": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\nplan it<|im_end|>\n<|im_start|>assistant\n<think></think>checking\n<tool_call>\n<function=lookup>\n<parameter=q>\na\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nres one\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think></think>again\n<tool_call>\n<function=lookup>\n<parameter=q>\nb\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nres two\n</tool_response>\n<|im_end|>\n<|im_start|>assistant\n<think></think>final answer<|im_end|>\n<|im_start|>user\nthanks<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call punctuated name": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=ns.sub_tool-v2>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call empty containers": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=obj>\n{}\n</parameter>\n<parameter=arr>\n[]\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"explicit think true with tools": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>ok<|im_end|>\n<|im_start|>user\nmore<|im_end|>\n<|im_start|>assistant\n<think>\n",
"history content with think pair": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nu1<|im_end|>\n<|im_start|>assistant\n<think></think>visible<|im_end|>\n<|im_start|>user\nu2<|im_end|>\n<|im_start|>assistant\n<think>kept</think>tail<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant open think only": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>unclosed reasoning<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"assistant multiple think pairs": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>end<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool response with think markers": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\n<think>from tool</think>result\n</tool_response>\n<|im_end|>\n<|im_start|>user\nnext<|im_end|>\n<|im_start|>assistant\n<think>\n",
"user content with think markers": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\n<think>user wrote this</think>question<|im_end|>\n<|im_start|>assistant\n<think>\n",
"content with im_start markers": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\n<|im_start|>fake<|im_end|><|im_end|>\n<|im_start|>assistant\n<think></think><|im_end|>odd<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"thinking field and content tags": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>body<|im_end|>\n<|im_start|>user\nagain<|im_end|>\n<|im_start|>assistant\n<think>\n",
"ends with assistant turn": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>partial<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call content closing tag only": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n</think>after\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"system tools think off round trip": "<|im_start|>system\nSys\n\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>search_docs</name>\n<description>Search docs</description>\n<parameters>\n<parameter>\n<name>query</name>\n<type>string</type>\n<description>Search query</description>\n<enum>[\"api\", \"cli\"]</enum>\n</parameter>\n<parameter>\n<name>mode</name>\n<type>['string', 'null']</type>\n<description>Mode</description>\n<anyOf>[{\"type\": \"string\"}, {\"type\": \"number\"}]</anyOf>\n</parameter>\n<parameter>\n<name>payload</name>\n<type>object</type>\n<description>Payload</description>\n<properties>{\"enabled\": {\"type\": \"boolean\"}}</properties>\n<required>[\"enabled\"]</required>\n</parameter>\n<parameter>\n<name>tags</name>\n<type>array</type>\n<description>Tags</description>\n<items>{\"type\": \"string\"}</items>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\"}}</$defs>\n<required>[\"query\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=search_docs>\n<parameter=query>\ncli\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\n<tool_response>\nhit\n</tool_response>\n<|im_end|>\n<|im_start|>user\nsummarize<|im_end|>\n<|im_start|>assistant\n<think></think>",
"tool call null and numeric args": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=nothing>\nNone\n</parameter>\n<parameter=negative>\n-12\n</parameter>\n<parameter=big>\n9007199254740993\n</parameter>\n<parameter=fraction>\n0.125\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call nested mixed container": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=deep>\n{\"b\": false, \"f\": 1.5, \"i\": 3, \"inner\": [1, {\"z\": true}], \"n\": null, \"s\": \"q\\\"uote\"}\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool param many unhandled keys": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>q</name>\n<description>Query</description>\n<parameters>\n<parameter>\n<name>x</name>\n<type>string</type>\n<description>X</description>\n<anyOf>[{\"type\": \"null\"}]</anyOf>\n<items>{\"minimum\": 0, \"type\": \"number\"}</items>\n</parameter>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call content think pair only": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\nu1<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>user\nu2<|im_end|>\n<|im_start|>assistant\n<think>y</think>z\n<tool_call>\n<function=g>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call quoted string arg": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=s>\nline\nbreak \"quoted\"\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema nested and items": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>deep</name>\n<description>Nested schema</description>\n<parameters>\n<parameter>\n<name>outer</name>\n<type>object</type>\n<description>Outer object</description>\n<enum>[\"a\", \"b\"]</enum>\n<items>{\"type\": \"integer\"}</items>\n<properties>{\"inner\": {\"type\": \"string\", \"description\": \"Inner\"}, \"flag\": {\"type\": [\"boolean\", \"null\"]}}</properties>\n<required>[\"inner\"]</required>\n</parameter>\n<items>{\"type\": \"string\"}</items>\n<required>[\"outer\"]</required>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call unsigned numeric container": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=nums>\n{\"f32\": 1.5, \"i64\": -9, \"u\": 7, \"u8\": 255}\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call whitespace content": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema defs property map": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>defs</name>\n<parameters>\n<parameter>\n<name>x</name>\n<type>string</type>\n</parameter>\n<$defs>{\"shared\": {\"type\": \"string\", \"description\": \"Shared\"}}</$defs>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema defs rich property": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>rich</name>\n<parameters>\n<parameter>\n<name>p</name>\n<type>string</type>\n</parameter>\n<$defs>{\"node\": {\"anyOf\": [{\"type\": \"string\"}, {\"type\": \"null\"}], \"type\": \"object\", \"items\": {\"type\": \"integer\"}, \"description\": \"Node\", \"enum\": [\"x\", \"y\"], \"properties\": {\"child\": {\"type\": \"string\"}}, \"required\": [\"child\"]}}</$defs>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema items property type": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>pt</name>\n<parameters>\n<parameter>\n<name>p</name>\n<type>string</type>\n</parameter>\n<items>[\"string\", \"null\"]</items>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema nil defs map": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>niltest</name>\n<parameters>\n<parameter>\n<name>p</name>\n<type>string</type>\n</parameter>\n<$defs>None</$defs>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool call struct argument": "<|im_start|>system\n<|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think></think>\n<tool_call>\n<function=f>\n<parameter=custom>\n{\"a\": \"x\", \"b\": 2}\n</parameter>\n<parameter=list>\n[1, 2, 3]\n</parameter>\n</function>\n</tool_call>\n<|im_end|>\n<|im_start|>assistant\n<think>\n",
"tool schema nested nil properties": "<|im_start|>system\n# Tools\n\nYou have access to the following functions:\n\n<tools>\n<function>\n<name>nested</name>\n<parameters>\n<parameter>\n<name>p</name>\n<type>string</type>\n</parameter>\n<$defs>{\"node\": {\"type\": \"object\", \"required\": [\"q\"]}}</$defs>\n</parameters>\n</function>\n</tools>\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT><|im_end|>\n<|im_start|>user\ngo<|im_end|>\n<|im_start|>assistant\n<think>\n",
}
func TestNemotron35RendererMatchesReference(t *testing.T) {
verifyJinja2 := os.Getenv("VERIFY_JINJA2") != ""
cases := append(nemotronReferenceCases(), nemotronExtraCases()...)
cases = append(cases, nemotronReferenceCase{
name: "medium effort",
messages: []api.Message{{Role: "user", Content: "Hello"}},
think: &api.ThinkValue{Value: "medium"},
})
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
want, ok := nemotron35Expected[tc.name]
if !ok {
t.Fatalf("no 3.5 expectation for %q: every reference case must cover both flavors", tc.name)
}
got, err := RenderWithRenderer("nemotron-3.5-nano", tc.messages, tc.tools, tc.think)
if err != nil {
t.Fatalf("Render() error = %v", err)
}
if diff := cmp.Diff(want, got); diff != "" {
t.Fatalf("v35 renderer mismatch (-want +got):\n%s", diff)
}
if verifyJinja2 {
jinja2Output := renderNemotronWithJinja2(t, nemotron35Template, tc.messages, tc.tools, tc.think)
if diff := cmp.Diff(want, jinja2Output); diff != "" {
t.Fatalf("3.5 template mismatch (-want +got):\n%s", diff)
}
}
})
}
}
+26 -5
View File
@@ -11,7 +11,10 @@ import (
"github.com/ollama/ollama/api"
)
type Nemotron3NanoRenderer struct{}
type Nemotron3NanoRenderer struct {
// v35 renders the Nemotron 3.5 prompt layout.
v35 bool
}
func (r *Nemotron3NanoRenderer) LeadingBOS() string {
return ""
@@ -22,6 +25,7 @@ func (r *Nemotron3NanoRenderer) Render(messages []api.Message, tools []api.Tool,
imageOffset := 0
enableThinking := r.resolveThinking(messages, thinkValue)
mediumEffort := r.v35 && thinkValue != nil && thinkValue.IsString() && thinkValue.String() == "medium"
// Extract system message if present
var systemMessage string
@@ -74,9 +78,13 @@ func (r *Nemotron3NanoRenderer) Render(messages []api.Message, tools []api.Tool,
case "user", "system":
sb.WriteString("<|im_start|>" + message.Role + "\n")
// The template strips toggle text from user and system turns
// alike, keeping any real closing tag intact.
sb.WriteString(strings.TrimSpace(stripThinkToggles(r.renderMessageContent(message, imageOffset))))
content := r.renderMessageContent(message, imageOffset)
if !r.v35 {
content = strings.TrimSpace(stripThinkToggles(content))
} else if message.Role == "user" && i == lastUserIdx && mediumEffort {
content += "\n\n{reasoning effort: efficient}"
}
sb.WriteString(content)
imageOffset += len(message.Images)
sb.WriteString("<|im_end|>\n")
@@ -175,6 +183,9 @@ func (r *Nemotron3NanoRenderer) renderTools(tools []api.Tool) string {
func (r *Nemotron3NanoRenderer) buildContent(message api.Message) string {
content := nemotron3NanoRenderContent(message.Content)
if message.Thinking != "" {
if r.v35 {
return "<think>\n" + message.Thinking + "</think>" + content
}
return "<think>\n" + message.Thinking + "\n</think>\n" + content
}
if !strings.Contains(content, "<think>") && !strings.Contains(content, "</think>") {
@@ -305,6 +316,11 @@ func nemotron3NanoRenderContent(content any) string {
func (r *Nemotron3NanoRenderer) resolveThinking(messages []api.Message, thinkValue *api.ThinkValue) bool {
enableThinking := thinkValue == nil || thinkValue.Bool()
// Under v35 only the request controls thinking; "/think" and "/no_think"
// in a prompt are ordinary text.
if r.v35 {
return enableThinking
}
for _, message := range messages {
if message.Role != "user" && message.Role != "system" {
continue
@@ -320,7 +336,12 @@ func (r *Nemotron3NanoRenderer) resolveThinking(messages []api.Message, thinkVal
}
func (r *Nemotron3NanoRenderer) sanitizeSystemMessage(content string) string {
return stripThinkToggles(nemotron3NanoRenderContent(content))
system := nemotron3NanoRenderContent(content)
// Under v35 the system message is passed through unaltered.
if r.v35 {
return system
}
return stripThinkToggles(system)
}
// stripThinkToggles removes the inline /think and /no_think markers the
@@ -450,12 +450,16 @@ func renderNemotronWithJinja2(t *testing.T, templateRel string, messages []api.M
}
thinking := "unset"
mediumEffort := "false"
if think != nil {
if think.Bool() {
thinking = "true"
} else {
thinking = "false"
}
if think.IsString() && think.String() == "medium" {
mediumEffort = "true"
}
}
repoRoot := nemotron3NanoRepoRoot(t)
@@ -467,7 +471,7 @@ import sys
from pathlib import Path
from transformers.utils.chat_template_utils import _compile_jinja_template
template_path, messages_json, tools_json, thinking = sys.argv[1:5]
template_path, messages_json, tools_json, thinking, medium_effort = sys.argv[1:6]
tmpl = _compile_jinja_template(Path(template_path).read_text())
kwargs = {
"messages": json.loads(messages_json),
@@ -479,10 +483,12 @@ if thinking == "true":
kwargs["enable_thinking"] = True
elif thinking == "false":
kwargs["enable_thinking"] = False
if medium_effort == "true":
kwargs["medium_effort"] = True
print(tmpl.render(**kwargs), end="")
`
cmd := exec.CommandContext(t.Context(), pythonPath, "-c", script, templatePath, string(msgsJSON), toolsJSON, thinking)
cmd := exec.CommandContext(t.Context(), pythonPath, "-c", script, templatePath, string(msgsJSON), toolsJSON, thinking, mediumEffort)
var stdout, stderr strings.Builder
cmd.Stdout = &stdout
cmd.Stderr = &stderr
+2
View File
@@ -93,6 +93,8 @@ func rendererForName(name string) Renderer {
return renderer
case "nemotron-3-nano":
return &Nemotron3NanoRenderer{}
case "nemotron-3.5-nano":
return &Nemotron3NanoRenderer{v35: true}
case "gemma4", "gemma4-small":
return &Gemma4Renderer{useImgTags: RenderImgTags}
case "gemma4-large":
+2
View File
@@ -38,6 +38,8 @@ func TestBuiltInRendererStillWorks(t *testing.T) {
}{
{name: "qwen3-coder"},
{name: "qwen3.5"},
{name: "nemotron-3-nano"},
{name: "nemotron-3.5-nano"},
}
messages := []api.Message{
+195
View File
@@ -0,0 +1,195 @@
{% macro render_extra_keys(json_dict, handled_keys) %}
{%- if json_dict is mapping %}
{%- for json_key in json_dict if json_key not in handled_keys %}
{%- if json_dict[json_key] is mapping or (json_dict[json_key] is sequence and json_dict[json_key] is not string) %}
{{- '\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | tojson | safe) ~ '</' ~ json_key ~ '>' }}
{%- else %}
{{-'\n<' ~ json_key ~ '>' ~ (json_dict[json_key] | string) ~ '</' ~ json_key ~ '>' }}
{%- endif %}
{%- endfor %}
{%- endif %}
{% endmacro %}
{%- set enable_thinking = enable_thinking if enable_thinking is defined else True %}
{%- set medium_effort = medium_effort if medium_effort is defined else False %}
{%- set truncate_history_thinking = truncate_history_thinking if truncate_history_thinking is defined else True %}
{%- set ns = namespace(last_user_idx = -1) %}
{%- set loop_messages = messages %}
{%- for m in loop_messages %}
{%- if m["role"] == "user" %}
{%- set ns.last_user_idx = loop.index0 %}
{%- endif %}
{%- endfor %}
{%- if messages[0]["role"] == "system" %}
{%- set system_message = messages[0]["content"] %}
{%- set loop_messages = messages[1:] %}
{%- else %}
{%- set system_message = "" %}
{%- set loop_messages = messages %}
{%- endif %}
{%- if not tools is defined %}
{%- set tools = [] %}
{%- endif %}
{%- set ns = namespace(last_user_idx = -1) %}
{%- for m in loop_messages %}
{%- if m["role"] == "user" %}
{%- set ns.last_user_idx = loop.index0 %}
{%- endif %}
{%- endfor %}
{%- if system_message is defined %}
{{- "<|im_start|>system\n" + system_message }}
{%- else %}
{%- if tools is iterable and tools | length > 0 %}
{{- "<|im_start|>system\n" }}
{%- endif %}
{%- endif %}
{%- if tools is iterable and tools | length > 0 %}
{%- if system_message is defined and system_message | length > 0 %}
{{- "\n\n" }}
{%- endif %}
{{- "# Tools\n\nYou have access to the following functions:\n\n" }}
{{- "<tools>" }}
{%- for tool in tools %}
{%- if tool.function is defined %}
{%- set tool = tool.function %}
{%- endif %}
{{- "\n<function>\n<name>" ~ tool.name ~ "</name>" }}
{%- if tool.description is defined %}
{{- '\n<description>' ~ (tool.description | trim) ~ '</description>' }}
{%- endif %}
{{- '\n<parameters>' }}
{%- if tool.parameters is defined and tool.parameters is mapping and tool.parameters.properties is defined and tool.parameters.properties is mapping %}
{%- for param_name, param_fields in tool.parameters.properties|items %}
{{- '\n<parameter>' }}
{{- '\n<name>' ~ param_name ~ '</name>' }}
{%- if param_fields.type is defined %}
{{- '\n<type>' ~ (param_fields.type | string) ~ '</type>' }}
{%- endif %}
{%- if param_fields.description is defined %}
{{- '\n<description>' ~ (param_fields.description | trim) ~ '</description>' }}
{%- endif %}
{%- if param_fields.enum is defined %}
{{- '\n<enum>' ~ (param_fields.enum | tojson | safe) ~ '</enum>' }}
{%- endif %}
{%- set handled_keys = ['name', 'type', 'description', 'enum'] %}
{{- render_extra_keys(param_fields, handled_keys) }}
{{- '\n</parameter>' }}
{%- endfor %}
{%- endif %}
{% set handled_keys = ['type', 'properties', 'required'] %}
{{- render_extra_keys(tool.parameters, handled_keys) }}
{%- if tool.parameters is defined and tool.parameters.required is defined %}
{{- '\n<required>' ~ (tool.parameters.required | tojson | safe) ~ '</required>' }}
{%- endif %}
{{- '\n</parameters>' }}
{%- set handled_keys = ['type', 'name', 'description', 'parameters'] %}
{{- render_extra_keys(tool, handled_keys) }}
{{- '\n</function>' }}
{%- endfor %}
{{- "\n</tools>" }}
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n<tool_call>\n<function=example_function_name>\n<parameter=example_parameter_1>\nvalue_1\n</parameter>\n<parameter=example_parameter_2>\nThis is the value for the second parameter\nthat can span\nmultiple lines\n</parameter>\n</function>\n</tool_call>\n\n<IMPORTANT>\nReminder:\n- Function calls MUST follow the specified format: an inner <function=...></function> block must be nested within <tool_call></tool_call> 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</IMPORTANT>' }}
{%- endif %}
{%- if system_message is defined %}
{{- '<|im_end|>\n' }}
{%- else %}
{%- if tools is iterable and tools | length > 0 %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- for message in loop_messages %}
{%- if message.role == "assistant" %}
{%- if message.reasoning_content is defined and message.reasoning_content is string and message.reasoning_content | trim | length > 0 %}
{%- set content = "<think>\n" ~ message.reasoning_content ~ "</think>" ~ (message.content | default('', true)) %}
{%- else %}
{%- set content = message.content | default('', true) %}
{%- if content is string -%}
{%- if '<think>' not in content and '</think>' not in content -%}
{%- set content = "<think></think>" ~ content -%}
{%- endif -%}
{%- else -%}
{%- set content = content -%}
{%- endif -%}
{%- endif %}
{%- if message.tool_calls is defined and message.tool_calls is iterable and message.tool_calls | length > 0 %}
{{- '<|im_start|>assistant\n' }}
{%- set include_content = not (truncate_history_thinking and loop.index0 < ns.last_user_idx) %}
{%- if content is string and content | trim | length > 0 %}
{%- if include_content %}
{{- (content | trim) ~ '\n' -}}
{%- else %}
{%- set c = (content | string) %}
{%- if '</think>' in c %}
{%- set c = c.split('</think>')[-1] %}
{%- elif '<think>' in c %}
{%- set c = c.split('<think>')[0] %}
{%- endif %}
{%- set c = "<think></think>" ~ c %}
{%- if c | length > 0 %}
{{- c ~ '\n' -}}
{%- endif %}
{%- endif %}
{%- else %}
{{- "<think></think>" -}}
{%- endif %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined %}
{%- set tool_call = tool_call.function %}
{%- endif %}
{{- '<tool_call>\n<function=' ~ tool_call.name ~ '>\n' -}}
{%- if tool_call.arguments is defined %}
{%- for args_name, args_value in tool_call.arguments|items %}
{{- '<parameter=' ~ args_name ~ '>\n' -}}
{%- set args_value = args_value | tojson | safe if args_value is mapping or (args_value is sequence and args_value is not string) else args_value | string %}
{{- args_value ~ '\n</parameter>\n' -}}
{%- endfor %}
{%- endif %}
{{- '</function>\n</tool_call>\n' -}}
{%- endfor %}
{{- '<|im_end|>\n' }}
{%- else %}
{%- if not (truncate_history_thinking and loop.index0 < ns.last_user_idx) %}
{{- '<|im_start|>assistant\n' ~ (content | default('', true) | string | trim) ~ '<|im_end|>\n' }}
{%- else %}
{%- set c = (content | default('', true) | string) %}
{%- if '<think>' in c and '</think>' in c %}
{%- set c = "<think></think>" ~ c.split('</think>')[-1] %}
{%- endif %}
{%- set c = c | trim %}
{%- if c | length > 0 %}
{{- '<|im_start|>assistant\n' ~ c ~ '<|im_end|>\n' }}
{%- else %}
{{- '<|im_start|>assistant\n<|im_end|>\n' }}
{%- endif %}
{%- endif %}
{%- endif %}
{%- elif message.role == "user" or message.role == "system" %}
{{- '<|im_start|>' + message.role + '\n' }}
{%- set content = message.content | string %}
{%- if message.role == "user" and loop.index0 == ns.last_user_idx and medium_effort %}
{{- content + '\n\n{reasoning effort: efficient}' }}
{%- else %}
{{- content }}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- elif message.role == "tool" %}
{%- if loop.previtem and loop.previtem.role != "tool" %}
{{- '<|im_start|>user\n' }}
{%- endif %}
{{- '<tool_response>\n' }}
{{- message.content }}
{{- '\n</tool_response>\n' }}
{%- if not loop.last and loop.nextitem.role != "tool" %}
{{- '<|im_end|>\n' }}
{%- elif loop.last %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- else %}
{{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>\n' }}
{%- endif %}
{%- endfor %}
{%- if add_generation_prompt %}
{%- if enable_thinking %}
{{- '<|im_start|>assistant\n<think>\n' }}
{%- else %}
{{- '<|im_start|>assistant\n<think></think>' }}
{%- endif %}
{%- endif %}
+18 -2
View File
@@ -600,6 +600,22 @@ func lagunaRendererParserName(modelDir string) string {
return "laguna"
}
func nemotronRendererParserName(modelDir string) string {
const v35Marker = "{reasoning effort: efficient}"
// Nemotron 3.5 publishes its updated template as a standalone file while
// tokenizer_config.json can retain the older template, so inspect both.
if data, err := os.ReadFile(filepath.Join(modelDir, "chat_template.jinja")); err == nil &&
strings.Contains(string(data), v35Marker) {
return "nemotron-3.5-nano"
}
if strings.Contains(readChatTemplate(modelDir), v35Marker) {
return "nemotron-3.5-nano"
}
return "nemotron-3-nano"
}
// getParserName returns the parser name for a model based on its architecture.
// This reads the config.json from the model directory and determines the appropriate parser.
func getParserName(modelDir string) string {
@@ -657,7 +673,7 @@ func parserNameForIdentifier(modelDir, s string) string {
// NemotronH_Nano_Omni_Reasoning_V3 for omni; model_type is nemotron_h,
// nemotron_h_moe, or the omni name. The two stems cover all of them.
case strings.Contains(s, "nemotronh") || strings.Contains(s, "nemotron_h"):
return "nemotron-3-nano"
return nemotronRendererParserName(modelDir)
default:
return ""
}
@@ -720,7 +736,7 @@ func rendererNameForIdentifier(modelDir, s string) string {
// NemotronH_Nano_Omni_Reasoning_V3 for omni; model_type is nemotron_h,
// nemotron_h_moe, or the omni name. The two stems cover all of them.
case strings.Contains(s, "nemotronh") || strings.Contains(s, "nemotron_h"):
return "nemotron-3-nano"
return nemotronRendererParserName(modelDir)
default:
return ""
}
+17
View File
@@ -65,6 +65,23 @@ func TestNemotronNanoOmniMetadataInference(t *testing.T) {
}
}
func TestNemotron35MetadataInference(t *testing.T) {
dir := t.TempDir()
config := `{"architectures":["NemotronHForCausalLM"],"model_type":"nemotron_h"}`
if err := os.WriteFile(filepath.Join(dir, "config.json"), []byte(config), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "chat_template.jinja"), []byte("{reasoning effort: efficient}"), 0o644); err != nil {
t.Fatal(err)
}
if got, want := getParserName(dir), "nemotron-3.5-nano"; got != want {
t.Fatalf("parser = %q, want %q", got, want)
}
if got, want := getRendererName(dir), "nemotron-3.5-nano"; got != want {
t.Fatalf("renderer = %q, want %q", got, want)
}
}
func TestConfigFromModelfile(t *testing.T) {
modelfile, err := parser.ParseFile(strings.NewReader(`
FROM ./model