Add chat parser for MiroThinker (#1138)

* Add chat parser for MiroThinker

* Add MiroThinker template

* delete space
This commit is contained in:
hksdpc255
2026-01-13 17:07:12 +11:00
committed by GitHub
parent 1a461525d5
commit 54a1f68d32
6 changed files with 284 additions and 4 deletions

View File

@@ -266,6 +266,17 @@ void build_grammar_xml_tool_call(common_chat_params & data, const json & tools,
if (data.format == COMMON_CHAT_FORMAT_KIMI_K2) {
quoted_name = "\"functions.\" " + quoted_name + " \":\" [0-9]+";
}
// MiroThinker uses {{ name_part_1 }}</server_name>\n<tool_name>{{ name_part_2 }} as function name
if (data.format == COMMON_CHAT_FORMAT_MIROTHINKER) {
auto server_split_pos = name.find("_");
if (std::string::npos == server_split_pos) {
quoted_name = "\"system_default</server_name>\\n<tool_name>\" " + quoted_name;
} else {
quoted_name = gbnf_format_literal(name.substr(0, server_split_pos)) +
" \"</server_name>\\n<tool_name>\" " +
gbnf_format_literal(name.substr(server_split_pos + 1));
}
}
tool_rules.push_back(builder.add_rule(name + "-call",
gbnf_format_literal(form.tool_start) + " " +
quoted_name + " " +
@@ -422,6 +433,10 @@ inline bool parse_xml_tool_calls(common_chat_msg_parser & builder, const struct
auto [sz, tc] = try_find_tool_end();
func_name = tc;
}
// Skip when tool_sep may be partial
if (builder.pos() == builder.input().size()) {
throw common_chat_msg_partial_exception("Partial literal: " + gbnf_format_literal(form.key_start));
}
// Parse tool name
builder.move_to(all_space(form.tool_sep) ? func_name->groups[0].begin : func_name->groups[0].end);
@@ -435,6 +450,17 @@ inline bool parse_xml_tool_calls(common_chat_msg_parser & builder, const struct
}
}
}
// MiroThinker uses {{ name_part_1 }}</server_name>\n<tool_name>{{ name_part_2 }} as function name
if (builder.syntax().format == COMMON_CHAT_FORMAT_MIROTHINKER) {
if (string_starts_with(function_name, "system_default</server_name>\n<tool_name>")) {
function_name = function_name.substr(14 + 26);
} else {
auto server_split_pos = function_name.find("</server_name>\n<tool_name>");
if (std::string::npos != server_split_pos) {
function_name = function_name.substr(0, server_split_pos) + "_" + function_name.substr(server_split_pos + 26);
}
}
}
// Argument JSON
json arguments = json::object();

View File

@@ -31,7 +31,7 @@ struct xml_tool_call_format {
std::optional<std::string> last_val_end = std::nullopt;
std::optional<std::string> last_tool_end = std::nullopt;
bool trim_raw_argval = false;
bool allow_toolcall_in_think = false; // TODO: UNTESTED!!!
bool allow_toolcall_in_think = false;
};
// make a GBNF that accept any strings except those containing any of the forbidden strings.

View File

@@ -967,6 +967,24 @@ static void common_chat_parse_xiaomi_mimo(common_chat_msg_parser & builder) {
builder.consume_reasoning_with_xml_tool_calls(form);
}
static void common_chat_parse_mirothinker(common_chat_msg_parser & builder) {
static const xml_tool_call_format form = ([]() {
xml_tool_call_format form {};
form.scope_start = "<use_mcp_tool>";
form.tool_start = "<server_name>";
form.tool_sep = "</tool_name>\n<arguments>\n{";
form.key_start = "\"";
form.key_val_sep = "\":";
form.val_end = ",";
form.tool_end = "}\n</arguments>";
form.scope_end = "</use_mcp_tool>";
form.raw_argval = false;
form.last_val_end = "";
return form;
})();
builder.consume_reasoning_with_xml_tool_calls(form);
}
static void common_chat_parse_gpt_oss(common_chat_msg_parser & builder) {
static const std::string constraint = "(?: (<\\|constrain\\|>)?([a-zA-Z0-9_-]+))";
static const std::string recipient("(?: to=functions\\.([^<\\s]+))");
@@ -1480,6 +1498,9 @@ static void common_chat_parse(common_chat_msg_parser & builder) {
case COMMON_CHAT_FORMAT_XIAOMI_MIMO:
common_chat_parse_xiaomi_mimo(builder);
break;
case COMMON_CHAT_FORMAT_MIROTHINKER:
common_chat_parse_mirothinker(builder);
break;
default:
throw std::runtime_error(std::string("Unsupported format: ") + common_chat_format_name(builder.syntax().format));
}

View File

@@ -649,6 +649,7 @@ const char * common_chat_format_name(common_chat_format format) {
case COMMON_CHAT_FORMAT_QWEN3_CODER_XML: return "Qwen3 Coder";
case COMMON_CHAT_FORMAT_APRIEL_1_5: return "Apriel 1.5";
case COMMON_CHAT_FORMAT_XIAOMI_MIMO: return "Xiaomi MiMo";
case COMMON_CHAT_FORMAT_: return "";
default:
throw std::runtime_error("Unknown chat format");
}
@@ -696,7 +697,8 @@ static std::string apply(
const struct templates_params & inputs,
const std::optional<json> & messages_override = std::nullopt,
const std::optional<json> & tools_override = std::nullopt,
const std::optional<json> & additional_context = std::nullopt)
const std::optional<json> & additional_context = std::nullopt,
const std::optional<minja::chat_template_options> & tmpl_opts = std::nullopt)
{
minja::chat_template_inputs tmpl_inputs;
tmpl_inputs.messages = messages_override ? *messages_override : inputs.messages;
@@ -713,11 +715,11 @@ static std::string apply(
// TODO: add flag to control date/time, if only for testing purposes.
// tmpl_inputs.now = std::chrono::system_clock::now();
minja::chat_template_options tmpl_opts;
minja::chat_template_options default_tmpl_opts;
// To avoid double BOS / EOS tokens, we're manually removing begining / trailing tokens
// instead of using `chat_template_options.use_bos_token = false`, since these tokens
// may be needed inside the template / between messages too.
auto result = tmpl.apply(tmpl_inputs, tmpl_opts);
auto result = tmpl.apply(tmpl_inputs, tmpl_opts ? *tmpl_opts : default_tmpl_opts);
if (inputs.add_bos && string_starts_with(result, tmpl.bos_token())) {
result = result.substr(tmpl.bos_token().size());
}
@@ -1351,6 +1353,53 @@ static common_chat_params common_chat_params_init_xiaomi_mimo(const common_chat_
form.tool_end = "}\n</tool_call>";
form.scope_end = "";
form.raw_argval = false;
form.last_val_end = "}";
return form;
})();
build_grammar_xml_tool_call(data, params.tools, form);
return data;
}
static common_chat_params common_chat_params_init_(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;
// Disable every Minja polyfill
minja::chat_template_options topts;
topts.apply_polyfills = true;
topts.polyfill_tools = false;
topts.polyfill_tool_call_examples = false;
topts.polyfill_tool_calls = false;
topts.polyfill_tool_responses = false;
topts.polyfill_system_role = false;
topts.polyfill_object_arguments = true;
topts.polyfill_typed_content = false;
topts.use_bos_token = true;
topts.use_eos_token = true;
data.prompt = apply(tmpl, params, std::nullopt, std::nullopt, std::nullopt, topts);
data.format = COMMON_CHAT_FORMAT_;
data.preserved_tokens = {
"<use_mcp_tool>", "</use_mcp_tool>",
"<server_name>", "</server_name>",
"<tool_name>", "</tool_name>",
"<arguments>", "</arguments>",
};
// build grammar for tool call
static const xml_tool_call_format form = ([]() {
xml_tool_call_format form {};
form.scope_start = "<use_mcp_tool>\n";
form.tool_start = "<server_name>\n";
form.tool_sep = "</tool_name>\n<arguments>\n{";
form.key_start = "\"";
form.key_val_sep = "\": ";
form.val_end = ", ";
form.tool_end = "}\n</arguments>";
form.scope_end = "</use_mcp_tool>";
form.raw_argval = false;
form.last_val_end = "";
return form;
})();
@@ -2032,6 +2081,14 @@ static common_chat_params common_chat_templates_apply_jinja(
return common_chat_params_init_xiaomi_mimo(tmpl, params);
}
// MiroThinker format detection (must come before Hermes 2 Pro)
if (src.find("</use_mcp_tool>") != std::string::npos &&
src.find("</server_name>") != std::string::npos &&
src.find("</tool_name>") != std::string::npos &&
src.find("</arguments>") != std::string::npos) {
return common_chat_params_init_mirothinker(tmpl, params);
}
// Hermes 2/3 Pro, Qwen 2.5 Instruct (w/ tools)
if (src.find("<tool_call>") != std::string::npos && params.json_schema.is_null()) {
return common_chat_params_init_hermes_2_pro(tmpl, params);

View File

@@ -123,6 +123,7 @@ enum common_chat_format {
COMMON_CHAT_FORMAT_QWEN3_CODER_XML,
COMMON_CHAT_FORMAT_APRIEL_1_5,
COMMON_CHAT_FORMAT_XIAOMI_MIMO,
COMMON_CHAT_FORMAT_MIROTHINKER,
COMMON_CHAT_FORMAT_COUNT, // Not a format, just the # formats
};