@@ -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