diff --git a/common/chat-parser.cpp b/common/chat-parser.cpp
index 29819e48d3..060578f0b7 100644
--- a/common/chat-parser.cpp
+++ b/common/chat-parser.cpp
@@ -893,23 +893,6 @@ static void common_chat_parse_minimax_m2(common_chat_msg_parser & builder) {
builder.consume_reasoning_with_xml_tool_calls(form, "", "");
}
-static void common_chat_parse_qwen3_coder_xml(common_chat_msg_parser & builder) {
- static const xml_tool_call_format form = ([]() {
- xml_tool_call_format form {};
- form.scope_start = "";
- form.tool_start = "") != std::string::npos);
+
// Handle thinking tags appropriately based on inputs.enable_thinking
- if (string_ends_with(data.prompt, "\n")) {
+ if (supports_reasoning && string_ends_with(data.prompt, "\n")) {
if (!inputs.enable_thinking) {
data.prompt += "";
} else {
@@ -1538,19 +1540,21 @@ static common_chat_params common_chat_params_init_nemotron_v3(const common_chat_
}
data.preserved_tokens = {
- "",
- "",
"",
"",
};
+ if (supports_reasoning) {
+ data.preserved_tokens.insert(data.preserved_tokens.end(), {"", ""});
+ }
+
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
auto include_grammar = true;
auto parser = build_chat_peg_constructed_parser([&](auto & p) {
auto reasoning = p.eps();
- if (inputs.enable_thinking && extract_reasoning) {
+ if (supports_reasoning && inputs.enable_thinking && extract_reasoning) {
auto reasoning_content = p.reasoning(p.until("")) + ("" | p.end());
if (data.thinking_forced_open) {
reasoning = reasoning_content;
@@ -1888,38 +1892,6 @@ static common_chat_params common_chat_params_init_minimax_m2(const common_chat_t
return data;
}
-static common_chat_params common_chat_params_init_qwen3_coder_xml(const common_chat_template & tmpl, const struct templates_params & params) {
- common_chat_params data;
- data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
-
- data.prompt = apply(tmpl, params);
- data.format = COMMON_CHAT_FORMAT_QWEN3_CODER_XML;
-
- data.preserved_tokens = {
- "",
- "",
- "",
- "",
- };
-
- // build grammar for tool call
- static const xml_tool_call_format form {
- /* form.scope_start = */ "\n",
- /* form.tool_start = */ "\n",
- /* form.key_start = */ "\n",
- /* form.val_end = */ "\n\n",
- /* form.tool_end = */ "\n",
- /* form.scope_end = */ "",
- };
- build_grammar_xml_tool_call(data, params.tools, form);
-
- return data;
-}
-
static common_chat_params common_chat_params_init_kimi_k2(const common_chat_template & tmpl, const struct templates_params & params) {
common_chat_params data;
data.grammar_lazy = params.tools.is_array() && !params.tools.empty() && params.tool_choice != COMMON_CHAT_TOOL_CHOICE_REQUIRED;
@@ -3147,13 +3119,7 @@ static common_chat_params common_chat_templates_apply_jinja(
src.find(" support (Step-3.5-Flash, Nemotron 3 Nano) use the
- // Nemotron v3 PEG parser for streaming and schema-aware parameter parsing.
- // Qwen3-Coder has no in its template.
- if (src.find("") != std::string::npos) {
- return common_chat_params_init_nemotron_v3(tmpl, params);
- }
- return common_chat_params_init_qwen3_coder_xml(tmpl, params);
+ return common_chat_params_init_qwen3_coder(tmpl, params);
}
// Xiaomi MiMo format detection (must come before Hermes 2 Pro)
diff --git a/common/chat.h b/common/chat.h
index 1bf43f7261..6f0b9409ec 100644
--- a/common/chat.h
+++ b/common/chat.h
@@ -128,7 +128,6 @@ enum common_chat_format {
COMMON_CHAT_FORMAT_GLM_4_5,
COMMON_CHAT_FORMAT_MINIMAX_M2,
COMMON_CHAT_FORMAT_KIMI_K2,
- COMMON_CHAT_FORMAT_QWEN3_CODER_XML,
COMMON_CHAT_FORMAT_APRIEL_1_5,
COMMON_CHAT_FORMAT_XIAOMI_MIMO,
COMMON_CHAT_FORMAT_SOLAR_OPEN,
diff --git a/tests/test-chat.cpp b/tests/test-chat.cpp
index 1bef5b9f44..f3d19118b5 100644
--- a/tests/test-chat.cpp
+++ b/tests/test-chat.cpp
@@ -229,6 +229,20 @@ common_chat_tool python_tool {
"required": ["code"]
})",
};
+common_chat_tool todo_list_tool {
+ /* .name = */ "todo_list",
+ /* .description = */ "Create or update the todo list",
+ /* .parameters = */ R"({
+ "type": "object",
+ "properties": {
+ "todos": {
+ "type": "array",
+ "description": "List of TODO list items"
+ }
+ },
+ "required": ["todos"]
+ })",
+};
common_chat_tool code_interpreter_tool {
/* .name = */ "code_interpreter",
/* .description = */ "an ipython interpreter",
@@ -3018,542 +3032,6 @@ Hey there!<|im_end|>
);
}
- // Test Qwen3-Coder XML format
- {
- // Basic XML tool call parsing
- assert_msg_equals(
- message_assist_call,
- test_chat_parse(
- "\n"
- " \n"
- " \n"
- " 1\n"
- " \n"
- " \n"
- "",
- /* is_partial= */ false,
- {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}));
-
- // Multiple parameters with different types
- common_chat_msg expected_multi_param;
- expected_multi_param.role = "assistant";
- expected_multi_param.tool_calls = {
- { "complex_function", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}", "" }
- };
-
- test_parser_with_streaming(expected_multi_param,
- "\n"
- " \n"
- " \n"
- " John Doe\n"
- " \n"
- " \n"
- " 30\n"
- " \n"
- " \n"
- " true\n"
- " \n"
- " \n"
- " 95.5\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Special characters and Unicode
- common_chat_msg expected_special_chars;
- expected_special_chars.role = "assistant";
- expected_special_chars.tool_calls = {
- { "unicode_function", "{\"message\":\"Hello δΈη! π Special chars: @#$%^&*()\"}", "" }
- };
-
- test_parser_with_streaming(expected_special_chars,
- "\n"
- " \n"
- " \n"
- " Hello δΈη! π Special chars: @#$%^&*()\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Multiline content with newlines and indentation
- common_chat_msg expected_multiline;
- expected_multiline.role = "assistant";
- expected_multiline.tool_calls = {
- { "code_function", "{\"code\":\"def hello():\\n print(\\\"Hello, World!\\\")\\n return True\"}", "" }
- };
-
- test_parser_with_streaming(expected_multiline,
- "\n"
- " \n"
- " \n"
- "def hello():\n"
- " print(\"Hello, World!\")\n"
- " return True\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // JSON object as parameter value
- common_chat_msg expected_json_param;
- expected_json_param.role = "assistant";
- expected_json_param.tool_calls = {
- { "json_function", "{\"config\":{\"host\":\"localhost\",\"port\":8080,\"ssl\":false}}", "" }
- };
-
- test_parser_with_streaming(
- expected_json_param,
- "\n"
- " \n"
- " \n"
- " {\"host\": \"localhost\", \"port\": 8080, \"ssl\": false}\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Array as parameter value
- common_chat_msg expected_array_param;
- expected_array_param.role = "assistant";
- expected_array_param.tool_calls = {
- { "array_function", "{\"items\":[\"apple\",\"banana\",\"cherry\"]}", "" }
- };
-
- test_parser_with_streaming(
- expected_array_param,
- "\n"
- " \n"
- " \n"
- " [\"apple\", \"banana\", \"cherry\"]\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Empty parameter
- common_chat_msg expected_empty_param;
- expected_empty_param.role = "assistant";
- expected_empty_param.tool_calls = {
- { "empty_function", "{\"empty_param\":\"\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_empty_param,
- "\n"
- " \n"
- " \n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Boolean values (true/false)
- common_chat_msg expected_boolean;
- expected_boolean.role = "assistant";
- expected_boolean.tool_calls = {
- { "boolean_function", "{\"enabled\":true,\"debug\":false}", "" }
- };
-
- test_parser_with_streaming(
- expected_boolean,
- "\n"
- " \n"
- " \n"
- " true\n"
- " \n"
- " \n"
- " false\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Null value
- common_chat_msg expected_null;
- expected_null.role = "assistant";
- expected_null.tool_calls = {
- { "null_function", "{\"optional_param\":null}", "" }
- };
-
- test_parser_with_streaming(
- expected_null,
- "\n"
- " \n"
- " \n"
- " null\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Negative numbers and scientific notation
- common_chat_msg expected_numbers;
- expected_numbers.role = "assistant";
- expected_numbers.tool_calls = {
- { "math_function", "{\"negative\":-42,\"decimal\":-3.14,\"scientific\":1.23e-4}", "" }
- };
-
- test_parser_with_streaming(
- expected_numbers,
- "\n"
- " \n"
- " \n"
- " -42\n"
- " \n"
- " \n"
- " -3.14\n"
- " \n"
- " \n"
- " 1.23e-4\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // XML-like content in parameters (should be escaped)
- common_chat_msg expected_xml_content;
- expected_xml_content.role = "assistant";
- expected_xml_content.tool_calls = {
- { "xml_function", "{\"xml_content\":\"- value
\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_xml_content,
- "\n"
- " \n"
- " \n"
- " - value
\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Quotes and escape characters
- common_chat_msg expected_quotes;
- expected_quotes.role = "assistant";
- expected_quotes.tool_calls = {
- { "quote_function", "{\"message\":\"She said \\\"Hello!\\\" and left.\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_quotes,
- "\n"
- " \n"
- " \n"
- " She said \"Hello!\" and left.\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Long parameter value (simplified)
- std::string long_text = "This is a long text parameter that should test the parser's ability to handle larger amounts of text data.";
-
- common_chat_msg expected_long_text;
- expected_long_text.role = "assistant";
- expected_long_text.tool_calls = {
- { "long_function", "{\"long_text\":\"" + long_text + "\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_long_text,
- "\n"
- " \n"
- " \n"
- " " + long_text + "\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Mixed content with text before and after tool call
- common_chat_msg expected_mixed_content;
- expected_mixed_content.role = "assistant";
- expected_mixed_content.content = "I'll help you search for products. ";
- expected_mixed_content.tool_calls = {
- { "search_function", "{\"query\":\"laptops\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_mixed_content,
- "I'll help you search for products. \n"
- " \n"
- " \n"
- " laptops\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Compact format (no extra whitespace)
- common_chat_msg expected_compact;
- expected_compact.role = "assistant";
- expected_compact.tool_calls = {
- { "compact_function", "{\"param\":\"value\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_compact,
- "value",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Function name with underscores and numbers
- common_chat_msg expected_complex_name;
- expected_complex_name.role = "assistant";
- expected_complex_name.tool_calls = {
- { "get_user_data_v2", "{\"user_id\":12345}", "" }
- };
-
- test_parser_with_streaming(
- expected_complex_name,
- "\n"
- " \n"
- " \n"
- " 12345\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Parameter names with underscores and numbers
- common_chat_msg expected_complex_params;
- expected_complex_params.role = "assistant";
- expected_complex_params.tool_calls = {
- { "test_function", "{\"param_1\":\"value1\",\"param_2_name\":\"value2\",\"param3\":123}", "" }
- };
-
- test_parser_with_streaming(
- expected_complex_params,
- "\n"
- " \n"
- " \n"
- " value1\n"
- " \n"
- " \n"
- " value2\n"
- " \n"
- " \n"
- " 123\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Very deeply nested XML content in parameter
- common_chat_msg expected_deep_xml;
- expected_deep_xml.role = "assistant";
- expected_deep_xml.tool_calls = {
- { "xml_parser", "{\"xml\":\"deep content\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_deep_xml,
- "\n"
- " \n"
- " \n"
- " deep content\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Parameter with only whitespace
- common_chat_msg expected_whitespace_param;
- expected_whitespace_param.role = "assistant";
- expected_whitespace_param.tool_calls = {
- { "whitespace_function", "{\"spaces\":\"\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_whitespace_param,
- "\n"
- " \n"
- " \n"
- " \n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Parameter with tabs and mixed whitespace
- common_chat_msg expected_mixed_whitespace;
- expected_mixed_whitespace.role = "assistant";
- expected_mixed_whitespace.tool_calls = {
- { "tab_function", "{\"content\":\"line1\\n\\tindented line\\n spaces\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_mixed_whitespace,
- "\n"
- " \n"
- " \n"
- "line1\n"
- "\tindented line\n"
- " spaces\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Control characters and special Unicode
- common_chat_msg expected_control_chars;
- expected_control_chars.role = "assistant";
- expected_control_chars.tool_calls = {
- { "control_function", "{\"text\":\"Line1\\nLine2\\tTabbed\\rCarriage return\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_control_chars,
- "\n"
- " \n"
- " \n"
- "Line1\nLine2\tTabbed\rCarriage return\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Emoji and extended Unicode characters
- common_chat_msg expected_emoji;
- expected_emoji.role = "assistant";
- expected_emoji.tool_calls = {
- { "emoji_function", "{\"message\":\"Hello! π π π Testing emojis: ππππ and symbols: ββββ\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_emoji,
- "\n"
- " \n"
- " \n"
- " Hello! π π π Testing emojis: ππππ and symbols: ββββ\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Mathematical expressions and formulas
- common_chat_msg expected_math;
- expected_math.role = "assistant";
- expected_math.tool_calls = {
- { "math_function", "{\"formula\":\"E = mcΒ² and β«f(x)dx = F(x) + C\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_math,
- "\n"
- " \n"
- " \n"
- " E = mcΒ² and β«f(x)dx = F(x) + C\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // SQL injection-like content (should be safely escaped)
- common_chat_msg expected_sql;
- expected_sql.role = "assistant";
- expected_sql.tool_calls = {
- { "sql_function", "{\"query\":\"SELECT * FROM users WHERE id = 1; DROP TABLE users; --\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_sql,
- "\n"
- " \n"
- " \n"
- " SELECT * FROM users WHERE id = 1; DROP TABLE users; --\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // HTML/XML injection content
- common_chat_msg expected_html;
- expected_html.role = "assistant";
- expected_html.tool_calls = {
- { "html_function", "{\"content\":\"
\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_html,
- "\n"
- " \n"
- " \n"
- "
\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Binary-like content (base64)
- common_chat_msg expected_binary;
- expected_binary.role = "assistant";
- expected_binary.tool_calls = {
- { "binary_function", "{\"data\":\"SGVsbG8gV29ybGQhIFRoaXMgaXMgYmFzZTY0IGVuY29kZWQgdGV4dC4=\"}", "" }
- };
-
- test_parser_with_streaming(
- expected_binary,
- "\n"
- " \n"
- " \n"
- " SGVsbG8gV29ybGQhIFRoaXMgaXMgYmFzZTY0IGVuY29kZWQgdGV4dC4=\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
-
- // Very large numbers (should be parsed as scientific notation)
- common_chat_msg expected_large_numbers;
- expected_large_numbers.role = "assistant";
- expected_large_numbers.tool_calls = {
- { "number_function", "{\"big_int\":1e+60}", "" } // Large number becomes scientific notation
- };
-
- test_parser_with_streaming(
- expected_large_numbers,
- "\n"
- " \n"
- " \n"
- " 999999999999999999999999999999999999999999999999999999999999\n"
- " \n"
- " \n"
- "",
- [&](const std::string &msg) { return test_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_QWEN3_CODER_XML}); });
- }
-
- {
- // Qwen3-Coder template
- auto tmpls = read_templates("models/templates/Qwen3-Coder.jinja");
- common_chat_templates_inputs inputs;
- inputs.messages = { message_user };
-
- common_chat_tool qwen_union_tool {
- /* .name = */ "qwen_union",
- /* .description = */ "Test tool for union/anyOf handling",
- /* .parameters = */ R"({
- "type": "object",
- "properties": {
- "priority": { "type": ["number", "null"] },
- "maybe_text": { "anyOf": [ { "type": "string" } ] },
- "config": { "anyOf": [ { "type": "object" }, { "type": "null" } ] }
- },
- "required": []
- })",
- };
- inputs.tools = { qwen_union_tool };
-
- auto params = common_chat_templates_apply(tmpls.get(), inputs);
- assert_equals(COMMON_CHAT_FORMAT_QWEN3_CODER_XML, params.format);
- assert_equals(false, params.grammar.empty());
-
- // Grammar should compile successfully
- auto grammar = build_grammar(params.grammar);
- GGML_ASSERT(grammar && "Failed to build Qwen3-Coder grammar with union types");
- }
-
{
// Step-3.5-Flash template: uses same XML output format as Qwen3-Coder and Nemotron v3,
// but with support. Routes to the Nemotron v3 PEG parser for streaming and
@@ -3665,6 +3143,135 @@ static void test_template_output_peg_parsers() {
});
}
+ {
+ // Qwen3-Coder
+ auto tmpls = read_templates("models/templates/Qwen3-Coder.jinja");
+
+ // Test basic message
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input = "Hello, world!\nWhat's up?";
+ t.expect = message_assist;
+ });
+
+ // Test tool call
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "\n"
+ "\n"
+ "\n"
+ "1\n"
+ "\n"
+ "\n"
+ "";
+ t.params.tools = {special_function_tool};
+ t.expect = message_assist_call;
+ });
+
+ // Test parallel tool calls
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "\n"
+ "\n"
+ "\n"
+ "1\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "\n"
+ "1\n"
+ "\n"
+ "\n"
+ "2\n"
+ "\n"
+ "\n"
+ "";
+ t.params.parallel_tool_calls = true;
+ t.params.tools = {special_function_tool, special_function_tool_with_optional_param};
+
+ t.expect.tool_calls = {{
+ /* .name = */ "special_function",
+ /* .arguments = */ R"({"arg1": 1})",
+ /* .id = */ {},
+ }, {
+ /* .name = */ "special_function_with_opt",
+ /* .arguments = */ R"({"arg1": 1, "arg2": 2})",
+ /* .id = */ {},
+ }};
+ });
+
+ // Test tool call with string parameter
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "\n"
+ "\n"
+ "\n"
+ "def hello():\n"
+ " print(\"Hello, world!\")\n"
+ "\n"
+ "hello()\n"
+ "\n"
+ "\n"
+ "";
+ t.params.tools = {python_tool};
+
+ t.expect.tool_calls = {{
+ /* .name = */ "python",
+ /* .arguments = */ "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}",
+ /* .id = */ {},
+ }};
+ });
+
+ // Test tool call with JSON parameter
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "\n"
+ "\n"
+ "\n"
+ "[{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]\n"
+ "\n"
+ "\n"
+ "";
+ t.params.tools = {todo_list_tool};
+
+ t.expect.tool_calls = {{
+ /* .name = */ "todo_list",
+ /* .arguments = */ "{\"todos\": [{\"item\": \"Check stuff\", \"selected\": false}, {\"item\": \"Prepare stuff\", \"selected\": true}]}",
+ /* .id = */ {},
+ }};
+ });
+
+ // Test tool call with string parameter and no closing tag
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "\n"
+ "\n"
+ "\n"
+ "def hello():\n"
+ " print(\"Hello, world!\")\n"
+ "\n"
+ "hello()\n"
+ "\n"
+ "";
+ t.params.tools = {python_tool};
+
+ t.expect.tool_calls = {{
+ /* .name = */ "python",
+ /* .arguments = */ "{\"code\": \"def hello():\\n print(\\\"Hello, world!\\\")\\n\\nhello()\"}",
+ /* .id = */ {},
+ }};
+ });
+
+ // Test response format
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input = R"({"amount": 123.45, "date": "2025-12-03"})";
+ t.params.json_schema = invoice_schema;
+
+ t.expect.content = R"({"amount": 123.45, "date": "2025-12-03"})";
+ });
+ }
+
{
// NVIDIA Nemotron-3 Nano
auto tmpls = read_templates("models/templates/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16.jinja");