Skip to content

Commit a641283

Browse files
committed
patch(Gemma4ChatHandler): Synchronize huggingface gemma4 latest chat template
- fix: chat template — null handling, reasoning preservation, turn-tag balance, input validation - https://huggingface.co/google/gemma-4-31B-it/commit/68abe48010cbe15293462fa11e901a60639a44e5 Signed-off-by: JamePeng <jame_peng@sina.com>
1 parent 3df6144 commit a641283

1 file changed

Lines changed: 91 additions & 51 deletions

File tree

llama_cpp/llama_multimodal.py

Lines changed: 91 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -2582,7 +2582,9 @@ class Gemma4ChatHandler(MTMDChatHandler):
25822582
" }\n"
25832583
"{%- endmacro -%}\n"
25842584
"{%- macro format_argument(argument, escape_keys=True) -%}\n"
2585-
" {%- if argument is string -%}\n"
2585+
" {%- if argument is none -%}\n"
2586+
" {{- 'null' -}}\n"
2587+
" {%- elif argument is string -%}\n"
25862588
" {{- '<|\"|>' + argument + '<|\"|>' -}}\n"
25872589
" {%- elif argument is boolean -%}\n"
25882590
" {{- 'true' if argument else 'false' -}}\n"
@@ -2638,18 +2640,21 @@ class Gemma4ChatHandler(MTMDChatHandler):
26382640
" {{- '<tool_response|>' -}}\n"
26392641
"{%- endmacro -%}\n"
26402642
"\n"
2643+
"{#- ===== SETUP ===== -#}"
26412644
"{%- set ns = namespace(prev_message_type=None) -%}\n"
26422645
"{%- set loop_messages = messages -%}\n"
2646+
"{%- set enable_thinking = enable_thinking | default(false) -%}\n"
2647+
"{%- set preserve_thinking = preserve_thinking | default(false) -%}\n"
26432648
"{{- bos_token -}}\n"
26442649
"{#- Handle System/Tool Definitions Block -#}\n"
2645-
"{%- if (enable_thinking is defined and enable_thinking) or tools or messages[0]['role'] in ['system', 'developer'] -%}\n"
2650+
"{%- if enable_thinking or tools or (messages and messages[0]['role'] in ['system', 'developer']) -%}\n"
26462651
" {{- '<|turn>system\\n' -}}\n"
26472652
" {#- Inject Thinking token at the very top of the FIRST system turn -#}\n"
2648-
" {%- if enable_thinking is defined and enable_thinking -%}\n"
2653+
" {%- if enable_thinking -%}\n"
26492654
" {{- '<|think|>\\n' -}}\n"
26502655
" {%- set ns.prev_message_type = 'think' -%}\n"
26512656
" {%- endif -%}\n"
2652-
" {%- if messages[0]['role'] in ['system', 'developer'] -%}\n"
2657+
" {%- if messages and messages[0]['role'] in ['system', 'developer'] -%}\n"
26532658
" {%- if messages[0]['content'] is string -%}\n"
26542659
" {{- messages[0]['content'] | trim -}}\n"
26552660
" {%- elif messages[0]['content'] is sequence -%}\n"
@@ -2683,31 +2688,21 @@ class Gemma4ChatHandler(MTMDChatHandler):
26832688
" {%- if message['role'] != 'tool' -%}\n"
26842689
" {%- set ns.prev_message_type = None -%}\n"
26852690
" {%- set role = 'model' if message['role'] == 'assistant' else message['role'] -%}\n"
2686-
" {#- Detect continuation: suppress duplicate <|turn>model when previous non-tool message was also assistant -#}\n"
2687-
" {%- set prev_nt = namespace(role=None, found=false) -%}\n"
2688-
" {%- if loop.index0 > 0 -%}\n"
2689-
" {%- for j in range(loop.index0 - 1, -1, -1) -%}\n"
2690-
" {%- if not prev_nt.found -%}\n"
2691-
" {%- if loop_messages[j]['role'] != 'tool' -%}\n"
2692-
" {%- set prev_nt.role = loop_messages[j]['role'] -%}\n"
2693-
" {%- set prev_nt.found = true -%}\n"
2694-
" {%- endif -%}\n"
2695-
" {%- endif -%}\n"
2696-
" {%- endfor -%}\n"
2697-
" {%- endif -%}\n"
2698-
" {%- set continue_same_model_turn = (role == 'model' and prev_nt.role == 'assistant') -%}\n"
2691+
"{#- Detect continuation using tracked state — O(1) instead of O(n) backward scan -#}\n"
2692+
"{%- set continue_same_model_turn = (role == 'model' and ns.prev_non_tool_role == 'assistant') -%}\n"
26992693
" {%- if not continue_same_model_turn -%}\n"
27002694
" {{- '<|turn>' + role + '\\n' }}\n"
27012695
" {%- endif -%}\n"
27022696
"\n"
27032697
" {#- Render reasoning/reasoning_content as thinking channel -#}\n"
27042698
" {%- set thinking_text = message.get('reasoning') or message.get('reasoning_content') -%}\n"
2705-
" {%- if thinking_text and loop.index0 > ns_turn.last_user_idx and message.get('tool_calls') -%}\n"
2706-
" {{- '<|channel>thought\\n' + thinking_text + '\\n<channel|>' -}}\n"
2699+
" {%- set thinking_gate = (loop.index0 > ns_turn.last_user_idx) or (preserve_thinking and message.get('tool_calls')) -%}\n"
2700+
" {%- if thinking_text and thinking_gate -%}\n"
2701+
" {{- '<|channel>thought\n' + thinking_text + '\n<channel|>' -}}\n"
27072702
" {%- endif -%}\n"
27082703
"\n"
27092704
" {%- if message.get('tool_calls') -%}\n"
2710-
" {%- for tool_call in message['tool_calls'] -%}\n"
2705+
" {%- for tool_call in message.get('tool_calls') -%}\n"
27112706
" {%- set function = tool_call['function'] -%}\n"
27122707
" {{- '<|tool_call>call:' + function['name'] + '{' -}}\n"
27132708
" {%- if function['arguments'] is mapping -%}\n"
@@ -2717,8 +2712,13 @@ class Gemma4ChatHandler(MTMDChatHandler):
27172712
" {%- set ns_args.found_first = true -%}\n"
27182713
" {{- key -}}:{{- format_argument(value, escape_keys=False) -}}\n"
27192714
" {%- endfor -%}\n"
2720-
" {%- elif function['arguments'] is string -%}\n"
2721-
" {{- function['arguments'] -}}\n"
2715+
" {%- elif function['arguments'] is none -%}\n"
2716+
" {%- else -%}\n"
2717+
" {{- raise_exception(\n"
2718+
" \"chat_template: tool_calls[].function.arguments must be a \"\n"
2719+
" \"JSON object (mapping), not a string. Deserialize arguments \"\n"
2720+
" \"before passing to the template.\"\n"
2721+
" ) -}}\n"
27222722
" {%- endif -%}\n"
27232723
" {{- '}<tool_call|>' -}}\n"
27242724
" {%- endfor -%}\n"
@@ -2728,8 +2728,8 @@ class Gemma4ChatHandler(MTMDChatHandler):
27282728
" {%- set ns_tr_out = namespace(flag=false) -%}\n"
27292729
" {%- if message.get('tool_responses') -%}\n"
27302730
" {#- Legacy: tool_responses embedded on the assistant message (Google/Gemma native) -#}\n"
2731-
" {%- for tool_response in message['tool_responses'] -%}\n"
2732-
" {{- format_tool_response_block(tool_response['name'] | default('unknown'), tool_response['response']) -}}\n"
2731+
" {%- for tool_response in message.get('tool_responses') -%}\n"
2732+
" {{- format_tool_response_block(tool_response['name'] | default('unknown', true), tool_response['response']) -}}\n"
27332733
" {%- set ns_tr_out.flag = true -%}\n"
27342734
" {%- set ns.prev_message_type = 'tool_response' -%}\n"
27352735
" {%- endfor -%}\n"
@@ -2743,8 +2743,8 @@ class Gemma4ChatHandler(MTMDChatHandler):
27432743
" {%- else -%}\n"
27442744
" {%- set follow = loop_messages[k] -%}\n"
27452745
" {#- Resolve tool_call_id to function name -#}\n"
2746-
" {%- set ns_tname = namespace(name=follow.get('name') | default('unknown')) -%}\n"
2747-
" {%- for tc in message['tool_calls'] -%}\n"
2746+
" {%- set ns_tname = namespace(name=follow.get('name') or 'unknown') -%}\n"
2747+
" {%- for tc in message.get('tool_calls') -%}\n"
27482748
" {%- if tc.get('id') == follow.get('tool_call_id') -%}\n"
27492749
" {%- set ns_tname.name = tc['function']['name'] -%}\n"
27502750
" {%- endif -%}\n"
@@ -2762,9 +2762,14 @@ class Gemma4ChatHandler(MTMDChatHandler):
27622762
" {%- endfor -%}\n"
27632763
" {{- format_tool_response_block(ns_tname.name, ns_txt.s) -}}\n"
27642764
" {%- for part in tool_body -%}\n"
2765-
" {%- if part.get('type') == 'image_url' -%}\n"
2766-
" {%- set url_val = part['image_url'] if part['image_url'] is string else part['image_url']['url'] -%}\n"
2767-
" {{- '<|image|>' + url_val -}}\n"
2765+
" {%- if part.get('type') in ['image', 'image_url'] -%}\n"
2766+
" {%- if part.get('type') == 'image_url' -%}\n"
2767+
" {%- set url_val = part['image_url'] if part['image_url'] is string else part['image_url']['url'] -%}\n"
2768+
" {{- '<|image|>' + url_val -}}\n"
2769+
" {%- elif part.get('type') == 'image' -%}\n"
2770+
" {%- set url_val = part['image'] if part['image'] is string else part['image']['url'] -%}\n"
2771+
" {{- '<|image|>' + url_val -}}\n"
2772+
" {%- endif -%}\n"
27682773
" {%- elif part.get('type') in ['audio_url', 'input_audio'] -%}\n"
27692774
" {%- if part.get('type') == 'audio_url' -%}\n"
27702775
" {%- set audio_val = part['audio_url'] if part['audio_url'] is string else part['audio_url']['url'] -%}\n"
@@ -2773,9 +2778,14 @@ class Gemma4ChatHandler(MTMDChatHandler):
27732778
" {%- set audio_val = part['input_audio'] if part['input_audio'] is string else ('data:audio/' + part['input_audio']['format'] + ';base64,' + part['input_audio']['data']) -%}\n"
27742779
" {{- '<|audio|>' + audio_val -}}\n"
27752780
" {%- endif -%}\n"
2776-
# " {%- elif part.get('type') == 'video_url' -%}\n"
2777-
# " {%- set video_val = part['video_url'] if part['video_url'] is string else part['video_url']['url'] -%}\n"
2778-
# " {{- '<|video|>' + video_val -}}\n"
2781+
" {%- elif part.get('type') in ['video', 'video_url'] -%}\n"
2782+
" {%- if part.get('type') == 'video_url' -%}\n"
2783+
" {%- set video_val = part['video_url'] if part['video_url'] is string else part['video_url']['url'] -%}\n"
2784+
" {{- '<|video|>' + video_val -}}\n"
2785+
" {%- elif part.get('type') == 'video' -%}\n"
2786+
" {%- set video_val = part['video'] if part['video'] is string else part['video']['url'] -%}\n"
2787+
" {{- '<|video|>' + video_val -}}\n"
2788+
" {%- endif -%}\n"
27792789
" {%- endif -%}\n"
27802790
" {%- endfor -%}\n"
27812791
" {%- else -%}\n"
@@ -2788,59 +2798,89 @@ class Gemma4ChatHandler(MTMDChatHandler):
27882798
" {%- endif -%}\n"
27892799
"\n"
27902800
" {%- set captured_content -%}\n"
2791-
" {%- if message['content'] is string -%}\n"
2801+
" {%- if message.get('content') is string -%}\n"
27922802
" {%- if role == 'model' -%}\n"
27932803
" {{- strip_thinking(message['content']) -}}\n"
27942804
" {%- else -%}\n"
27952805
" {{- message['content'] | trim -}}\n"
27962806
" {%- endif -%}\n"
2797-
" {%- elif message['content'] is sequence -%}\n"
2807+
" {%- elif message.get('content') is sequence -%}\n"
27982808
" {%- for item in message['content'] -%}\n"
2799-
" {%- if item['type'] == 'text' -%}\n"
2809+
" {%- if item.get('type') == 'text' -%}\n"
28002810
" {%- if role == 'model' -%}\n"
28012811
" {{- strip_thinking(item['text']) -}}\n"
28022812
" {%- else -%}\n"
28032813
" {{- item['text'] | trim -}}\n"
28042814
" {%- endif -%}\n"
2805-
" {%- elif item['type'] == 'image_url' -%}\n"
2806-
" {%- set url_val = item['image_url'] if item['image_url'] is string else item['image_url']['url'] -%}\n"
2807-
" {{- '<|image|>' + url_val -}}\n"
2808-
" {%- set ns.prev_message_type = 'image' -%}\n"
2809-
" {%- elif item['type'] in ['audio_url', 'input_audio'] -%}\n"
2810-
" {%- if item['type'] == 'audio_url' -%}\n"
2815+
" {%- elif item.get('type') in ['image', 'image_url'] -%}\n"
2816+
" {%- if item.get('type')== 'image_url' -%}\n"
2817+
" {%- set url_val = item['image_url'] if item['image_url'] is string else item['image_url']['url'] -%}\n"
2818+
" {{- '<|image|>' + url_val -}}\n"
2819+
" {%- elif item.get('type') == 'image' -%}\n"
2820+
" {%- set url_val = item['image'] if item['image'] is string else item['image']['url'] -%}\n"
2821+
" {{- '<|image|>' + url_val -}}\n"
2822+
" {%- endif -%}\n"
2823+
" {%- elif item.get('type') in ['audio_url', 'input_audio'] -%}\n"
2824+
" {%- if item.get('type') == 'audio_url' -%}\n"
28112825
" {%- set audio_val = item['audio_url'] if item['audio_url'] is string else item['audio_url']['url'] -%}\n"
28122826
" {{- '<|audio|>' + audio_val -}}\n"
2813-
" {%- elif item['type'] == 'input_audio' -%}\n"
2827+
" {%- elif item.get('type') == 'input_audio' -%}\n"
28142828
" {%- set audio_val = item['input_audio'] if item['input_audio'] is string else ('data:audio/' + item['input_audio']['format'] + ';base64,' + item['input_audio']['data']) -%}\n"
28152829
" {{- '<|audio|>' + audio_val -}}\n"
28162830
" {%- endif -%}\n"
2817-
" {%- set ns.prev_message_type = 'audio' -%}\n"
2831+
" {%- elif item.get('type') in ['video', 'video_url'] -%}\n"
2832+
" {%- if item.get('type') == 'video_url' -%}\n"
2833+
" {%- set video_val = part['video_url'] if part['video_url'] is string else part['video_url']['url'] -%}\n"
2834+
" {{- '<|video|>' + video_val -}}\n"
2835+
" {%- elif item.get('type') == 'video' -%}\n"
2836+
" {%- set video_val = part['video'] if part['video'] is string else part['video']['url'] -%}\n"
2837+
" {{- '<|video|>' + video_val -}}\n"
2838+
" {%- endif -%}\n"
28182839
" {%- endif -%}\n"
2819-
# " {%- elif item['type'] == 'video_url' -%}\n"
2820-
# " {%- set video_val = item['video_url'] if item['video_url'] is string else item['video_url']['url'] -%}\n"
2821-
# " {{- '<|video|>' + video_val -}}\n"
2822-
# " {%- set ns.prev_message_type = 'video' -%}\n"
28232840
" {%- endfor -%}\n"
28242841
" {%- endif -%}\n"
28252842
" {%- endset -%}\n"
28262843
"\n"
28272844
" {{- captured_content -}}\n"
28282845
" {%- set has_content = captured_content | trim | length > 0 -%}\n"
28292846
"\n"
2847+
" {#- Forward-scan: find next non-tool message role for continuation detection -#}\n"
2848+
" {%- set next_nt = namespace(role=None, found=false) -%}\n"
2849+
" {%- for j in range(loop.index0 + 1, loop_messages | length) -%}\n"
2850+
" {%- if not next_nt.found -%}\n"
2851+
" {%- if loop_messages[j]['role'] != 'tool' -%}\n"
2852+
" {%- set next_nt.role = loop_messages[j]['role'] -%}\n"
2853+
" {%- set next_nt.found = true -%}\n"
2854+
" {%- endif -%}\n"
2855+
" {%- endif -%}\n"
2856+
" {%- endfor -%}\n"
2857+
2858+
" {%- set continues_into_next = (\n"
2859+
" role == 'model'\n"
2860+
" and next_nt.role == 'assistant'\n"
2861+
" and (not message.get('tool_calls') or ns_tr_out.flag)\n"
2862+
" ) -%}\n"
2863+
"\n"
28302864
" {%- if ns.prev_message_type == 'tool_call' and not ns_tr_out.flag -%}\n"
28312865
" {{- '<|tool_response>' -}}\n"
2832-
" {%- elif not (ns_tr_out.flag and not has_content) -%}\n"
2866+
" {%- elif continues_into_next -%}\n"
2867+
" {%- elif not (ns_tr_out.flag and not has_content and not next_nt.found) -%}\n"
28332868
" {{- '<turn|>\\n' -}}\n"
28342869
" {%- endif -%}\n"
2870+
"\n"
2871+
" {#- Track previous non-tool role for next iteration (avoids O(n) backward scan) -#}\n"
2872+
" {%- set ns.prev_non_tool_role = message['role'] -%}\n"
28352873
" {%- endif -%}\n"
28362874
"{%- endfor -%}\n"
28372875
"\n"
28382876
"{%- if add_generation_prompt -%}\n"
28392877
" {%- if ns.prev_message_type != 'tool_response' and ns.prev_message_type != 'tool_call' -%}\n"
2840-
" {{- '<|turn>model\\n' -}}\n"
2841-
" {%- if not enable_thinking | default(false) -%}\n"
2842-
" {{- '<|channel>thought\\n<channel|>' -}}\n"
2878+
" {{- '<|turn>model\n' -}}\n"
2879+
" {%- if not enable_thinking -%}\n"
2880+
" {{- '<|channel>thought\n<channel|>' -}}\n"
28432881
" {%- endif -%}\n"
2882+
" {%- elif ns.prev_message_type == 'tool_response' and enable_thinking -%}\n"
2883+
" {{- '<|channel>thought\n' -}}\n"
28442884
" {%- endif -%}\n"
28452885
"{%- endif -%}\n"
28462886
)

0 commit comments

Comments
 (0)