LFM2.5-1.2B-Instruct-Template-Fix / chat_template.jinja
kth8's picture
Update chat_template.jinja
bf6399b verified
{{- bos_token -}}
{#-
Fixed chat template for LiquidAI/LFM2.5-1.2B-Instruct.
Framework-agnostic — works with HuggingFace transformers apply_chat_template(),
vLLM, TGI, and any Jinja2-based rendering pipeline.
Bugs fixed in the upstream HF template:
1. content=None on assistant messages renders "null" (via tojson) instead of empty
2. tool_calls field on assistant messages is completely ignored — multi-turn tool
conversations lose all tool call context
3. content=None on tool messages renders "null" instead of empty
Tool call reconstruction: when assistant messages carry a tool_calls field (OpenAI
format), this template reconstructs them into LFM's native format:
<|tool_call_start|>[func_name(param="value")]<|tool_call_end|>
Handles function.arguments as either a Python dict (pre-parsed) or a JSON string
(OpenAI wire format). Dict arguments get full pythonic formatting; JSON strings
are embedded as-is (best effort — no from_json filter in standard Jinja2).
Options:
keep_past_thinking (default: false) — preserve <think> blocks in non-final assistant turns
Ref: https://docs.liquid.ai/lfm/key-concepts/tool-use
Ref: https://huggingface.co/LiquidAI/LFM2.5-1.2B-Instruct/blob/main/chat_template.jinja
-#}
{%- set keep_past_thinking = keep_past_thinking | default(false) -%}
{#- Extract system prompt from first message if present -#}
{%- set ns = namespace(system_prompt="") -%}
{%- if messages[0]["role"] == "system" -%}
{%- set ns.system_prompt = messages[0]["content"] -%}
{%- set messages = messages[1:] -%}
{%- endif -%}
{#- Append tool definitions to system prompt -#}
{%- if tools -%}
{%- set ns.system_prompt = ns.system_prompt + ("\n" if ns.system_prompt else "") + "List of tools: [" -%}
{%- for tool in tools -%}
{%- if tool is not string -%}
{%- set tool = tool | tojson -%}
{%- endif -%}
{%- set ns.system_prompt = ns.system_prompt + tool -%}
{%- if not loop.last -%}
{%- set ns.system_prompt = ns.system_prompt + ", " -%}
{%- endif -%}
{%- endfor -%}
{%- set ns.system_prompt = ns.system_prompt + "]" -%}
{%- endif -%}
{%- if ns.system_prompt -%}
{{- "<|im_start|>system\n" + ns.system_prompt + "<|im_end|>\n" -}}
{%- endif -%}
{#- Find last assistant index for think-stripping logic -#}
{%- set ns.last_assistant_index = -1 -%}
{%- for message in messages -%}
{%- if message["role"] == "assistant" -%}
{%- set ns.last_assistant_index = loop.index0 -%}
{%- endif -%}
{%- endfor -%}
{#- Macro: format a Python value in the style LFM was trained on -#}
{#- Strings → double quotes (via tojson), bools → True/False, None → None -#}
{%- macro pyval(v) -%}
{%- if v is none -%}None
{%- elif v is boolean and v -%}True
{%- elif v is boolean and not v -%}False
{%- elif v is string -%}{{ v | tojson }}
{%- elif v is mapping -%}{
{%- for mk, mv in v.items() -%}
{{ mk | tojson }}: {{ pyval(mv) }}
{%- if not loop.last -%}, {% endif -%}
{%- endfor -%}}
{%- elif v is iterable -%}[
{%- for item in v -%}
{{ pyval(item) }}
{%- if not loop.last -%}, {% endif -%}
{%- endfor -%}]
{%- else -%}{{ v }}
{%- endif -%}
{%- endmacro -%}
{#- Render each message -#}
{%- for message in messages -%}
{{- "<|im_start|>" + message["role"] + "\n" -}}
{%- if message["role"] == "assistant" -%}
{#- --- ASSISTANT MESSAGE --- -#}
{#- Get text content, treating None as empty -#}
{%- set text_content = message["content"] if "content" in message and message["content"] is string else "" -%}
{#- Reconstruct tool_calls into native format if present -#}
{%- set tc = message.get("tool_calls", none) if message.get is defined else message["tool_calls"] if "tool_calls" in message else none -%}
{%- if tc -%}
{%- set ns.tc_parts = [] -%}
{%- for call in tc -%}
{%- if call is mapping -%}
{%- set func = call["function"] -%}
{%- else -%}
{%- set func = call.function -%}
{%- endif -%}
{%- if func is mapping -%}
{%- set fname = func["name"] -%}
{%- set fargs = func.get("arguments", {}) if func.get is defined else func["arguments"] if "arguments" in func else {} -%}
{%- else -%}
{%- set fname = func.name -%}
{%- set fargs = func.arguments if func.arguments is defined else {} -%}
{%- endif -%}
{#- Arguments can be a dict (pre-parsed) or a JSON string (wire format) -#}
{%- if fargs is mapping -%}
{#- Dict: format as pythonic key=value pairs -#}
{%- set ns.kv_parts = [] -%}
{%- for k, v in fargs.items() -%}
{%- set ns.kv_parts = ns.kv_parts + [k + "=" + pyval(v)] -%}
{%- endfor -%}
{%- set ns.tc_parts = ns.tc_parts + [fname + "(" + ns.kv_parts | join(", ") + ")"] -%}
{%- elif fargs is string -%}
{#- JSON string: embed as-is (no from_json in standard Jinja2) -#}
{%- set ns.tc_parts = ns.tc_parts + [fname + "(" + fargs + ")"] -%}
{%- else -%}
{%- set ns.tc_parts = ns.tc_parts + [fname + "()"] -%}
{%- endif -%}
{%- endfor -%}
{%- set tool_call_str = "<|tool_call_start|>[" + ns.tc_parts | join(", ") + "]<|tool_call_end|>" -%}
{%- else -%}
{%- set tool_call_str = "" -%}
{%- endif -%}
{#- Combine: text content BEFORE tool call markers -#}
{#- This ordering is safe for think-stripping: -#}
{#- <think>...</think>text<|tool_call_start|>...<|tool_call_end|> -#}
{#- split("</think>")[-1] → text + markers preserved -#}
{%- if text_content and tool_call_str -%}
{%- set content = text_content + tool_call_str -%}
{%- elif tool_call_str -%}
{%- set content = tool_call_str -%}
{%- else -%}
{%- set content = text_content -%}
{%- endif -%}
{#- Strip thinking from non-final assistant messages -#}
{%- if not keep_past_thinking and loop.index0 != ns.last_assistant_index -%}
{%- if "</think>" in content -%}
{%- set content = content.split("</think>")[-1] | trim -%}
{%- endif -%}
{%- endif -%}
{{- content -}}
{%- elif message["role"] == "tool" -%}
{#- --- TOOL RESULT MESSAGE --- -#}
{#- Handle content=None gracefully (render empty instead of "null") -#}
{%- set content = message["content"] if "content" in message else "" -%}
{%- if content is none -%}
{%- set content = "" -%}
{%- endif -%}
{%- if content is not string -%}
{%- set content = content | tojson -%}
{%- endif -%}
{{- content -}}
{%- else -%}
{#- --- USER / SYSTEM / OTHER MESSAGES --- -#}
{%- set content = message["content"] -%}
{%- if content is not string -%}
{%- set content = content | tojson -%}
{%- endif -%}
{{- content -}}
{%- endif -%}
{{- "<|im_end|>\n" -}}
{%- endfor -%}
{%- if add_generation_prompt -%}
{{- "<|im_start|>assistant\n" -}}
{%- endif -%}