From 685c01d8ad4a458c988730f747c92e0b6b1a2854 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Mon, 16 Mar 2026 14:17:52 +0800
Subject: [PATCH 1/7] Implement MiroThinker tool call parser
---
common/chat.cpp | 118 ++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 118 insertions(+)
diff --git a/common/chat.cpp b/common/chat.cpp
index cfd5df30a7..79c4c278e2 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -1278,6 +1278,116 @@ static common_chat_params common_chat_params_init_kimi_k2(const common_chat_temp
return data;
}
+// MiroThinker - uses MCP style toolcalling
+static common_chat_params common_chat_params_init_mirothinker(const common_chat_template & tmpl,
+ const autoparser::templates_params & inputs) {
+ common_chat_params data;
+
+ data.prompt = common_chat_template_direct_apply(tmpl, inputs);
+ data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
+ data.supports_thinking = true;
+ data.thinking_start_tag = "";
+ data.thinking_end_tag = "";
+ data.preserved_tokens = {
+ "",
+ "",
+ };
+
+ auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
+ auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
+ auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
+
+ auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
+ // MiroThinker Thinking format:
+ // - Reasoning: {reasoning}
+ // - Content: text after reasoning
+ // - Tool calls section:
+ //
+ // {server_name}
+ // {tool_name}
+ //
+ // {json_args}
+ //
+ // ...
+ //
+
+ auto reasoning = extract_reasoning ? p.optional("" + p.reasoning(p.until("")) + "") : p.eps();
+
+ // Tool call markers
+ const std::string SECTION_BEGIN = "";
+ const std::string SECTION_END = "";
+ const std::string CALL_BEGIN = "";
+ const std::string ARGS_BEGIN = "";
+ const std::string CALL_END = "";
+
+ auto end = p.end();
+
+ // Content only parser (no tools)
+ if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
+ return reasoning + p.content(p.rest()) + end;
+ }
+
+ // Build tool call parsers for each available function
+ // Function name format is: {tool_name}
+ // We need to match: {what_ever}{spaces}{tool_name}
+ auto tool_choice = p.choice();
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ std::string name = function.at("name");
+ const auto & schema = function.at("parameters");
+
+ // Match: {what_ever}{spaces}{tool_name}
+ auto tool_parser = p.tool(
+ p.tool_open(
+ p.until("") +
+ p.literal("") +
+ p.space() +
+ p.literal("") +
+ p.tool_name(p.literal(name)) +
+ p.literal(ARGS_BEGIN)
+ ) + p.space() +
+ p.tool_args(p.schema(p.json(), "tool-" + name + "-schema", schema)) +
+ p.space() + p.tool_close(p.literal(CALL_END))
+ );
+
+ tool_choice |= p.rule("tool-" + name, tool_parser);
+ });
+
+ // Tool calls section: tool_calls
+ auto min_calls = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_REQUIRED ? 1 : 0;
+ auto max_calls = inputs.parallel_tool_calls ? -1 : 1;
+ auto tool_calls = p.trigger_rule("tool-calls",
+ p.literal(SECTION_BEGIN) + p.space() +
+ p.rule("tool-call", p.repeat(CALL_BEGIN + tool_choice, min_calls, max_calls) +
+ p.space() + p.literal(SECTION_END))
+ );
+
+ auto content_before_tools = p.content(p.until(SECTION_BEGIN));
+
+ return reasoning + content_before_tools + tool_calls + end;
+ });
+
+ data.parser = parser.save();
+
+ if (include_grammar) {
+ data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
+ data.grammar = build_grammar([&](const common_grammar_builder & builder) {
+ foreach_function(inputs.tools, [&](const json & tool) {
+ const auto & function = tool.at("function");
+ auto schema = function.at("parameters");
+ builder.resolve_refs(schema);
+ });
+ parser.build_grammar(builder, data.grammar_lazy);
+ });
+
+ data.grammar_triggers = {
+ { COMMON_GRAMMAR_TRIGGER_TYPE_WORD, "" }
+ };
+ }
+
+ return data;
+}
+
// LFM2 format:
// - Reasoning: {reasoning} (optional, only if enable_thinking is true)
// - Content: text after reasoning (optional)
@@ -1588,6 +1698,14 @@ static common_chat_params common_chat_templates_apply_jinja(const struct common_
return common_chat_params_init_kimi_k2(tmpl, params);
}
+ // MiroThinker - uses MCP style toolcalling ...
+ // Detection: template has "" and ""
+ if (src.find("") != std::string::npos &&
+ src.find("") != std::string::npos) {
+ LOG_DBG("Using specialized template: MiroThinker\n");
+ return common_chat_params_init_mirothinker(tmpl, params);
+ }
+
// LFM2 - uses <|tool_list_start|>/<|tool_list_end|> markers and <|tool_call_start|>[name(args)]<|tool_call_end|> format
// Detection: template has "<|tool_list_start|>" and "<|tool_list_end|>" markers
if (src.find("<|tool_list_start|>") != std::string::npos &&
From eaf6e7912ce6f6ee9901777831ec2aab7f1cf87f Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Mon, 16 Mar 2026 14:23:57 +0800
Subject: [PATCH 2/7] Add chat template for MiroThinker
---
models/templates/MiroThinker-v1.jinja | 285 ++++++++++++++++++++++++++
1 file changed, 285 insertions(+)
create mode 100644 models/templates/MiroThinker-v1.jinja
diff --git a/models/templates/MiroThinker-v1.jinja b/models/templates/MiroThinker-v1.jinja
new file mode 100644
index 0000000000..34c6c02573
--- /dev/null
+++ b/models/templates/MiroThinker-v1.jinja
@@ -0,0 +1,285 @@
+{#- ========== MiroThinker Tool Parsing Macro ========== #}
+
+{%- macro function_name() %}
+ {%- if tool.function is defined %}
+ {{- tool.function.name }}
+ {%- elif tool.name is defined and tool.description is defined %}
+ {{- tool.name }}
+ {%- endif %}
+{%- endmacro %}
+{%- macro function_description() %}
+ {%- if tool.function is defined %}
+ {{- tool.function.description }}
+ {%- elif tool.name is defined and tool.description is defined %}
+ {{- tool.description }}
+ {%- endif %}
+{%- endmacro %}
+{%- macro function_parameters() %}
+ {%- if tool.function is defined %}
+ {{- tool.function.parameters }}
+ {%- elif tool.name is defined and tool.description is defined %}
+ {{- tool.parameters }}
+ {%- endif %}
+{%- endmacro %}
+
+{%- macro render_tool(server_name) %}
+ {%- if tool.mt_visited is not defined %}
+ {%- if server_name != ns.last_server %}
+ {{- "\n## Server name: " + server_name + "\n" }}
+ {%- set ns.last_server = server_name %}
+ {%- endif %}
+ {{- "### Tool name: " + function_name() + "\n" }}
+ {{- "Description: " + function_description() + "\n" }}
+ {{- "Input JSON schema: " + (function_parameters() | tojson(ensure_ascii=False)) + "\n" }}
+ {{- "\n" }}
+ {%- endif %}
+{%- endmacro %}
+
+{%- macro render_tool_server() %}
+ {%- if (function_name().split('_sandbox') | length > 1) or function_name().startswith('run_') or (function_name().split('python') | length > 1) %}
+ {{- "tool-python" }}
+ {%- elif function_name().split('_search') | length > 1 %}
+ {{- "search_and_scrape_webpage" }}
+ {%- elif function_name() == 'scrape_and_extract_info' %}
+ {{- "jina_scrape_llm_summary" }}
+ {%- else %}
+ {{- "generic-extras" }}
+ {%- endif %}
+{%- endmacro %}
+
+{%- macro render_tool_call() %}
+ {%- if message.tool_calls %}
+ {{- "\n" }}
+ {%- for tool_call in message.tool_calls %}
+ {%- set function = tool_call.function %}
+ {{- "\n" }}
+ {{- render_tool_server() }}
+ {{- "\n" }}
+ {{- function.name }}
+ {{- "\n\n" }}
+ {{- function.arguments | tojson(ensure_ascii=False) }}
+ {{- "\n" }}
+ {%- endfor %}
+ {{- "\n" }}
+ {%- endif %}
+{%- endmacro %}
+
+{#- ========== MiroThinker System Message ========== #}
+
+{%- set system_message = namespace(role='system', content='') %}
+{%- if date_string is string %}
+ {%- set date_string = 'Today is: ' + date_string %}
+{%- else %}
+ {%- set date_string = '' %}
+{%- endif %}
+{%- if tools %}
+ {%- set system_message.content = "In this environment you have access to a set of tools you can use to answer the user's question. \n\nYou only have access to the tools provided below. You can only use one tool per message, and will receive the result of that tool in the user's next response. You use tools step-by-step to accomplish a given task, with each tool-use informed by the result of the previous tool-use. " + date_string + "\n\n# Tool-Use Formatting Instructions \n\nTool-use is formatted using XML-style tags. The tool-use is enclosed in and each parameter is similarly enclosed within its own set of tags.\n\nThe Model Context Protocol (MCP) connects to servers that provide additional tools and resources to extend your capabilities. You can use the server's tools via the `use_mcp_tool`.\n\nDescription: \nRequest to use a tool provided by a MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.\n\nParameters:\n- server_name: (required) The name of the MCP server providing the tool\n- tool_name: (required) The name of the tool to execute\n- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema, quotes within string must be properly escaped, ensure it's valid JSON\n\nUsage:\n\nserver name here\ntool name here\n\n{\n\"param1\": \"value1\",\n\"param2\": \"value2 \\\"escaped string\\\"\"\n}\n\n\n\nImportant Notes:\n- Tool-use must be placed **at the end** of your response, **top-level**, and not nested within other tags.\n- Always adhere to this format for the tool use to ensure proper parsing and execution.\n\nString and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.\nHere are the functions available in JSONSchema format:\n\n" %}
+ {%- set ns = namespace(formatted_tools='', last_server=None) %}
+ {%- for tool_server in ['tool-python', 'search_and_scrape_webpage', 'jina_scrape_llm_summary', 'generic-extras'] %}
+ {%- for tool in tools %}
+ {%- set this_server = render_tool_server() %}
+ {%- if this_server == tool_server %}
+ {%- set ns.formatted_tools = ns.formatted_tools + render_tool(tool_server) %}
+ {%- set tool.mt_visited = 1 %}
+ {%- endif %}
+ {%- endfor %}
+ {%- endfor %}
+ {%- set system_message.content = system_message.content + ns.formatted_tools + "\n# General Objective\n\nYou accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.\n\n" %}
+ {%- set tools = None %}
+ {%- if messages[0].role == 'system' %}
+ {%- if messages[0].content.split('') | length > 1 %}
+ {%- set system_message = messages[0] %}
+ {%- set messages = messages[1:] %}
+ {%- endif %}
+ {%- if messages[0].content.split('') | length > 1 %}
+ {%- set messages = messages[1:] %}
+ {%- endif %}
+ {%- endif %}
+{%- else %}
+ {%- set system_message.content = "In this environment you have access to a set of tools you can use to answer the user's question. " + date_string + "\n\nImportant Notes:\n- Tool-use must be placed **at the end** of your response, **top-level**, and not nested within other tags.\n- Always adhere to this format for the tool use to ensure proper parsing and execution.\n\nString and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.\n\n# General Objective\n\nYou accomplish a given task iteratively, breaking it down into clear steps and working through them methodically.\n\n" %}
+ {%- if messages[0].role == 'system' %}
+ {%- set system_message = messages[0] %}
+ {%- set messages = messages[1:] %}
+ {%- endif %}
+{%- endif %}
+{%- set messages = [system_message] + messages %}
+
+{#- ========== MiroThinker Context Management ========== #}
+
+{%- set tool_count = namespace(keep=5, total=0) %}
+{%- if keep_tool_result is defined %}
+ {%- set tool_count.keep = keep_tool_result %}
+{%- endif %}
+{%- if tool_count.keep != -1 %}
+ {%- for message in messages %}
+ {%- if message.role == 'assistant' %}
+ {%- if message.tool_calls %}
+ {%- set message.have_tools = message.tool_calls | length %}
+ {%- set tool_count.total = tool_count.total + message.have_tools %}
+ {%- else %}
+ {%- set msg = message.content %}
+ {%- if msg.endswith('\n') %}
+ {%- set msg = msg[:-16] %}
+ {%- set msg = msg.split('\n') %}
+ {%- if msg | length > 1 %}
+ {%- set message.have_tools = (msg[-1].split('\n') | length) - 1 %}
+ {%- set tool_count.total = tool_count.total + message.have_tools %}
+ {%- endif %}
+ {%- endif %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+ {%- if tool_count.total > tool_count.keep %}
+ {%- set tool_count.total = tool_count.total - tool_count.keep %}
+ {%- else %}
+ {%- set tool_count.total = 0 %}
+ {%- endif %}
+ {%- set should_consume = namespace(count=0) %}
+ {%- for message in messages %}
+ {%- if message.role == 'assistant' %}
+ {%- if message.have_tools is defined %}
+ {%- if message.have_tools < tool_count.total %}
+ {%- set should_consume.count = should_consume.count + message.have_tools %}
+ {%- set tool_count.total = tool_count.total - message.have_tools %}
+ {%- else %}
+ {%- set should_consume.count = should_consume.count + tool_count.total %}
+ {%- set tool_count.total = 0 %}
+ {%- endif %}
+ {%- endif %}
+ {%- elif message.role == 'user' or message.role == 'tool' %}
+ {%- if should_consume.count > 0 %}
+ {%- set message.content = 'Tool result is omitted to save tokens.' %}
+ {%- set should_consume.count = should_consume.count - 1 %}
+ {%- endif %}
+ {%- endif %}
+ {%- endfor %}
+{%- endif %}
+
+{#- ========== MiroThinker Tool Response ========== #}
+
+{%- for message in messages %}
+ {%- if message.role == 'assistant' %}
+ {%- if message.tool_calls %}
+ {%- set message.content = message.content + render_tool_call() %}
+ {%- set message.tool_calls = [] %}
+ {%- endif %}
+ {%- elif message.role == 'user' %}
+ {%- if message.content.startswith('') %}
+ {%- set message.content = message.content[15:] | trim %}
+ {%- if message.content.endswith('') %}
+ {%- set message.content = message.content[:-16] | trim %}
+ {%- endif %}
+ {%- endif %}
+ {%- elif message.role == 'tool' %}
+ {%- set message.role = 'user' %}
+ {%- endif %}
+{%- endfor %}
+
+{#- ========== MiroThinker Tool Usage Patched ========== #}
+
+{%- if tools %}
+ {{- '<|im_start|>system\n' }}
+ {%- if messages[0].role == 'system' %}
+ {{- messages[0].content + '\n\n' }}
+ {%- endif %}
+ {{- "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within XML tags:\n" }}
+ {%- for tool in tools %}
+ {{- "\n" }}
+ {{- tool | tojson }}
+ {%- endfor %}
+ {{- "\n\n\nFor each function call, return a json object with function name and arguments within XML tags:\n\n{\"name\": , \"arguments\": }\n<|im_end|>\n" }}
+{%- else %}
+ {%- if messages[0].role == 'system' %}
+ {{- '<|im_start|>system\n' + messages[0].content + '<|im_end|>\n' }}
+ {%- endif %}
+{%- endif %}
+{%- set ns = namespace(multi_step_tool=true, last_query_index=messages|length - 1) %}
+{%- for forward_message in messages %}
+ {%- set index = (messages|length - 1) - loop.index0 %}
+ {%- set message = messages[index] %}
+ {%- set current_content = message.content if message.content is not none else '' %}
+ {%- set tool_start = '' %}
+ {%- set tool_start_length = tool_start|length %}
+ {%- set start_of_message = current_content[:tool_start_length] %}
+ {%- set tool_end = '' %}
+ {%- set tool_end_length = tool_end|length %}
+ {%- set start_pos = (current_content|length) - tool_end_length %}
+ {%- if start_pos < 0 %}
+ {%- set start_pos = 0 %}
+ {%- endif %}
+ {%- set end_of_message = current_content[start_pos:] %}
+ {%- if ns.multi_step_tool and message.role == "user" and not(start_of_message == tool_start and end_of_message == tool_end) %}
+ {%- set ns.multi_step_tool = false %}
+ {%- set ns.last_query_index = index %}
+ {%- endif %}
+{%- endfor %}
+{%- for message in messages %}
+ {%- if (message.role == "user") or (message.role == "system" and not loop.first) %}
+ {{- '<|im_start|>' + message.role + '\n' + message.content + '<|im_end|>' + '\n' }}
+ {%- elif message.role == "assistant" %}
+ {%- set content = message.content %}
+ {%- set reasoning_content = '' %}
+ {%- if message.reasoning_content is defined and message.reasoning_content is not none %}
+ {%- set reasoning_content = message.reasoning_content %}
+ {%- else %}
+ {%- if '' in message.content %}
+ {%- set content = (message.content.split('')|last).lstrip('\n') %}
+ {%- set reasoning_content = (message.content.split('')|first).rstrip('\n') %}
+ {%- set reasoning_content = (reasoning_content.split('')|last).lstrip('\n') %}
+ {%- endif %}
+ {%- endif %}
+ {%- if loop.index0 > ns.last_query_index %}
+ {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }}
+ {%- else %}
+ {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content.strip('\n') + '\n\n\n' + content.lstrip('\n') }}
+ {%- endif %}
+ {%- if message.tool_calls %}
+ {%- for tool_call in message.tool_calls %}
+ {%- if (loop.first and content) or (not loop.first) %}
+ {{- '\n' }}
+ {%- endif %}
+ {%- if tool_call.function %}
+ {%- set tool_call = tool_call.function %}
+ {%- endif %}
+ {{- '\n{"name": "' }}
+ {{- tool_call.name }}
+ {{- '", "arguments": ' }}
+ {%- if tool_call.arguments is string %}
+ {{- tool_call.arguments }}
+ {%- else %}
+ {{- tool_call.arguments | tojson }}
+ {%- endif %}
+ {{- '}\n' }}
+ {%- endfor %}
+ {%- endif %}
+ {{- '<|im_end|>\n' }}
+ {%- elif message.role == "tool" %}
+ {%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
+ {{- '<|im_start|>user' }}
+ {%- endif %}
+ {{- '\n\n' }}
+ {{- message.content }}
+ {{- '\n' }}
+ {%- if loop.last or (messages[loop.index0 + 1].role != "tool") %}
+ {{- '<|im_end|>\n' }}
+ {%- endif %}
+ {%- endif %}
+{%- endfor %}
+{%- if add_generation_prompt %}
+ {{- '<|im_start|>assistant\n' }}
+ {%- if enable_thinking is defined and enable_thinking is false %}
+ {{- '\n\n\n\n' }}
+ {%- endif %}
+{%- endif %}
+
+{#- ========== Workaround for llama.cpp crashing ========== #}
+{%- for message in messages %}
+ {%- if message.role == "assistant" %}
+ {%- if message.tool_calls | length == 0 %}
+ {%- set fake_function = namespace(name='fake_name', arguments='{}') %}
+ {%- set fake_function = namespace(function=fake_function) %}
+ {%- set message.tool_calls = [fake_function, fake_function] %}
+ {%- endif %}
+ {%- endif %}
+{%- endfor %}
+{#- ========== Workaround for llama.cpp crashing ========== #}
From d6ec9772769846af7d6217c8c6745776a4e34a95 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Tue, 17 Mar 2026 09:11:27 +0800
Subject: [PATCH 3/7] Add CALL_BEGIN2 for server name parsing
---
common/chat.cpp | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/common/chat.cpp b/common/chat.cpp
index 79c4c278e2..6feb59a9e6 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -1317,6 +1317,7 @@ static common_chat_params common_chat_params_init_mirothinker(const common_chat_
const std::string SECTION_BEGIN = "";
const std::string SECTION_END = "";
const std::string CALL_BEGIN = "";
+ const std::string CALL_BEGIN2 = "";
const std::string ARGS_BEGIN = "";
const std::string CALL_END = "";
@@ -1339,8 +1340,8 @@ static common_chat_params common_chat_params_init_mirothinker(const common_chat_
// Match: {what_ever}{spaces}{tool_name}
auto tool_parser = p.tool(
p.tool_open(
- p.until("") +
- p.literal("") +
+ p.until(CALL_BEGIN2) +
+ p.literal(CALL_BEGIN2) +
p.space() +
p.literal("") +
p.tool_name(p.literal(name)) +
From 02e67012e5507f213cb4502c733f34eca66f4cc4 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Tue, 17 Mar 2026 09:20:27 +0800
Subject: [PATCH 4/7] Refactor macros to accept tool parameter
---
models/templates/MiroThinker-v1.jinja | 37 ++++++++++++++-------------
1 file changed, 19 insertions(+), 18 deletions(-)
diff --git a/models/templates/MiroThinker-v1.jinja b/models/templates/MiroThinker-v1.jinja
index 34c6c02573..215d96fc78 100644
--- a/models/templates/MiroThinker-v1.jinja
+++ b/models/templates/MiroThinker-v1.jinja
@@ -1,20 +1,20 @@
{#- ========== MiroThinker Tool Parsing Macro ========== #}
-{%- macro function_name() %}
+{%- macro function_name(tool) %}
{%- if tool.function is defined %}
{{- tool.function.name }}
{%- elif tool.name is defined and tool.description is defined %}
{{- tool.name }}
{%- endif %}
{%- endmacro %}
-{%- macro function_description() %}
+{%- macro function_description(tool) %}
{%- if tool.function is defined %}
{{- tool.function.description }}
{%- elif tool.name is defined and tool.description is defined %}
{{- tool.description }}
{%- endif %}
{%- endmacro %}
-{%- macro function_parameters() %}
+{%- macro function_parameters(tool) %}
{%- if tool.function is defined %}
{{- tool.function.parameters }}
{%- elif tool.name is defined and tool.description is defined %}
@@ -22,38 +22,37 @@
{%- endif %}
{%- endmacro %}
-{%- macro render_tool(server_name) %}
+{%- macro render_tool(server_name, tool, last_server) %}
{%- if tool.mt_visited is not defined %}
- {%- if server_name != ns.last_server %}
+ {%- if server_name != last_server %}
{{- "\n## Server name: " + server_name + "\n" }}
- {%- set ns.last_server = server_name %}
{%- endif %}
- {{- "### Tool name: " + function_name() + "\n" }}
- {{- "Description: " + function_description() + "\n" }}
- {{- "Input JSON schema: " + (function_parameters() | tojson(ensure_ascii=False)) + "\n" }}
+ {{- "### Tool name: " + function_name(tool) + "\n" }}
+ {{- "Description: " + function_description(tool) + "\n" }}
+ {{- "Input JSON schema: " + (function_parameters(tool) | tojson(ensure_ascii=False)) + "\n" }}
{{- "\n" }}
{%- endif %}
{%- endmacro %}
-{%- macro render_tool_server() %}
- {%- if (function_name().split('_sandbox') | length > 1) or function_name().startswith('run_') or (function_name().split('python') | length > 1) %}
+{%- macro render_tool_server(tool) %}
+ {%- if (function_name(tool).split('_sandbox') | length > 1) or function_name(tool).startswith('run_') or (function_name(tool).split('python') | length > 1) %}
{{- "tool-python" }}
- {%- elif function_name().split('_search') | length > 1 %}
+ {%- elif function_name(tool).split('_search') | length > 1 %}
{{- "search_and_scrape_webpage" }}
- {%- elif function_name() == 'scrape_and_extract_info' %}
+ {%- elif function_name(tool) == 'scrape_and_extract_info' %}
{{- "jina_scrape_llm_summary" }}
{%- else %}
{{- "generic-extras" }}
{%- endif %}
{%- endmacro %}
-{%- macro render_tool_call() %}
+{%- macro render_tool_call(message) %}
{%- if message.tool_calls %}
{{- "\n" }}
{%- for tool_call in message.tool_calls %}
{%- set function = tool_call.function %}
{{- "\n" }}
- {{- render_tool_server() }}
+ {{- render_tool_server(tool_call) }}
{{- "\n" }}
{{- function.name }}
{{- "\n\n" }}
@@ -74,12 +73,13 @@
{%- endif %}
{%- if tools %}
{%- set system_message.content = "In this environment you have access to a set of tools you can use to answer the user's question. \n\nYou only have access to the tools provided below. You can only use one tool per message, and will receive the result of that tool in the user's next response. You use tools step-by-step to accomplish a given task, with each tool-use informed by the result of the previous tool-use. " + date_string + "\n\n# Tool-Use Formatting Instructions \n\nTool-use is formatted using XML-style tags. The tool-use is enclosed in and each parameter is similarly enclosed within its own set of tags.\n\nThe Model Context Protocol (MCP) connects to servers that provide additional tools and resources to extend your capabilities. You can use the server's tools via the `use_mcp_tool`.\n\nDescription: \nRequest to use a tool provided by a MCP server. Each MCP server can provide multiple tools with different capabilities. Tools have defined input schemas that specify required and optional parameters.\n\nParameters:\n- server_name: (required) The name of the MCP server providing the tool\n- tool_name: (required) The name of the tool to execute\n- arguments: (required) A JSON object containing the tool's input parameters, following the tool's input schema, quotes within string must be properly escaped, ensure it's valid JSON\n\nUsage:\n\nserver name here\ntool name here\n\n{\n\"param1\": \"value1\",\n\"param2\": \"value2 \\\"escaped string\\\"\"\n}\n\n\n\nImportant Notes:\n- Tool-use must be placed **at the end** of your response, **top-level**, and not nested within other tags.\n- Always adhere to this format for the tool use to ensure proper parsing and execution.\n\nString and scalar parameters should be specified as is, while lists and objects should use JSON format. Note that spaces for string values are not stripped. The output is not expected to be valid XML and is parsed with regular expressions.\nHere are the functions available in JSONSchema format:\n\n" %}
- {%- set ns = namespace(formatted_tools='', last_server=None) %}
+ {%- set ns = namespace(formatted_tools = '', last_server=None) %}
{%- for tool_server in ['tool-python', 'search_and_scrape_webpage', 'jina_scrape_llm_summary', 'generic-extras'] %}
{%- for tool in tools %}
- {%- set this_server = render_tool_server() %}
+ {%- set this_server = render_tool_server(tool) %}
{%- if this_server == tool_server %}
- {%- set ns.formatted_tools = ns.formatted_tools + render_tool(tool_server) %}
+ {%- set ns.formatted_tools = ns.formatted_tools + render_tool(tool_server, tool, ns.last_server) %}
+ {%- set ns.last_server = server_name %}
{%- set tool.mt_visited = 1 %}
{%- endif %}
{%- endfor %}
@@ -159,6 +159,7 @@
{%- for message in messages %}
{%- if message.role == 'assistant' %}
+ {%- if message.role == 'user' %}
{%- if message.tool_calls %}
{%- set message.content = message.content + render_tool_call() %}
{%- set message.tool_calls = [] %}
From aa5a30f272168be60a0c85ed9b7932837a33f582 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Tue, 17 Mar 2026 09:21:57 +0800
Subject: [PATCH 5/7] Remove ensure_ascii=False from JSON serialization
---
models/templates/MiroThinker-v1.jinja | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/models/templates/MiroThinker-v1.jinja b/models/templates/MiroThinker-v1.jinja
index 215d96fc78..eb2445cd3b 100644
--- a/models/templates/MiroThinker-v1.jinja
+++ b/models/templates/MiroThinker-v1.jinja
@@ -29,7 +29,7 @@
{%- endif %}
{{- "### Tool name: " + function_name(tool) + "\n" }}
{{- "Description: " + function_description(tool) + "\n" }}
- {{- "Input JSON schema: " + (function_parameters(tool) | tojson(ensure_ascii=False)) + "\n" }}
+ {{- "Input JSON schema: " + (function_parameters(tool) | tojson) + "\n" }}
{{- "\n" }}
{%- endif %}
{%- endmacro %}
@@ -56,7 +56,7 @@
{{- "\n" }}
{{- function.name }}
{{- "\n\n" }}
- {{- function.arguments | tojson(ensure_ascii=False) }}
+ {{- function.arguments | tojson }}
{{- "\n" }}
{%- endfor %}
{{- "\n" }}
From 0aba8f5ed3c6a3e65b5c7f15ec048f9cc802ef82 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Tue, 17 Mar 2026 17:48:51 +0800
Subject: [PATCH 6/7] Remove redundant user role check in template
---
models/templates/MiroThinker-v1.jinja | 1 -
1 file changed, 1 deletion(-)
diff --git a/models/templates/MiroThinker-v1.jinja b/models/templates/MiroThinker-v1.jinja
index eb2445cd3b..1da7b40217 100644
--- a/models/templates/MiroThinker-v1.jinja
+++ b/models/templates/MiroThinker-v1.jinja
@@ -159,7 +159,6 @@
{%- for message in messages %}
{%- if message.role == 'assistant' %}
- {%- if message.role == 'user' %}
{%- if message.tool_calls %}
{%- set message.content = message.content + render_tool_call() %}
{%- set message.tool_calls = [] %}
From b156e20b9e0c15eb7c71e326e52bbbbe9b0ab8f4 Mon Sep 17 00:00:00 2001
From: hksdpc255 <43977088+hksdpc255@users.noreply.github.com>
Date: Sat, 21 Mar 2026 21:39:57 +0800
Subject: [PATCH 7/7] Add structured outputs support for
common_chat_params_init_mirothinker
---
common/chat.cpp | 19 ++++++++++++++-----
1 file changed, 14 insertions(+), 5 deletions(-)
diff --git a/common/chat.cpp b/common/chat.cpp
index 27b3e28fb6..a457c6988e 100644
--- a/common/chat.cpp
+++ b/common/chat.cpp
@@ -1269,7 +1269,7 @@ static common_chat_params common_chat_params_init_kimi_k2(const common_chat_temp
// MiroThinker - uses MCP style toolcalling
static common_chat_params common_chat_params_init_mirothinker(const common_chat_template & tmpl,
- const autoparser::templates_params & inputs) {
+ const autoparser::generation_params & inputs) {
common_chat_params data;
data.prompt = common_chat_template_direct_apply(tmpl, inputs);
@@ -1282,9 +1282,10 @@ static common_chat_params common_chat_params_init_mirothinker(const common_chat_
"",
};
- auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
- auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
- auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
+ auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
+ auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
+ auto has_response_format = !inputs.json_schema.is_null() && inputs.json_schema.is_object();
+ auto include_grammar = has_response_format || (has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE);
auto parser = build_chat_peg_parser([&](common_chat_peg_builder & p) {
// MiroThinker Thinking format:
@@ -1312,6 +1313,14 @@ static common_chat_params common_chat_params_init_mirothinker(const common_chat_
auto end = p.end();
+ if (has_response_format) {
+ auto response_format = p.rule("response-format", p.content(p.schema(p.json(), "response-format-schema", inputs.json_schema)));
+ return reasoning + p.space() + p.choice({
+ p.literal("```json") + p.space() + response_format + p.space() + p.literal("```"),
+ response_format
+ }) + p.end();
+ }
+
// Content only parser (no tools)
if (!has_tools || inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_NONE) {
return reasoning + p.content(p.rest()) + end;
@@ -1360,7 +1369,7 @@ static common_chat_params common_chat_params_init_mirothinker(const common_chat_
data.parser = parser.save();
if (include_grammar) {
- data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO;
+ data.grammar_lazy = inputs.tool_choice == COMMON_CHAT_TOOL_CHOICE_AUTO || !has_response_format;
data.grammar = build_grammar([&](const common_grammar_builder & builder) {
foreach_function(inputs.tools, [&](const json & tool) {
const auto & function = tool.at("function");