Fix Kimi-K2 tool-call parsing issues (#17376)

* Fix kimi-k2 parsing

* fix template & add more tests for kimi-k2

* Another fix for Kimi-K2 chat template.

* enable allow_toolcall_in_think for Kimi-K2

* Refine key-value separator and value end format

* Enable tool call in think for kimi-k2

* allow_toolcall_in_think is now tested with Kimi-K2

* Remove outdated TODO comment in XML tool call parser

Removed TODO comment about untested tool call feature.

* Rename function from "utf8_truncate_safe" to "utf8_truncate_safe_len"
This commit is contained in:
hksdpc255 2025-12-09 00:32:04 +11:00 committed by GitHub
parent 51e0c2d917
commit 636fc17a37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 194 additions and 50 deletions

View File

@ -724,16 +724,10 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
if (reasoning_unclosed) { if (reasoning_unclosed) {
if (auto pos = content.find(end_think); pos == std::string::npos && builder.pos() != builder.input().size()) { if (auto pos = content.find(end_think); pos == std::string::npos && builder.pos() != builder.input().size()) {
unclosed_reasoning_content += content; unclosed_reasoning_content += content;
if (form.allow_toolcall_in_think) { if (!(form.allow_toolcall_in_think && tc)) {
builder.move_to(tc->groups[0].begin);
if (!builder.try_consume_xml_tool_calls(form)) {
unclosed_reasoning_content += tool_call_start; unclosed_reasoning_content += tool_call_start;
builder.move_to(tc->groups[0].end);
}
} else {
unclosed_reasoning_content += tool_call_start;
}
continue; continue;
}
} else { } else {
reasoning_unclosed = false; reasoning_unclosed = false;
std::string reasoning_content; std::string reasoning_content;
@ -781,8 +775,12 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
} }
} else { } else {
// This <tool_call> start is in thinking block, skip this tool call // This <tool_call> start is in thinking block, skip this tool call
auto pos = think_start + start_think.size(); // This <tool_call> start is in thinking block
unclosed_reasoning_content = content.substr(pos) + tool_call_start; if (form.allow_toolcall_in_think) {
unclosed_reasoning_content = content.substr(think_start + start_think.size());
} else {
unclosed_reasoning_content = content.substr(think_start + start_think.size()) + tool_call_start;
}
reasoning_unclosed = true; reasoning_unclosed = true;
content.resize(think_start); content.resize(think_start);
toolcall_in_think = true; toolcall_in_think = true;
@ -805,14 +803,35 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
} }
// remove potential partial suffix // remove potential partial suffix
if (content.size() > 0 && builder.pos() == builder.input().size() && unclosed_reasoning_content.empty()) { if (builder.pos() == builder.input().size()) {
if (unclosed_reasoning_content.empty()) {
rstrip(content); rstrip(content);
trim_potential_partial_word(content); trim_potential_partial_word(content);
rstrip(content); rstrip(content);
} else {
rstrip(unclosed_reasoning_content);
trim_potential_partial_word(unclosed_reasoning_content);
rstrip(unclosed_reasoning_content);
}
}
// consume unclosed_reasoning_content if allow_toolcall_in_think is set
if (form.allow_toolcall_in_think && !unclosed_reasoning_content.empty()) {
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content) {
builder.add_reasoning_content(unclosed_reasoning_content);
} else {
if (content.empty()) {
content = start_think + unclosed_reasoning_content;
} else {
content += "\n\n" + start_think;
content += unclosed_reasoning_content;
}
}
unclosed_reasoning_content.clear();
} }
// Add content // Add content
if (content.size() != 0) { if (!content.empty()) {
// If there are multiple content blocks // If there are multiple content blocks
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content && builder.result().content.size() != 0) { if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content && builder.result().content.size() != 0) {
builder.add_content("\n\n"); builder.add_content("\n\n");
@ -820,7 +839,7 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
builder.add_content(content); builder.add_content(content);
} }
// This <tool_call> start is in thinking block, skip this tool call // This <tool_call> start is in thinking block and toolcall_in_think not set, skip this tool call
if (toolcall_in_think && !form.allow_toolcall_in_think) { if (toolcall_in_think && !form.allow_toolcall_in_think) {
continue; continue;
} }
@ -829,7 +848,7 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
if (!tc) { if (!tc) {
GGML_ASSERT(builder.pos() == builder.input().size()); GGML_ASSERT(builder.pos() == builder.input().size());
GGML_ASSERT(unclosed_reasoning_content.empty()); GGML_ASSERT(unclosed_reasoning_content.empty());
GGML_ASSERT(!reasoning_unclosed); if (!form.allow_toolcall_in_think) GGML_ASSERT(!reasoning_unclosed);
break; break;
} }
@ -854,7 +873,6 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
/** /**
* Parse content uses reasoning and XML-Style tool call * Parse content uses reasoning and XML-Style tool call
* TODO: Note that form.allow_toolcall_in_think is not tested yet. If anyone confirms it works, this comment can be removed.
*/ */
void common_chat_msg_parser::consume_reasoning_with_xml_tool_calls(const struct xml_tool_call_format & form, const std::string & start_think, const std::string & end_think) { void common_chat_msg_parser::consume_reasoning_with_xml_tool_calls(const struct xml_tool_call_format & form, const std::string & start_think, const std::string & end_think) {
parse_msg_with_xml_tool_calls(*this, form, start_think, end_think); parse_msg_with_xml_tool_calls(*this, form, start_think, end_think);

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_val_end = std::nullopt;
std::optional<std::string> last_tool_end = std::nullopt; std::optional<std::string> last_tool_end = std::nullopt;
bool trim_raw_argval = false; 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. // make a GBNF that accept any strings except those containing any of the forbidden strings.

View File

@ -923,6 +923,7 @@ static void common_chat_parse_kimi_k2(common_chat_msg_parser & builder) {
form.scope_end = "<|tool_calls_section_end|>"; form.scope_end = "<|tool_calls_section_end|>";
form.raw_argval = false; form.raw_argval = false;
form.last_val_end = ""; form.last_val_end = "";
form.allow_toolcall_in_think = true;
return form; return form;
})(); })();
builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>"); builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");

