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 &&