mirror of
https://github.com/ikawrakow/ik_llama.cpp.git
synced 2026-01-26 17:20:01 +00:00
Add chat parser for MiroThinker (#1138)
* Add chat parser for MiroThinker * Add MiroThinker template * delete space
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
175
models/templates/MiroThinker.jinja
Normal file
175
models/templates/MiroThinker.jinja
Normal file
@@ -0,0 +1,175 @@
|
||||
{#- ========== 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 <use_mcp_tool></use_mcp_tool> 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<use_mcp_tool>\n<server_name>server name here</server_name>\n<tool_name>tool name here</tool_name>\n<arguments>\n{\n\"param1\": \"value1\",\n\"param2\": \"value2 \\\"escaped string\\\"\"\n}\n</arguments>\n</use_mcp_tool>\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='') %}
|
||||
{%- for tool in tools %}
|
||||
{%- set tool_name = tool.function.name.split('_') %}
|
||||
{%- if tool_name | length > 1 %}
|
||||
{%- set curr_server = tool_name[0] %}
|
||||
{%- set tool_name = tool_name[1:] | join('_') %}
|
||||
{%- else %}
|
||||
{%- set curr_server = 'system_default' %}
|
||||
{%- set tool_name = tool.function.name %}
|
||||
{%- endif %}
|
||||
{%- if curr_server != ns.last_server %}
|
||||
{%- set ns.formatted_tools = ns.formatted_tools + "\n## Server name: " + curr_server + "\n" %}
|
||||
{%- set ns.last_server = curr_server %}
|
||||
{%- endif %}
|
||||
{%- set ns.formatted_tools = ns.formatted_tools + "### Tool name: " + tool_name + "\n" %}
|
||||
{%- set ns.formatted_tools = ns.formatted_tools + "Description: " + tool.function.description + "\n" %}
|
||||
{%- set ns.formatted_tools = ns.formatted_tools + "Input JSON schema: " + ( tool.parameters | tojson(ensure_ascii=False) ) + "\n" %}
|
||||
{%- set ns.formatted_tools = ns.formatted_tools + '\n' %}
|
||||
{%- 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('</use_mcp_tool>') | length > 1 %}
|
||||
{%- set system_message = messages[0] %}
|
||||
{%- set messages = messages[1:] %}
|
||||
{%- endif %}
|
||||
{%- if messages[0].content.split('</tools>') | 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 Tool Response ========== #}
|
||||
|
||||
{%- for message in messages %}
|
||||
{%- if message.role == 'assistant' %}
|
||||
{%- if message.tool_calls %}
|
||||
{%- set message.content = message.content + "\n<use_mcp_tool>" %}
|
||||
{%- for tool_call in message.tool_calls %}
|
||||
{%- set tool = tool_call.function %}
|
||||
{%- set tool_name = tool.name.split('_') %}
|
||||
{%- if tool_name | length > 1 %}
|
||||
{%- set server_name = tool_name[0] %}
|
||||
{%- set tool_name = tool_name[1:] | join('_') %}
|
||||
{%- else %}
|
||||
{%- set server_name = 'system_default' %}
|
||||
{%- set tool_name = tool.name %}
|
||||
{%- endif %}
|
||||
{%- set message.content = message.content + "\n<server_name>" + server_name + "</server_name>\n<tool_name>" + tool_name + "</tool_name>\n<arguments>\n" + (tool.arguments | tojson(ensure_ascii=False)) + "\n</arguments>" %}
|
||||
{%- endfor %}
|
||||
{%- set message.content = message.content + "\n</use_mcp_tool>" %}
|
||||
{%- set message.tool_calls = [] %}
|
||||
{%- endif %}
|
||||
{%- elif message.role == 'user' %}
|
||||
{%- if message.content.startswith('<tool_response>') %}
|
||||
{%- set message.content = message.content[15:] | trim %}
|
||||
{%- 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 <tools></tools> XML tags:\n<tools>" }}
|
||||
{%- for tool in tools %}
|
||||
{{- "\n" }}
|
||||
{{- tool | tojson }}
|
||||
{%- endfor %}
|
||||
{{- "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call><|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 = '<tool_response>' %}
|
||||
{%- set tool_start_length = tool_start|length %}
|
||||
{%- set start_of_message = current_content[:tool_start_length] %}
|
||||
{%- set tool_end = '</tool_response>' %}
|
||||
{%- 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 '</think>' in message.content %}
|
||||
{%- set content = (message.content.split('</think>')|last).lstrip('\n') %}
|
||||
{%- set reasoning_content = (message.content.split('</think>')|first).rstrip('\n') %}
|
||||
{%- set reasoning_content = (reasoning_content.split('<think>')|last).lstrip('\n') %}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
{%- if loop.index0 > ns.last_query_index %}
|
||||
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\n\n' + content.lstrip('\n') }}
|
||||
{%- else %}
|
||||
{{- '<|im_start|>' + message.role + '\n<think>\n' + reasoning_content.strip('\n') + '\n</think>\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 %}
|
||||
{{- '<tool_call>\n{"name": "' }}
|
||||
{{- tool_call.name }}
|
||||
{{- '", "arguments": ' }}
|
||||
{%- if tool_call.arguments is string %}
|
||||
{{- tool_call.arguments }}
|
||||
{%- else %}
|
||||
{{- tool_call.arguments | tojson }}
|
||||
{%- endif %}
|
||||
{{- '}\n</tool_call>' }}
|
||||
{%- endfor %}
|
||||
{%- endif %}
|
||||
{{- '<|im_end|>\n' }}
|
||||
{%- elif message.role == "tool" %}
|
||||
{%- if loop.first or (messages[loop.index0 - 1].role != "tool") %}
|
||||
{{- '<|im_start|>user' }}
|
||||
{%- endif %}
|
||||
{{- '\n<tool_response>\n' }}
|
||||
{{- message.content }}
|
||||
{{- '\n</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' }}
|
||||
{%- if enable_thinking is defined and enable_thinking is false %}
|
||||
{{- '<think>\n\n</think>\n\n' }}
|
||||
{%- endif %}
|
||||
{%- endif %}
|
||||
Reference in New Issue
Block a user