Skip to content

Commit 636fc17

Browse files
authored
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"
1 parent 51e0c2d commit 636fc17

File tree

6 files changed

+194
-50
lines changed

6 files changed

+194
-50
lines changed

common/chat-parser-xml-toolcall.cpp

Lines changed: 36 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -724,16 +724,10 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
724724
if (reasoning_unclosed) {
725725
if (auto pos = content.find(end_think); pos == std::string::npos && builder.pos() != builder.input().size()) {
726726
unclosed_reasoning_content += content;
727-
if (form.allow_toolcall_in_think) {
728-
builder.move_to(tc->groups[0].begin);
729-
if (!builder.try_consume_xml_tool_calls(form)) {
730-
unclosed_reasoning_content += tool_call_start;
731-
builder.move_to(tc->groups[0].end);
732-
}
733-
} else {
727+
if (!(form.allow_toolcall_in_think && tc)) {
734728
unclosed_reasoning_content += tool_call_start;
729+
continue;
735730
}
736-
continue;
737731
} else {
738732
reasoning_unclosed = false;
739733
std::string reasoning_content;
@@ -781,8 +775,12 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
781775
}
782776
} else {
783777
// This <tool_call> start is in thinking block, skip this tool call
784-
auto pos = think_start + start_think.size();
785-
unclosed_reasoning_content = content.substr(pos) + tool_call_start;
778+
// This <tool_call> start is in thinking block
779+
if (form.allow_toolcall_in_think) {
780+
unclosed_reasoning_content = content.substr(think_start + start_think.size());
781+
} else {
782+
unclosed_reasoning_content = content.substr(think_start + start_think.size()) + tool_call_start;
783+
}
786784
reasoning_unclosed = true;
787785
content.resize(think_start);
788786
toolcall_in_think = true;
@@ -805,22 +803,43 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
805803
}
806804

807805
// remove potential partial suffix
808-
if (content.size() > 0 && builder.pos() == builder.input().size() && unclosed_reasoning_content.empty()) {
809-
rstrip(content);
810-
trim_potential_partial_word(content);
811-
rstrip(content);
806+
if (builder.pos() == builder.input().size()) {
807+
if (unclosed_reasoning_content.empty()) {
808+
rstrip(content);
809+
trim_potential_partial_word(content);
810+
rstrip(content);
811+
} else {
812+
rstrip(unclosed_reasoning_content);
813+
trim_potential_partial_word(unclosed_reasoning_content);
814+
rstrip(unclosed_reasoning_content);
815+
}
816+
}
817+
818+
// consume unclosed_reasoning_content if allow_toolcall_in_think is set
819+
if (form.allow_toolcall_in_think && !unclosed_reasoning_content.empty()) {
820+
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content) {
821+
builder.add_reasoning_content(unclosed_reasoning_content);
822+
} else {
823+
if (content.empty()) {
824+
content = start_think + unclosed_reasoning_content;
825+
} else {
826+
content += "\n\n" + start_think;
827+
content += unclosed_reasoning_content;
828+
}
829+
}
830+
unclosed_reasoning_content.clear();
812831
}
813832

814833
// Add content
815-
if (content.size() != 0) {
834+
if (!content.empty()) {
816835
// If there are multiple content blocks
817836
if (builder.syntax().reasoning_format != COMMON_REASONING_FORMAT_NONE && !builder.syntax().reasoning_in_content && builder.result().content.size() != 0) {
818837
builder.add_content("\n\n");
819838
}
820839
builder.add_content(content);
821840
}
822841

823-
// This <tool_call> start is in thinking block, skip this tool call
842+
// This <tool_call> start is in thinking block and toolcall_in_think not set, skip this tool call
824843
if (toolcall_in_think && !form.allow_toolcall_in_think) {
825844
continue;
826845
}
@@ -829,7 +848,7 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
829848
if (!tc) {
830849
GGML_ASSERT(builder.pos() == builder.input().size());
831850
GGML_ASSERT(unclosed_reasoning_content.empty());
832-
GGML_ASSERT(!reasoning_unclosed);
851+
if (!form.allow_toolcall_in_think) GGML_ASSERT(!reasoning_unclosed);
833852
break;
834853
}
835854

@@ -854,7 +873,6 @@ inline void parse_msg_with_xml_tool_calls(common_chat_msg_parser & builder, cons
854873

855874
/**
856875
* Parse content uses reasoning and XML-Style tool call
857-
* TODO: Note that form.allow_toolcall_in_think is not tested yet. If anyone confirms it works, this comment can be removed.
858876
*/
859877
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) {
860878
parse_msg_with_xml_tool_calls(*this, form, start_think, end_think);

common/chat-parser-xml-toolcall.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ struct xml_tool_call_format {
3131
std::optional<std::string> last_val_end = std::nullopt;
3232
std::optional<std::string> last_tool_end = std::nullopt;
3333
bool trim_raw_argval = false;
34-
bool allow_toolcall_in_think = false; // TODO: UNTESTED!!!
34+
bool allow_toolcall_in_think = false;
3535
};
3636

3737
// make a GBNF that accept any strings except those containing any of the forbidden strings.

common/chat-parser.cpp

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -917,12 +917,13 @@ static void common_chat_parse_kimi_k2(common_chat_msg_parser & builder) {
917917
form.tool_start = "<|tool_call_begin|>";
918918
form.tool_sep = "<|tool_call_argument_begin|>{";
919919
form.key_start = "\"";
920-
form.key_val_sep = "\": ";
921-
form.val_end = ", ";
920+
form.key_val_sep = "\":";
921+
form.val_end = ",";
922922
form.tool_end = "}<|tool_call_end|>";
923923
form.scope_end = "<|tool_calls_section_end|>";
924924
form.raw_argval = false;
925925
form.last_val_end = "";
926+
form.allow_toolcall_in_think = true;
926927
return form;
927928
})();
928929
builder.consume_reasoning_with_xml_tool_calls(form, "<think>", "</think>");

models/templates/Kimi-K2-Instruct.jinja

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
{%- endmacro %}
1515

1616
{%- set tool_response_queue = namespace(ids=[]) -%}
17-
{%- set tool_call_counter = namespace(value=1) -%}
17+
{%- set tool_call_counter = namespace(value=0) -%}
1818

1919
{%- if tools -%}
2020
<|im_system|>tool_declare<|im_middle|>{{ tools | tojson }}<|im_end|>
@@ -36,12 +36,8 @@
3636
{%- if message['role'] == 'assistant' and message.get('tool_calls') -%}
3737
{{render_content(message)}}<|tool_calls_section_begin|>
3838
{%- for tool_call in message['tool_calls'] -%}
39-
{%- if tool_call['id'] is defined -%}
40-
{%- set formatted_id = tool_call['id'] -%}
41-
{%- else -%}
42-
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
43-
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
44-
{%- endif -%}
39+
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
40+
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
4541
{%- set _ = tool_response_queue.ids.append(formatted_id) -%}
4642
<|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|>
4743
{%- endfor -%}

models/templates/Kimi-K2-Thinking.jinja

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,13 @@
2525
{%- endmacro -%}
2626

2727
{%- set tool_response_queue = namespace(ids=[]) -%}
28-
{%- set tool_call_counter = namespace(value=1) -%}
28+
{%- set tool_call_counter = namespace(value=0) -%}
2929

3030
{%- macro render_toolcalls(message) -%}
3131
<|tool_calls_section_begin|>
3232
{%- for tool_call in message['tool_calls'] -%}
33-
{%- if tool_call['id'] is defined -%}
34-
{%- set formatted_id = tool_call['id'] -%}
35-
{%- else -%}
36-
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
37-
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
38-
{%- endif -%}
33+
{%- set formatted_id = 'functions.' + tool_call['function']['name'] + ':' + (tool_call_counter.value | string) -%}
34+
{%- set tool_call_counter.value = tool_call_counter.value + 1 -%}
3935
{%- set _ = tool_response_queue.ids.append(formatted_id) -%}
4036
<|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|>
4137
{%- endfor -%}

0 commit comments

Comments
 (0)