--- /dev/null
+{% macro render_content(content) %}{% if content is none %}{{- '' }}{% elif content is string %}{{- content }}{% elif content is mapping %}{{- content['value'] if 'value' in content else content['text'] }}{% elif content is iterable %}{% for item in content %}{% if item.type == 'text' %}{{- item['value'] if 'value' in item else item['text'] }}{% elif item.type == 'image' %}<im_patch>{% endif %}{% endfor %}{% endif %}{% endmacro %}
+{{bos_token}}{%- if tools %}
+ {{- '<|im_start|>system\n' }}
+ {%- if messages[0].role == 'system' %}
+ {{- render_content(messages[0].content) + '\n\n' }}
+ {%- endif %}
+ {{- "# Tools\n\nYou have access to the following functions in JSONSchema format:\n\n<tools>" }}
+ {%- for tool in tools %}
+ {{- "\n" }}
+ {{- tool | tojson(ensure_ascii=False) }}
+ {%- 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=...>\n...\n</function> block must be nested within <tool_call>\n...\n</tool_call> XML tags\n- Required parameters MUST be specified\n</IMPORTANT><|im_end|>\n" }}
+{%- else %}
+ {%- if messages[0].role == 'system' %}
+ {{- '<|im_start|>system\n' + render_content(messages[0].content) + '<|im_end|>\n' }}
+ {%- endif %}
+{%- endif %}
+{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
+{%- for message in messages[::-1] %}
+ {%- set index = (messages|length - 1) - loop.index0 %}
+ {%- if ns.multi_step_tool and message.role == "user" and render_content(message.content) is string and not(render_content(message.content).startswith('<tool_response>') and render_content(message.content).endswith('</tool_response>')) %}
+ {%- set ns.multi_step_tool = false %}
+ {%- set ns.last_query_index = index %}
+ {%- endif %}
+{%- endfor %}
+{%- for message in messages %}
+ {%- set content = render_content(message.content) %}
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
+ {%- set role_name = 'observation' if (message.role == "system" and not loop.first and message.name == 'observation') else message.role %}
+ {{- '<|im_start|>' + role_name + '\n' + content + '<|im_end|>' + '\n' }}
+ {%- elif message.role == "assistant" %}
+ {%- if message.reasoning_content is string %}
+ {%- set reasoning_content = render_content(message.reasoning_content) %}
+ {%- else %}
+ {%- if '</think>' in content %}
+ {%- set reasoning_content = content.split('</think>')[0].rstrip('\n').split('<think>')[-1].lstrip('\n') %}
+ {%- set content = content.split('</think>')[-1].lstrip('\n') %}
+ {%- else %}
+ {%- set reasoning_content = '' %}
+ {%- endif %}
+ {%- endif %}
+ {%- if loop.index0 > ns.last_query_index %}
+ {{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content + '\n</think>\n' + content }}
+ {%- else %}
+ {{- '<|im_start|>' + message.role + '\n' + content }}
+ {%- endif %}
+ {%- if message.tool_calls %}
+ {%- 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 %}
+ {%- set arguments = tool_call.arguments %}
+ {%- for args_name, args_value in arguments|items %}
+ {{- '<parameter=' + args_name + '>\n' }}
+ {%- set args_value = args_value | tojson(ensure_ascii=False) | 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>' }}
+ {%- endfor %}
+ {%- endif %}
+ {{- '<|im_end|>\n' }}
+ {%- elif message.role == "tool" %}
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
+ {{- '<|im_start|>tool_response\n' }}
+ {%- endif %}
+ {{- '<tool_response>' }}
+ {{- content }}
+ {{- '</tool_response>' }}
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
+ {{- '<|im_end|>\n' }}
+ {%- endif %}
+ {%- endif %}
+{%- endfor %}
+{%- if add_generation_prompt %}
+ {{- '<|im_start|>assistant\n<think>\n' }}
+{%- endif %}
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 <think> support. Routes to the Nemotron v3 PEG parser for streaming and
+ // schema-aware parameter parsing.
+ auto tmpls = read_templates("models/templates/stepfun-ai-Step-3.5-Flash.jinja");
+ assert_equals(COMMON_CHAT_FORMAT_PEG_CONSTRUCTED, common_chat_templates_apply(tmpls.get(), inputs_tools).format);
+
+ // Grammar and PEG parser should be generated with thinking_forced_open
+ {
+ common_chat_templates_inputs inputs;
+ inputs.messages = { message_user };
+ inputs.tools = { special_function_tool };
+ auto params = common_chat_templates_apply(tmpls.get(), inputs);
+ assert_equals(COMMON_CHAT_FORMAT_PEG_CONSTRUCTED, params.format);
+ assert_equals(true, params.thinking_forced_open);
+ assert_equals(false, params.grammar.empty());
+ assert_equals(false, params.parser.empty());
+ auto grammar = build_grammar(params.grammar);
+ GGML_ASSERT(grammar && "Failed to build Step-3.5-Flash grammar");
+ }
+ }
}
static void test_template_output_peg_parsers() {
});
}
+ {
+ // Step-3.5-Flash (uses Nemotron v3 PEG parser with thinking_forced_open)
+ // Unlike Nemotron, Step-3.5-Flash always emits <think> regardless of enable_thinking,
+ // so all inputs must include a </think> delimiter.
+ auto tmpls = read_templates("models/templates/stepfun-ai-Step-3.5-Flash.jinja");
+
+ // Test basic message with reasoning
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input = "I'm\nthinking\n</think>\nHello, world!\nWhat's up?";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+
+ t.expect = message_assist_thoughts;
+ });
+
+ // Test basic message without thinking content
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input = "</think>\nHello, world!\nWhat's up?";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+
+ t.expect = message_assist;
+ });
+
+ // Test tool call without thinking content
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "</think>\n"
+ "<tool_call>\n"
+ "<function=special_function>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ t.params.tools = {special_function_tool};
+
+ t.expect = message_assist_call;
+ });
+
+ // Test tool call with thinking
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "I'm\nthinking\n</think>\n"
+ "<tool_call>\n"
+ "<function=special_function>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ t.params.tools = {special_function_tool};
+
+ t.expect = message_assist_call_thoughts;
+ });
+
+ // Test parallel tool calls with thinking
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "I'm\nthinking\n</think>\n"
+ "<tool_call>\n"
+ "<function=special_function>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>\n"
+ "<tool_call>\n"
+ "<function=special_function_with_opt>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "<parameter=arg2>\n"
+ "2\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ t.params.parallel_tool_calls = true;
+ t.params.tools = {special_function_tool, special_function_tool_with_optional_param};
+
+ t.expect.reasoning_content = "I'm\nthinking";
+ 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 parallel tool calls without thinking content
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "</think>\n"
+ "<tool_call>\n"
+ "<function=special_function>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>\n"
+ "<tool_call>\n"
+ "<function=special_function_with_opt>\n"
+ "<parameter=arg1>\n"
+ "1\n"
+ "</parameter>\n"
+ "<parameter=arg2>\n"
+ "2\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ 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 code string parameter
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "</think>\n"
+ "<tool_call>\n"
+ "<function=python>\n"
+ "<parameter=code>\n"
+ "def hello():\n"
+ " print(\"Hello, world!\")\n"
+ "\n"
+ "hello()\n"
+ "</parameter>\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ 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 string parameter and no closing </parameter> tag
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "</think>\n"
+ "<tool_call>\n"
+ "<function=python>\n"
+ "<parameter=code>\n"
+ "def hello():\n"
+ " print(\"Hello, world!\")\n"
+ "\n"
+ "hello()\n"
+ "</function>\n"
+ "</tool_call>";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ 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 (JSON schema with thinking)
+ test_peg_parser(tmpls.get(), [&](auto & t) {
+ t.input =
+ "I need to output the invoice details in JSON\n"
+ "</think>\n"
+ R"({"amount": 123.45, "date": "2025-12-03"})";
+ t.params.reasoning_format = COMMON_REASONING_FORMAT_AUTO;
+ t.params.json_schema = invoice_schema;
+
+ t.expect.reasoning_content = "I need to output the invoice details in JSON";
+ t.expect.content = R"({"amount": 123.45, "date": "2025-12-03"})";
+ });
+ }
+
{
// Solar-Open-100B
auto tmpls = read_templates("models/templates/upstage-Solar-Open-100B.jinja");