Skip to content

Commit 98d5e8b

Browse files
authored
common/chat : fix LFM2/LFM2.5 reasoning round-trip and <think> leak (#24234)
* common/chat : fix LFM2 reasoning round-trip and stray <think> leak * Gate by reasoning format and whether the template supports <think>
1 parent 31e8249 commit 98d5e8b

3 files changed

Lines changed: 263 additions & 179 deletions

File tree

common/chat.cpp

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,8 +1625,17 @@ static common_chat_params common_chat_params_init_lfm2(const common_chat_templat
16251625
const std::string THINK_END = "</think>";
16261626
const std::string GEN_PROMPT = "<|im_start|>assistant\n";
16271627

1628-
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs);
1629-
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs);
1628+
// Copy reasoning to the "thinking" field the template expects
1629+
auto adjusted_messages = json::array();
1630+
for (auto msg : inputs.messages) {
1631+
if (msg.contains("reasoning_content") && msg.at("reasoning_content").is_string()) {
1632+
msg["thinking"] = msg.at("reasoning_content");
1633+
}
1634+
adjusted_messages.push_back(msg);
1635+
}
1636+
1637+
data.prompt = common_chat_template_direct_apply_impl(tmpl, inputs, adjusted_messages);
1638+
data.generation_prompt = common_chat_template_generation_prompt_impl(tmpl, inputs, adjusted_messages);
16301639
data.format = COMMON_CHAT_FORMAT_PEG_NATIVE;
16311640
data.supports_thinking = true;
16321641
data.preserved_tokens = { TOOL_CALL_START, TOOL_CALL_END, THINK_START, THINK_END };
@@ -1639,7 +1648,9 @@ static common_chat_params common_chat_params_init_lfm2(const common_chat_templat
16391648
data.thinking_end_tag = THINK_END;
16401649

16411650
auto has_tools = inputs.tools.is_array() && !inputs.tools.empty();
1642-
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE;
1651+
// Gate by reasoning format and whether the template supports <think>
1652+
auto extract_reasoning = inputs.reasoning_format != COMMON_REASONING_FORMAT_NONE &&
1653+
tmpl.source().find(THINK_START) != std::string::npos;
16431654
auto include_grammar = has_tools && inputs.tool_choice != COMMON_CHAT_TOOL_CHOICE_NONE;
16441655

16451656
if (inputs.has_continuation()) {
@@ -1658,7 +1669,7 @@ static common_chat_params common_chat_params_init_lfm2(const common_chat_templat
16581669
auto end = p.end();
16591670

16601671
auto reasoning = p.eps();
1661-
if (extract_reasoning && inputs.enable_thinking) {
1672+
if (extract_reasoning) {
16621673
reasoning = p.optional(THINK_START + p.reasoning(p.until(THINK_END)) + THINK_END);
16631674
}
16641675

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
{{- bos_token -}}
2+
{%- set preserve_thinking = preserve_thinking | default(false) -%}
3+
4+
{%- macro format_arg_value(arg_value) -%}
5+
{%- if arg_value is string -%}
6+
{{- "'" + arg_value + "'" -}}
7+
{%- elif arg_value is mapping -%}
8+
{{- arg_value | tojson -}}
9+
{%- else -%}
10+
{{- arg_value | string -}}
11+
{%- endif -%}
12+
{%- endmacro -%}
13+
14+
{%- macro parse_content(content) -%}
15+
{%- if content is string -%}
16+
{{- content -}}
17+
{%- else -%}
18+
{%- set _ns = namespace(result="") -%}
19+
{%- for item in content -%}
20+
{%- if item["type"] == "image" -%}
21+
{%- set _ns.result = _ns.result + "<image>" -%}
22+
{%- elif item["type"] == "text" -%}
23+
{%- set _ns.result = _ns.result + item["text"] -%}
24+
{%- else -%}
25+
{%- set _ns.result = _ns.result + item | tojson -%}
26+
{%- endif -%}
27+
{%- endfor -%}
28+
{{- _ns.result -}}
29+
{%- endif -%}
30+
{%- endmacro -%}
31+
32+
{%- macro render_tool_calls(tool_calls) -%}
33+
{%- set tool_calls_ns = namespace(tool_calls=[]) -%}
34+
{%- for tool_call in tool_calls -%}
35+
{%- set func_name = tool_call["function"]["name"] -%}
36+
{%- set func_args = tool_call["function"]["arguments"] -%}
37+
{%- set args_ns = namespace(arg_strings=[]) -%}
38+
{%- for arg_name, arg_value in func_args.items() -%}
39+
{%- set args_ns.arg_strings = args_ns.arg_strings + [arg_name + "=" + format_arg_value(arg_value)] -%}
40+
{%- endfor -%}
41+
{%- set tool_calls_ns.tool_calls = tool_calls_ns.tool_calls + [func_name + "(" + (args_ns.arg_strings | join(", ")) + ")"] -%}
42+
{%- endfor -%}
43+
{{- "<|tool_call_start|>[" + (tool_calls_ns.tool_calls | join(", ")) + "]<|tool_call_end|>" -}}
44+
{%- endmacro -%}
45+
46+
{%- set ns = namespace(system_prompt="", last_user_index=-1) -%}
47+
{%- if messages[0]["role"] == "system" -%}
48+
{%- if messages[0].get("content") -%}
49+
{%- set ns.system_prompt = parse_content(messages[0]["content"]) -%}
50+
{%- endif -%}
51+
{%- set messages = messages[1:] -%}
52+
{%- endif -%}
53+
{%- if tools -%}
54+
{%- set ns.system_prompt = ns.system_prompt + ("\n" if ns.system_prompt else "") + "List of tools: [" -%}
55+
{%- for tool in tools -%}
56+
{%- if tool is not string -%}
57+
{%- set tool = tool | tojson -%}
58+
{%- endif -%}
59+
{%- set ns.system_prompt = ns.system_prompt + tool -%}
60+
{%- if not loop.last -%}
61+
{%- set ns.system_prompt = ns.system_prompt + ", " -%}
62+
{%- endif -%}
63+
{%- endfor -%}
64+
{%- set ns.system_prompt = ns.system_prompt + "]" -%}
65+
{%- endif -%}
66+
{%- if ns.system_prompt -%}
67+
{{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}}
68+
{%- endif -%}
69+
{%- for message in messages -%}
70+
{%- if message["role"] == "user" -%}
71+
{%- set ns.last_user_index = loop.index0 -%}
72+
{%- endif -%}
73+
{%- endfor -%}
74+
{%- for message in messages -%}
75+
{{- "<|im_start|>" + message.role + "\n" -}}
76+
{%- if message.role == "assistant" -%}
77+
{%- generation -%}
78+
{%- if message.thinking is defined and (preserve_thinking or loop.index0 > ns.last_user_index) -%}
79+
{{- "<think>" + message.thinking + "</think>" -}}
80+
{%- endif -%}
81+
{%- set _cfm_tag = "CONTINUE_FINAL_MESSAGE_TAG " -%}
82+
{%- set _has_cfm = false -%}
83+
{%- if message.content is defined -%}
84+
{%- set content = parse_content(message.content) -%}
85+
{%- if not (preserve_thinking or loop.index0 > ns.last_user_index) -%}
86+
{%- if "</think>" in content -%}
87+
{%- set content = content.split("</think>")[-1] | trim -%}
88+
{%- endif -%}
89+
{%- endif -%}
90+
{%- if message.tool_calls is defined and content.endswith(_cfm_tag) -%}
91+
{%- set _has_cfm = true -%}
92+
{%- set _trunc_len = (content | length) - (_cfm_tag | length) -%}
93+
{{- content[:_trunc_len] -}}
94+
{%- else -%}
95+
{{- content -}}
96+
{%- endif -%}
97+
{%- endif -%}
98+
{%- if message.tool_calls is defined -%}
99+
{{- render_tool_calls(message.tool_calls) -}}
100+
{%- endif -%}
101+
{%- if _has_cfm -%}
102+
{{- _cfm_tag -}}
103+
{%- endif -%}
104+
{{- "<|im_end|>\n" -}}
105+
{%- endgeneration -%}
106+
{%- else %}
107+
{%- if message.get("content") -%}
108+
{{- parse_content(message["content"]) -}}
109+
{%- endif -%}
110+
{{- "<|im_end|>\n" -}}
111+
{%- endif %}
112+
{%- endfor -%}
113+
{%- if add_generation_prompt -%}
114+
{{- "<|im_start|>assistant\n" -}}
115+
{%- endif -%}

0 commit comments

Comments
 (0)