View File

@ -14,7 +14,7 @@
{%- endmacro %} {%- endmacro %}
{%- set tool_response_queue = namespace(ids=[]) -%} {%- set tool_response_queue = namespace(ids=[]) -%}
{%- set tool_call_counter = namespace(value=1) -%} {%- set tool_call_counter = namespace(value=0) -%}
{%- if tools -%} {%- if tools -%}
<|im_system|>tool_declare<|im_middle|>{{ tools | tojson }}<|im_end|> <|im_system|>tool_declare<|im_middle|>{{ tools | tojson }}<|im_end|>
@ -36,12 +36,8 @@
{%- if message['role'] == 'assistant' and message.get('tool_calls') -%} {%- if message['role'] == 'assistant' and message.get('tool_calls') -%}
{{render_content(message)}}<|tool_calls_section_begin|> {{render_content(message)}}<|tool_calls_section_begin|>
{%- for tool_call in message['tool_calls'] -%} {%- for tool_call in message['tool_calls'] -%}
{%- if tool_call['id'] is defined -%}
{%- set formatted_id = tool_call['id'] -%}
{%- else -%}
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%} {%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%} {%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
{%- endif -%}
{%- set _ = tool_response_queue.ids.append(formatted_id) -%} {%- set _ = tool_response_queue.ids.append(formatted_id) -%}
<|tool_call_begin|>{{ formatted_id }}<|tool_call_argument_begin|>{% if tool_call['function']['arguments'] is string %}{{ tool_call['function']['arguments'] }}{% else %}{{ tool_call['function']['arguments'] | tojson }}{% endif %}<|tool_call_end|> <|tool_call_begin|>{{ formatted_id }}<|tool_call_argument_begin|>{% if tool_call['function']['arguments'] is string %}{{ tool_call['function']['arguments'] }}{% else %}{{ tool_call['function']['arguments'] | tojson }}{% endif %}<|tool_call_end|>
{%- endfor -%} {%- endfor -%}

View File

@ -25,17 +25,13 @@
{%- endmacro -%} {%- endmacro -%}
{%- set tool_response_queue = namespace(ids=[]) -%} {%- set tool_response_queue = namespace(ids=[]) -%}
{%- set tool_call_counter = namespace(value=1) -%} {%- set tool_call_counter = namespace(value=0) -%}
{%- macro render_toolcalls(message) -%} {%- macro render_toolcalls(message) -%}
<|tool_calls_section_begin|> <|tool_calls_section_begin|>
{%- for tool_call in message['tool_calls'] -%} {%- for tool_call in message['tool_calls'] -%}
{%- if tool_call['id'] is defined -%}
{%- set formatted_id = tool_call['id'] -%}
{%- else -%}
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%} {%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%} {%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
{%- endif -%}
{%- set _ = tool_response_queue.ids.append(formatted_id) -%} {%- set _ = tool_response_queue.ids.append(formatted_id) -%}
<|tool_call_begin|>{{ formatted_id }}<|tool_call_argument_begin|>{% if tool_call['function']['arguments'] is string %}{{ tool_call['function']['arguments'] }}{% else %}{{ tool_call['function']['arguments'] | tojson }}{% endif %}<|tool_call_end|> <|tool_call_begin|>{{ formatted_id }}<|tool_call_argument_begin|>{% if tool_call['function']['arguments'] is string %}{{ tool_call['function']['arguments'] }}{% else %}{{ tool_call['function']['arguments'] | tojson }}{% endif %}<|tool_call_end|>
{%- endfor -%} {%- endfor -%}

View File

@ -428,10 +428,38 @@ static void test_templates(const struct common_chat_templates * tmpls, const std
*/ */
template <typename T> template <typename T>
static void test_parser_with_streaming(const common_chat_msg & expected, const std::string & raw_message, T parse_msg) { static void test_parser_with_streaming(const common_chat_msg & expected, const std::string & raw_message, T parse_msg) {
constexpr auto utf8_truncate_safe_len = [](const std::string_view s) -> size_t {
auto len = s.size();
if (len == 0) return 0;
auto i = len;
for (size_t back = 0; back < 4 && i > 0; ++back) {
--i;
unsigned char c = s[i];
if ((c & 0x80) == 0) {
return len;
} else if ((c & 0xC0) == 0xC0) {
size_t expected_len = 0;
if ((c & 0xE0) == 0xC0) expected_len = 2;
else if ((c & 0xF0) == 0xE0) expected_len = 3;
else if ((c & 0xF8) == 0xF0) expected_len = 4;
else return i;
if (len - i >= expected_len) {
return len;
} else {
return i;
}
}
}
return len - std::min(len, size_t(3));
};
constexpr auto utf8_truncate_safe_view = [utf8_truncate_safe_len](const std::string_view s) {
return s.substr(0, utf8_truncate_safe_len(s));
};
auto merged = simple_assist_msg(""); auto merged = simple_assist_msg("");
auto last_msg = parse_msg(""); auto last_msg = parse_msg("");
for (size_t i = 1; i <= raw_message.size(); ++i) { for (size_t i = 1; i <= raw_message.size(); ++i) {
auto curr_msg = parse_msg(raw_message.substr(0, i)); auto curr_msg = parse_msg(std::string(utf8_truncate_safe_view(std::string_view(raw_message).substr(0, i))));
if (curr_msg == simple_assist_msg("")) continue; if (curr_msg == simple_assist_msg("")) continue;
LOG_INF("Streaming msg: %s\n", common_chat_msgs_to_json_oaicompat<json>({curr_msg}).dump().c_str()); LOG_INF("Streaming msg: %s\n", common_chat_msgs_to_json_oaicompat<json>({curr_msg}).dump().c_str());
for (auto diff: common_chat_msg_diff::compute_diffs(last_msg, curr_msg)) { for (auto diff: common_chat_msg_diff::compute_diffs(last_msg, curr_msg)) {
@ -2659,14 +2687,14 @@ Hey there!<|im_end|>
// Test parsing tool calls // Test parsing tool calls
assert_msg_equals(message_assist_call, assert_msg_equals(message_assist_call,
common_chat_parse( common_chat_parse(
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
/* is_partial= */ false, /* is_partial= */ false,
{COMMON_CHAT_FORMAT_KIMI_K2})); {COMMON_CHAT_FORMAT_KIMI_K2}));
// Test parsing tool calls with thinking // Test parsing tool calls with thinking
assert_msg_equals(message_assist_call_thoughts, assert_msg_equals(message_assist_call_thoughts,
common_chat_parse( common_chat_parse(
"<think>I'm\nthinking</think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking</think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
/* is_partial= */ false, /* is_partial= */ false,
{ {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
@ -2676,7 +2704,7 @@ Hey there!<|im_end|>
// Test tool calls with extra content // Test tool calls with extra content
assert_msg_equals(message_assist_call_content, assert_msg_equals(message_assist_call_content,
common_chat_parse( common_chat_parse(
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>Hello, world!\nWhat's up?", "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>Hello, world!\nWhat's up?",
/* is_partial= */ false, /* is_partial= */ false,
{COMMON_CHAT_FORMAT_KIMI_K2} {COMMON_CHAT_FORMAT_KIMI_K2}
)); ));
@ -2684,7 +2712,7 @@ Hey there!<|im_end|>
// Test tool calls with extra content AND thinking // Test tool calls with extra content AND thinking
assert_msg_equals(message_assist_call_thoughts_content, assert_msg_equals(message_assist_call_thoughts_content,
common_chat_parse( common_chat_parse(
"<think>I'm\nthinking</think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>Hello, world!\nWhat's up?", "<think>I'm\nthinking</think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>Hello, world!\nWhat's up?",
/* is_partial= */ false, /* is_partial= */ false,
{ {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
@ -2693,47 +2721,152 @@ Hey there!<|im_end|>
// Test streaming // Test streaming
test_parser_with_streaming(message_assist_call_thoughts_content, test_parser_with_streaming(message_assist_call_thoughts_content,
"<think>I'm\nthinking\n</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking\n</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); }); }); });
test_parser_with_streaming(message_assist_call_thoughts_unparsed, test_parser_with_streaming(message_assist_call_thoughts_unparsed,
"<think>I'm\nthinking</think>\n\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking</think>\n\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE /* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE
}); }); }); });
test_parser_with_streaming(message_assist_call_thoughts_content, test_parser_with_streaming(message_assist_call_thoughts_content,
"<think>I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?\n\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>\n", "<think>I'm\nthinking\n</think>\n\nHello, world!\nWhat's up?\n\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>\n",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); }); }); });
test_parser_with_streaming(message_assist_call_withopt, test_parser_with_streaming(message_assist_call_withopt,
"<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|><|tool_calls_section_end|>", "<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:0<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE /* .reasoning_format = */ COMMON_REASONING_FORMAT_NONE
}); }); }); });
test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": \"123456\"}"), test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": \"123456\"}"),
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": \"123456\"}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": \"123456\"}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); }); }); });
test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": [1, 2, \"345\", 6]}"), test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": [1, 2, \"345\", 6]}"),
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": [1, 2, \"345\", 6]}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": [1, 2, \"345\", 6]}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); }); }); });
test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": {\"12\": 34, \"5\": [67, 8], \"9\": \"10\"}}"), test_parser_with_streaming(simple_assist_msg("Hello, world!\nWhat's up?", "I'm\nthinking", "special_function", "{\"arg1\": {\"12\": 34, \"5\": [67, 8], \"9\": \"10\"}}"),
"<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": {\"12\": 34, \"5\": [67, 8], \"9\": \"10\"}}<|tool_call_end|><|tool_calls_section_end|>", "<think>I'm\nthinking</think>Hello, world!\nWhat's up?\n<|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": {\"12\": 34, \"5\": [67, 8], \"9\": \"10\"}}<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, { [&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
/* .format = */ COMMON_CHAT_FORMAT_KIMI_K2, /* .format = */ COMMON_CHAT_FORMAT_KIMI_K2,
/* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK /* .reasoning_format = */ COMMON_REASONING_FORMAT_DEEPSEEK
}); }); }); });
test_parser_with_streaming(
simple_assist_msg("", "", "complex_function", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}"),
"<|tool_calls_section_begin|><|tool_call_begin|>functions.complex_function:0<|tool_call_argument_begin|>"
"{\"name\": \"John Doe\", \"age\": 30, \"active\": true, \"score\": 95.5}"
"<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_KIMI_K2}); });
test_parser_with_streaming(
simple_assist_msg("", "", "web_search", "{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}"),
"<|tool_calls_section_begin|><|tool_call_begin|>functions.web_search:0<|tool_call_argument_begin|>"
"{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}"
"<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_KIMI_K2}); });
test_parser_with_streaming(
simple_assist_msg("", "", "read_file", "{\"args\": [{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]}"),
"<|tool_calls_section_begin|><|tool_call_begin|>functions.read_file:0<|tool_call_argument_begin|>"
"{\"args\": [{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]}"
"<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_KIMI_K2}); });
test_parser_with_streaming(
simple_assist_msg(
"Let me start by examining the relevant files to understand the current implementation.", "",
"read_file",
"{\"files\": [{\"path\": \"src/app/Partners.tsx\", \"line_ranges\": [\"1-100\"]}]}"),
"Let me start by examining the relevant files to understand the current implementation."
"<|tool_calls_section_begin|><|tool_call_begin|>functions.read_file:0<|tool_call_argument_begin|>"
"{\"files\":[{\"path\":\"src/app/Partners.tsx\",\"line_ranges\":[\"1-100\"]}]}"
"<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {COMMON_CHAT_FORMAT_KIMI_K2}); });
auto multi_tool_msg = simple_assist_msg("Let me call multiple tools.", "I'm thinking.");
multi_tool_msg.tool_calls.push_back({ "read_file", "{\"files\": [{\"path\": \"src/app/Partners.tsx\", \"line_ranges\": [\"1-100\"]}]}", "" });
multi_tool_msg.tool_calls.push_back({ "web_search", "{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}", "" });
multi_tool_msg.tool_calls.push_back({ "complex_function", "{\"name\": \"John Doe\", \"age\": 30, \"active\": true, \"score\": 95.5}", "" });
multi_tool_msg.tool_calls.push_back({ "emoji_function", "{\"message\":\"Hello! 👋 🌟 🚀 Testing emojis: 😀😃😄😁 and symbols: ∑∏∆∇\"}", "" });
test_parser_with_streaming(multi_tool_msg,
"<think>I'm thinking.</think>Let me call multiple tools."
"<|tool_calls_section_begin|>"
"<|tool_call_begin|>functions.read_file:0<|tool_call_argument_begin|>"
"{\"files\":[{\"path\":\"src/app/Partners.tsx\",\"line_ranges\":[\"1-100\"]}]}"
"<|tool_call_end|>"
"<|tool_call_begin|>functions.web_search:1<|tool_call_argument_begin|>"
"{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}"
"<|tool_call_end|>"
"<|tool_call_begin|>functions.complex_function:2<|tool_call_argument_begin|>"
"{\"name\": \"John Doe\", \"age\": 30, \"active\": true, \"score\": 95.5}"
"<|tool_call_end|>"
"<|tool_call_begin|>functions.emoji_function:3<|tool_call_argument_begin|>"
"{\"message\":\"Hello! 👋 🌟 🚀 Testing emojis: 😀😃😄😁 and symbols: ∑∏∆∇\"}"
"<|tool_call_end|>"
"<|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
COMMON_CHAT_FORMAT_KIMI_K2,
COMMON_REASONING_FORMAT_DEEPSEEK
}); });
test_parser_with_streaming(
simple_assist_msg("", "I'm thinking", "complex_function_in_think", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}"),
"<think>I'm thinking<|tool_calls_section_begin|><|tool_call_begin|>functions.complex_function_in_think:0<|tool_call_argument_begin|>"
"{\"name\": \"John Doe\", \"age\": 30, \"active\": true, \"score\": 95.5}"
"<|tool_call_end|><|tool_calls_section_end|>",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
COMMON_CHAT_FORMAT_KIMI_K2,
COMMON_REASONING_FORMAT_DEEPSEEK
}); });
test_parser_with_streaming(
simple_assist_msg("Hello", "I'm thinkingI'm still thinking", "complex_function_in_think", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}"),
"<think>I'm thinking<|tool_calls_section_begin|><|tool_call_begin|>functions.complex_function_in_think:0<|tool_call_argument_begin|>"
"{\"name\": \"John Doe\", \"age\": 30, \"active\": true, \"score\": 95.5}"
"<|tool_call_end|><|tool_calls_section_end|>I'm still thinking</think>Hello",
[&](const std::string &msg) { return common_chat_parse(msg, /* is_partial= */ true, {
COMMON_CHAT_FORMAT_KIMI_K2,
COMMON_REASONING_FORMAT_DEEPSEEK
}); });
// Test template rendering
common_chat_templates_inputs conversation_with_tools = inputs_tools;
conversation_with_tools.messages.push_back(simple_assist_msg("Let's do it", "Think first", "complex_function", "{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 1",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "complex_function",
/* .tool_call_id = */ "",
});
conversation_with_tools.messages.push_back(simple_assist_msg("Continue", "Think next", "web_search", "{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 2",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "web_search",
/* .tool_call_id = */ "",
});
conversation_with_tools.messages.push_back(simple_assist_msg("CC", "Think last", "read_file", "{\"args\": [{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]}"));
conversation_with_tools.messages.push_back({
"tool",
"Tool response 3",
/* .content_parts = */ {},
/* .tool_calls = */ {},
/* .reasoning_content = */ "",
/* .tool_name = */ "read_file",
/* .tool_call_id = */ "",
});
assert_equals(common_chat_templates_apply(tmpls.get(), conversation_with_tools).prompt, std::string("<|im_system|>tool_declare<|im_middle|>[{\"type\": \"function\", \"function\": {\"name\": \"special_function\", \"description\": \"I'm special\", \"parameters\": {\"type\": \"object\", \"properties\": {\"arg1\": {\"type\": \"integer\", \"description\": \"The arg.\"}}, \"required\": [\"arg1\"]}}}]<|im_end|><|im_system|>system<|im_middle|>You are Kimi, an AI assistant created by Moonshot AI.<|im_end|><|im_user|>user<|im_middle|>Hey there!<|im_end|><|im_assistant|>assistant<|im_middle|><think>Think first</think>Let's do it<|tool_calls_section_begin|><|tool_call_begin|>functions.complex_function:0<|tool_call_argument_begin|>{\"name\":\"John Doe\",\"age\":30,\"active\":true,\"score\":95.5}<|tool_call_end|><|tool_calls_section_end|><|im_end|><|im_system|>complex_function<|im_middle|>## Return of functions.complex_function:0\nTool response 1<|im_end|><|im_assistant|>assistant<|im_middle|><think>Think next</think>Continue<|tool_calls_section_begin|><|tool_call_begin|>functions.web_search:1<|tool_call_argument_begin|>{\"query\":\"\\\"From Zero\\\" Linkin Park album tracklist complete songs\",\"limit\":3,\"type\":\"text\"}<|tool_call_end|><|tool_calls_section_end|><|im_end|><|im_system|>web_search<|im_middle|>## Return of functions.web_search:1\nTool response 2<|im_end|><|im_assistant|>assistant<|im_middle|><think>Think last</think>CC<|tool_calls_section_begin|><|tool_call_begin|>functions.read_file:2<|tool_call_argument_begin|>{\"args\": [{\"path\": \"src/providers/ThemeProvider.tsx\"}, {\"path\": \"src/components/Header.tsx\"}, {\"path\": \"src/components/ThemeToggle.tsx\"}, {\"path\": \"src/app/globals.css\"}, {\"path\": \"src/app/layout.tsx\"}]}<|tool_call_end|><|tool_calls_section_end|><|im_end|><|im_system|>read_file<|im_middle|>## Return of functions.read_file:2\nTool response 3<|im_end|><|im_assistant|>assistant<|im_middle|>"));
// Test template generation for regular content // Test template generation for regular content
test_templates(tmpls.get(), end_tokens, message_assist, tools, test_templates(tmpls.get(), end_tokens, message_assist, tools,
@ -2742,7 +2875,7 @@ Hey there!<|im_end|>
// Test template generation for tool calls // Test template generation for tool calls
test_templates(tmpls.get(), end_tokens, message_assist_call, tools, test_templates(tmpls.get(), end_tokens, message_assist_call, tools,
"<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
/* expect_grammar_triggered= */ true, /* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true, /* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK, /* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,
@ -2751,14 +2884,14 @@ Hey there!<|im_end|>
// Test template generation for tools with optional parameters // Test template generation for tools with optional parameters
test_templates(tmpls.get(), end_tokens, message_assist_call_noopt, tools, test_templates(tmpls.get(), end_tokens, message_assist_call_noopt, tools,
"<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>", "<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:0<|tool_call_argument_begin|>{\"arg1\": 1}<|tool_call_end|><|tool_calls_section_end|>",
/* expect_grammar_triggered= */ true, /* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true, /* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK, /* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,
/* ignore_whitespace_differences= */ true /* ignore_whitespace_differences= */ true
); );
test_templates(tmpls.get(), end_tokens, message_assist_call_withopt, tools, test_templates(tmpls.get(), end_tokens, message_assist_call_withopt, tools,
"<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:1<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|><|tool_calls_section_end|>", "<think></think><|tool_calls_section_begin|><|tool_call_begin|>functions.special_function_with_opt:0<|tool_call_argument_begin|>{\"arg1\": 1, \"arg2\": 2}<|tool_call_end|><|tool_calls_section_end|>",
/* expect_grammar_triggered= */ true, /* expect_grammar_triggered= */ true,
/* test_grammar_if_triggered= */ true, /* test_grammar_if_triggered= */ true,
/* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK, /* common_reasoning_format= */ COMMON_REASONING_FORMAT_DEEPSEEK,