From c408fdd663afb34ab82a10b26f553bec9e8052d9 Mon Sep 17 00:00:00 2001 From: FredericOdermatt <50372080+FredericOdermatt@users.noreply.github.com> Date: Sat, 2 May 2026 05:06:54 +0200 Subject: [PATCH] [Fix] Sync gemma4 chat template from hf (#39570) Signed-off-by: Frederic Odermatt --- examples/tool_chat_template_gemma4.jinja | 111 ++++++++++++++--------- 1 file changed, 67 insertions(+), 44 deletions(-) diff --git a/examples/tool_chat_template_gemma4.jinja b/examples/tool_chat_template_gemma4.jinja index 15c5238ac33..f62ca843a40 100644 --- a/examples/tool_chat_template_gemma4.jinja +++ b/examples/tool_chat_template_gemma4.jinja @@ -1,9 +1,9 @@ -{%- macro format_parameters(properties, required) -%} +{%- macro format_parameters(properties, required, filter_keys=false) -%} {%- set standard_keys = ['description', 'type', 'properties', 'required', 'nullable'] -%} {%- set ns = namespace(found_first=false) -%} {%- for key, value in properties | dictsort -%} {%- set add_comma = false -%} - {%- if key not in standard_keys -%} + {%- if not filter_keys or key not in standard_keys -%} {%- if ns.found_first %},{% endif -%} {%- set ns.found_first = true -%} {{ key }}:{ @@ -11,34 +11,15 @@ description:<|"|>{{ value['description'] }}<|"|> {%- set add_comma = true -%} {%- endif -%} - {%- if value['nullable'] %} - {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} - nullable:true - {%- endif -%} {%- if value['type'] | upper == 'STRING' -%} {%- if value['enum'] -%} {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} enum:{{ format_argument(value['enum']) }} {%- endif -%} - {%- elif value['type'] | upper == 'OBJECT' -%} - ,properties:{ - {%- if value['properties'] is defined and value['properties'] is mapping -%} - {{- format_parameters(value['properties'], value['required'] | default([])) -}} - {%- elif value is mapping -%} - {{- format_parameters(value, value['required'] | default([])) -}} - {%- endif -%} - } - {%- if value['required'] -%} - ,required:[ - {%- for item in value['required'] | default([]) -%} - <|"|>{{- item -}}<|"|> - {%- if not loop.last %},{% endif -%} - {%- endfor -%} - ] - {%- endif -%} {%- elif value['type'] | upper == 'ARRAY' -%} {%- if value['items'] is mapping and value['items'] -%} - ,items:{ + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + items:{ {%- set ns_items = namespace(found_first=false) -%} {%- for item_key, item_value in value['items'] | dictsort -%} {%- if item_value is not none -%} @@ -71,6 +52,32 @@ } {%- endif -%} {%- endif -%} + {%- if value['nullable'] %} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + nullable:true + {%- endif -%} + {%- if value['type'] | upper == 'OBJECT' -%} + {%- if value['properties'] is defined and value['properties'] is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value['properties'], value['required'] | default([])) -}} + } + {%- elif value is mapping -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + properties:{ + {{- format_parameters(value, value['required'] | default([]), filter_keys=true) -}} + } + {%- endif -%} + {%- if value['required'] -%} + {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} + required:[ + {%- for item in value['required'] | default([]) -%} + <|"|>{{- item -}}<|"|> + {%- if not loop.last %},{% endif -%} + {%- endfor -%} + ] + {%- endif -%} + {%- endif -%} {%- if add_comma %},{%- else -%} {%- set add_comma = true -%} {% endif -%} type:<|"|>{{ value['type'] | upper }}<|"|>} {%- endif -%} @@ -167,20 +174,25 @@ {%- set ns = namespace(prev_message_type=None) -%} {%- set loop_messages = messages -%} -{{ bos_token }} +{{- bos_token -}} +{#- Handle System/Tool Definitions Block -#} {%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%} {{- '<|turn>system\n' -}} - + {#- Inject Thinking token at the very top of the FIRST system turn -#} {%- if enable_thinking is defined and enable_thinking -%} - {{- '<|think|>' -}} + {{- '<|think|>\n' -}} {%- set ns.prev_message_type = 'think' -%} {%- endif -%} - {%- if messages[0]['role'] in ['system', 'developer'] -%} - {{- messages[0]['content'] | trim -}} + {%- if messages[0]['content'] is string -%} + {{- messages[0]['content'] | trim -}} + {%- elif messages[0]['content'] is sequence -%} + {%- for item in messages[0]['content'] -%} + {{- item['text'] | trim + ' '-}} + {%- endfor -%} + {%- endif -%} {%- set loop_messages = messages[1:] -%} {%- endif -%} - {%- if tools -%} {%- for tool in tools %} {{- '<|tool>' -}} @@ -189,10 +201,10 @@ {%- endfor %} {%- set ns.prev_message_type = 'tool' -%} {%- endif -%} - {{- '\n' -}} {%- endif %} +{#- Pre-scan: find last user message index for reasoning guard -#} {%- set ns_turn = namespace(last_user_idx=-1) -%} {%- for i in range(loop_messages | length) -%} {%- if loop_messages[i]['role'] == 'user' -%} @@ -200,12 +212,12 @@ {%- endif -%} {%- endfor -%} +{#- Loop through messages -#} {%- for message in loop_messages -%} {%- if message['role'] != 'tool' -%} {%- set ns.prev_message_type = None -%} {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%} - {#- OpenAI may emit multiple assistant messages in one tool loop (user → asst → tool → asst → tool). - Only the first of those should open <|turn>model; later ones continue the same model turn. -#} + {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#} {%- set prev_nt = namespace(role=None, found=false) -%} {%- if loop.index0 > 0 -%} {%- for j in range(loop.index0 - 1, -1, -1) -%} @@ -222,8 +234,10 @@ {{- '<|turn>' + role + '\n' }} {%- endif -%} - {%- if message.get('reasoning') and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%} - {{- '<|channel>thought\n' + message['reasoning'] + '\n'}} + {#- Render reasoning/reasoning_content as thinking channel -#} + {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%} + {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%} + {{- '<|channel>thought\n' + thinking_text + '\n' -}} {%- endif -%} {%- if message['tool_calls'] -%} @@ -247,14 +261,14 @@ {%- set ns_tr_out = namespace(flag=false) -%} {%- if message.get('tool_responses') -%} - {#- Legacy: tool_responses embedded on the assistant message -#} + {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#} {%- for tool_response in message['tool_responses'] -%} {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}} {%- set ns_tr_out.flag = true -%} {%- set ns.prev_message_type = 'tool_response' -%} {%- endfor -%} {%- elif message.get('tool_calls') -%} - {#- OpenAI Chat Completions: consecutive following messages with role "tool" (no break/continue; range scan) -#} + {#- OpenAI Chat Completions: forward-scan consecutive role:tool messages -#} {%- set ns_tool_scan = namespace(stopped=false) -%} {%- for k in range(loop.index0 + 1, loop_messages | length) -%} {%- if ns_tool_scan.stopped -%} @@ -262,12 +276,14 @@ {%- set ns_tool_scan.stopped = true -%} {%- else -%} {%- set follow = loop_messages[k] -%} + {#- Resolve tool_call_id to function name -#} {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%} {%- for tc in message['tool_calls'] -%} {%- if tc.get('id') == follow.get('tool_call_id') -%} {%- set ns_tname.name = tc['function']['name'] -%} {%- endif -%} {%- endfor -%} + {#- Handle content as string or content-parts array -#} {%- set tool_body = follow.get('content') -%} {%- if tool_body is string -%} {{- format_tool_response_block(ns_tname.name, tool_body) -}} @@ -288,6 +304,7 @@ {%- endfor -%} {%- endif -%} + {%- set captured_content -%} {%- if message['content'] is string -%} {%- if role == 'model' -%} {{- strip_thinking(message['content']) -}} @@ -303,29 +320,35 @@ {{- item['text'] | trim -}} {%- endif -%} {%- elif item['type'] == 'image' -%} - {{- '\n\n<|image|>\n\n' -}} + {{- '<|image|>' -}} {%- set ns.prev_message_type = 'image' -%} {%- elif item['type'] == 'audio' -%} {{- '<|audio|>' -}} {%- set ns.prev_message_type = 'audio' -%} {%- elif item['type'] == 'video' -%} - {{- '\n\n<|video|>\n\n' -}} + {{- '<|video|>' -}} {%- set ns.prev_message_type = 'video' -%} {%- endif -%} {%- endfor -%} {%- endif -%} + {%- endset -%} - {%- if not (ns_tr_out.flag and not message.get('content')) -%} + {{- captured_content -}} + {%- set has_content = captured_content | trim | length > 0 -%} + + {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%} + {{- '<|tool_response>' -}} + {%- elif not (ns_tr_out.flag and not has_content) -%} {{- '\n' -}} {%- endif -%} {%- endif -%} {%- endfor -%} {%- if add_generation_prompt -%} - {%- if ns.prev_message_type != 'tool_response' -%} + {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%} {{- '<|turn>model\n' -}} + {%- if not enable_thinking | default(false) -%} + {{- '<|channel>thought\n' -}} + {%- endif -%} {%- endif -%} - {%- if not enable_thinking | default(false) -%} - {{- '<|channel>thought\n' -}} - {%- endif -%} -{%- endif -%} +{%- endif -%} \ No newline at end of file