{"slug": "merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric", "title": "Merged Qwen Multimodal Chat Template from allanchan339 and froggeric", "summary": "A merged Qwen multimodal chat template has been released, combining fixes from allanchan339 and froggeric to address multiple issues with Qwen model chat formatting. The template adds support for a developer role, `<|think_on|>`/`<|think_off|>` toggles, and hidden historical reasoning by default, along with JSON parsing for string-form tool arguments and non-ASCII escaping in tool/args JSON. Additional improvements include recognition of both `</thinking>` and `</think>` tags, auto-closing `<think>` before `<tool_call>`, and compact tool signatures with historical-reasoning gating.", "body_md": "| {# ========================= | |\n| Merged Qwen Multimodal Chat Template from | |\n| - https://github.com/allanchan339/vLLM-Qwen3-3.5-3.6-chat-template-fix | |\n| - https://huggingface.co/froggeric/Qwen-Fixed-Chat-Templates | |\n| Features: | |\n| - developer role supported (from froggeric) | |\n| - <|think_on|> / <|think_off|> toggles (from froggeric) | |\n| - Historical reasoning HIDDEN by default (from allanchan339) | |\n| - String-form tool arguments parsed as JSON (from allanchan339) | |\n| - Non-ASCII escaped in tools/args JSON (from froggeric) | |\n| - </thinking> recognized as well as </think> (from froggeric) | |\n| - Auto-close <think> before <tool_call> (from allanchan339) | |\n| - Same vision / tool_response structure | |\n| - Compact tool signatures (from froggeric) | |\n| - last_user_index historical-reasoning gating (from allanchan339) | |\n| ========================= #} | |\n| {# Minja-compatible hybrid Qwen template | |\n| Trade-offs vs full Jinja2 version: | |\n| - String-form tool arguments are emitted verbatim (no JSON re-parse) | |\n| - <think> auto-close uses simple replace, not positional insertion | |\n| - Reasoning extraction uses split + trim instead of lstrip/rstrip with arg | |\n| #} | |\n| {%- set image_count = namespace(value=0) -%} | |\n| {%- set video_count = namespace(value=0) -%} | |\n| {%- macro render_tool_parameter(args_name, args_value) -%} | |\n| {{- '<parameter=' ~ args_name ~ '>\\n' -}} | |\n| {%- if args_value is mapping or (args_value is iterable and args_value is not string) -%} | |\n| {{- args_value | tojson -}} | |\n| {%- else -%} | |\n| {{- args_value | string -}} | |\n| {%- endif -%} | |\n| {{- '\\n</parameter>\\n' -}} | |\n| {%- endmacro -%} | |\n| {%- macro render_tool_arguments(arguments) -%} | |\n| {%- if arguments is mapping -%} | |\n| {%- for args_name, args_value in arguments.items() -%} | |\n| {{- render_tool_parameter(args_name, args_value) -}} | |\n| {%- endfor -%} | |\n| {%- elif arguments is string -%} | |\n| {%- set trimmed = arguments | trim -%} | |\n| {%- if trimmed -%} | |\n| {{- trimmed -}} | |\n| {%- if not trimmed.endswith('\\n') -%}{{- '\\n' -}}{%- endif -%} | |\n| {%- endif -%} | |\n| {%- endif -%} | |\n| {%- endmacro -%} | |\n| {%- macro render_tool_signature(tool) -%} | |\n| {%- set fn = tool.function if tool.function is defined else tool -%} | |\n| {%- set props = {} -%} | |\n| {%- set req = [] -%} | |\n| {%- if fn.parameters is defined and fn.parameters is mapping -%} | |\n| {%- if fn.parameters.properties is defined -%} | |\n| {%- set props = fn.parameters.properties -%} | |\n| {%- endif -%} | |\n| {%- if fn.parameters.required is defined -%} | |\n| {%- set req = fn.parameters.required -%} | |\n| {%- endif -%} | |\n| {%- endif -%} | |\n| {%- set ns_p = namespace(sig='') -%} | |\n| {%- for pname in props -%} | |\n| {%- set pdef = props[pname] -%} | |\n| {%- set ptype = 'any' -%} | |\n| {%- if pdef.type is defined -%} | |\n| {%- if pdef.enum is defined and pdef.enum is iterable and pdef.enum is not string and pdef.enum is not mapping -%} | |\n| {%- set ptype = pdef.enum | join('|') -%} | |\n| {%- else -%} | |\n| {%- set ptype = pdef.type -%} | |\n| {%- endif -%} | |\n| {%- endif -%} | |\n| {%- set part = pname ~ ('' if pname in req else '?') ~ ': ' ~ ptype -%} | |\n| {%- if ns_p.sig -%} | |\n| {%- set ns_p.sig = ns_p.sig ~ ', ' ~ part -%} | |\n| {%- else -%} | |\n| {%- set ns_p.sig = part -%} | |\n| {%- endif -%} | |\n| {%- endfor -%} | |\n| {{- '\\n- ' ~ fn.name ~ '(' ~ ns_p.sig ~ ')' -}} | |\n| {%- if fn.description is defined -%} | |\n| {{- ' — ' ~ fn.description -}} | |\n| {%- endif -%} | |\n| {%- for pname in props -%} | |\n| {%- set pdef = props[pname] -%} | |\n| {%- if pdef.type is defined and (pdef.type == 'array' or pdef.type == 'object') -%} | |\n| {{- '\\n - ' ~ pname ~ ' schema: ' ~ pdef | tojson -}} | |\n| {%- endif -%} | |\n| {%- endfor -%} | |\n| {%- endmacro -%} | |\n| {%- macro render_content(content, do_vision_count, is_system_content=false) -%} | |\n| {%- if content is string -%} | |\n| {{- content -}} | |\n| {%- elif content is iterable and content is not mapping -%} | |\n| {%- for item in content -%} | |\n| {%- if 'image' in item or 'image_url' in item or item.type == 'image' -%} | |\n| {%- if is_system_content -%}{{- raise_exception('System message cannot contain images.') -}}{%- endif -%} | |\n| {%- if do_vision_count -%}{%- set image_count.value = image_count.value + 1 -%}{%- endif -%} | |\n| {%- if add_vision_id is defined and add_vision_id -%}{{- 'Picture ' ~ image_count.value ~ ': ' -}}{%- endif -%} | |\n| {{- '<|vision_start|><|image_pad|><|vision_end|>' -}} | |\n| {%- elif 'video' in item or item.type == 'video' -%} | |\n| {%- if is_system_content -%}{{- raise_exception('System message cannot contain videos.') -}}{%- endif -%} | |\n| {%- if do_vision_count -%}{%- set video_count.value = video_count.value + 1 -%}{%- endif -%} | |\n| {%- if add_vision_id is defined and add_vision_id -%}{{- 'Video ' ~ video_count.value ~ ': ' -}}{%- endif -%} | |\n| {{- '<|vision_start|><|video_pad|><|vision_end|>' -}} | |\n| {%- elif 'text' in item -%} | |\n| {{- item.text -}} | |\n| {%- endif -%} | |\n| {%- endfor -%} | |\n| {%- endif -%} | |\n| {%- endmacro -%} | |\n| {%- set ns_flags = namespace(enable_thinking=true) -%} | |\n| {%- if enable_thinking is defined -%} | |\n| {%- set ns_flags.enable_thinking = enable_thinking -%} | |\n| {%- endif -%} | |\n| {%- if not messages -%}{{- raise_exception('No messages provided.') -}}{%- endif -%} | |\n| {%- set has_system = false -%} | |\n| {%- set system_content = '' -%} | |\n| {%- if messages[0].role == 'system' or messages[0].role == 'developer' -%} | |\n| {%- set has_system = true -%} | |\n| {%- set sysc = render_content(messages[0].content, false, true) | trim -%} | |\n| {%- if '<|think_off|>' in sysc -%} | |\n| {%- set ns_flags.enable_thinking = false -%} | |\n| {%- set sysc = sysc | replace('<|think_off|>', '') -%} | |\n| {%- endif -%} | |\n| {%- if '<|think_on|>' in sysc -%} | |\n| {%- set ns_flags.enable_thinking = true -%} | |\n| {%- set sysc = sysc | replace('<|think_on|>', '') -%} | |\n| {%- endif -%} | |\n| {%- set system_content = sysc | trim -%} | |\n| {%- endif -%} | |\n| {%- if tools and tools is iterable and tools is not mapping -%} | |\n| {{- '<|im_start|>system\\n' -}} | |\n| {{- '# Tools\\n\\nYou have access to the following functions:\\n\\n<tools>' -}} | |\n| {%- for tool in tools -%} | |\n| {{- render_tool_signature(tool) -}} | |\n| {%- endfor -%} | |\n| {{- '\\n</tools>\\n\\n' -}} | |\n| {{- 'IMPORTANT: Always close any thinking with </think> BEFORE emitting <tool_call>. ' -}} | |\n| {{- 'Reasoning inside a <tool_call> or its parameters is forbidden. ' -}} | |\n| {{- 'When calling a tool, output one or more XML blocks in EXACTLY this format and nothing after the final </tool_call>:\\n\\n' -}} | |\n| {{- '<tool_call>\\n<function=example_function_name>\\n<parameter=example_parameter_1>\\nvalue_1\\n</parameter>\\n<parameter=example_parameter_2>\\nThis is the value for the second parameter\\nthat can span\\nmultiple lines\\n</parameter>\\n</function>\\n</tool_call>\\n\\n' -}} | |\n| {{- 'Rules:\\n' -}} | |\n| {{- '- Prefer a matching tool over answering from memory when applicable.\\n' -}} | |\n| {{- '- If a prior tool response contains an id and the user is modifying that object, reuse the id with the matching update_* tool.\\n' -}} | |\n| {{- '- Put any reasoning or natural language BEFORE the first <tool_call>, never after the last </tool_call>.\\n' -}} | |\n| {{- '- Include every required parameter; preserve user strings verbatim for ids, names, titles, subjects, emails, and queries.\\n' -}} | |\n| {{- '- For object/array parameter values, write valid JSON inside the parameter body.\\n' -}} | |\n| {{- '- If no tool is needed, answer normally.' -}} | |\n| {%- if has_system and system_content -%} | |\n| {{- '\\n\\n' ~ system_content -}} | |\n| {%- endif -%} | |\n| {{- '<|im_end|>\\n' -}} | |\n| {%- elif has_system and system_content -%} | |\n| {{- '<|im_start|>system\\n' ~ system_content ~ '<|im_end|>\\n' -}} | |\n| {%- endif -%} | |\n| {# Find last real user message (not a tool_response wrapper) #} | |\n| {%- set ns = namespace(last_user_index=-1) -%} | |\n| {%- for m in messages -%} | |\n| {%- if m.role == 'user' -%} | |\n| {%- set uc = render_content(m.content, false) | trim -%} | |\n| {%- if not (uc.startswith('<tool_response>') and uc.endswith('</tool_response>')) -%} | |\n| {%- set ns.last_user_index = loop.index0 -%} | |\n| {%- endif -%} | |\n| {%- endif -%} | |\n| {%- endfor -%} | |\n| {# Render messages #} | |\n| {%- for message in messages -%} | |\n| {%- set is_system = (message.role == 'system' or message.role == 'developer') -%} | |\n| {%- set content = render_content(message.content, true, is_system) | trim -%} | |\n| {%- if '<|think_off|>' in content -%} | |\n| {%- set ns_flags.enable_thinking = false -%} | |\n| {%- set content = content | replace('<|think_off|>', '') -%} | |\n| {%- endif -%} | |\n| {%- if '<|think_on|>' in content -%} | |\n| {%- set ns_flags.enable_thinking = true -%} | |\n| {%- set content = content | replace('<|think_on|>', '') -%} | |\n| {%- endif -%} | |\n| {%- set content = content | trim -%} | |\n| {%- if is_system -%} | |\n| {%- if not loop.first -%} | |\n| {{- raise_exception('System/developer message must be at the beginning.') -}} | |\n| {%- endif -%} | |\n| {%- elif message.role == 'user' -%} | |\n| {{- '<|im_start|>user\\n' ~ content ~ '<|im_end|>\\n' -}} | |\n| {%- elif message.role == 'assistant' -%} | |\n| {# Simple <think> auto-close: if there's a <tool_call> but no closing think tag, inject one #} | |\n| {%- if '<tool_call>' in content and '<think>' in content and '</think>' not in content and '</thinking>' not in content -%} | |\n| {%- set content = content | replace('<tool_call>', '</think>\\n<tool_call>', 1) -%} | |\n| {%- endif -%} | |\n| {# Extract reasoning_content (split-based, minja-safe) #} | |\n| {%- set reasoning_content = '' -%} | |\n| {%- if message.reasoning_content is defined and message.reasoning_content is string -%} | |\n| {%- set reasoning_content = message.reasoning_content -%} | |\n| {%- elif '</think>' in content -%} | |\n| {%- set _parts = content.split('</think>') -%} | |\n| {%- set _before = _parts[0] -%} | |\n| {%- if '<think>' in _before -%} | |\n| {%- set reasoning_content = _before.split('<think>')[1] | trim -%} | |\n| {%- else -%} | |\n| {%- set reasoning_content = _before | trim -%} | |\n| {%- endif -%} | |\n| {%- set content = _parts[1] | trim -%} | |\n| {%- elif '</thinking>' in content -%} | |\n| {%- set _parts = content.split('</thinking>') -%} | |\n| {%- set _before = _parts[0] -%} | |\n| {%- if '<think>' in _before -%} | |\n| {%- set reasoning_content = _before.split('<think>')[1] | trim -%} | |\n| {%- else -%} | |\n| {%- set reasoning_content = _before | trim -%} | |\n| {%- endif -%} | |\n| {%- set content = _parts[1] | trim -%} | |\n| {%- elif '<think>' in content -%} | |\n| {%- set reasoning_content = content.split('<think>')[1] | trim -%} | |\n| {%- set content = '' -%} | |\n| {%- endif -%} | |\n| {%- set reasoning_content = reasoning_content | trim -%} | |\n| {%- set content = content | trim -%} | |\n| {# Show <think> only for turns AFTER the last real user query #} | |\n| {%- set show_think = false -%} | |\n| {%- if reasoning_content -%} | |\n| {%- if preserve_thinking is defined and preserve_thinking is true -%} | |\n| {%- set show_think = true -%} | |\n| {%- elif loop.index0 > ns.last_user_index -%} | |\n| {%- set show_think = true -%} | |\n| {%- endif -%} | |\n| {%- endif -%} | |\n| {%- if show_think -%} | |\n| {{- '<|im_start|>assistant\\n<think>\\n' ~ reasoning_content ~ '\\n</think>\\n\\n' ~ content -}} | |\n| {%- else -%} | |\n| {{- '<|im_start|>assistant\\n' ~ content -}} | |\n| {%- endif -%} | |\n| {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping -%} | |\n| {%- for tool_call in message.tool_calls -%} | |\n| {%- if tool_call.function is defined -%} | |\n| {%- set tool_call = tool_call.function -%} | |\n| {%- endif -%} | |\n| {%- if loop.first -%} | |\n| {%- if content | trim -%} | |\n| {{- '\\n\\n<tool_call>\\n<function=' ~ tool_call.name ~ '>\\n' -}} | |\n| {%- else -%} | |\n| {{- '<tool_call>\\n<function=' ~ tool_call.name ~ '>\\n' -}} | |\n| {%- endif -%} | |\n| {%- else -%} | |\n| {{- '\\n<tool_call>\\n<function=' ~ tool_call.name ~ '>\\n' -}} | |\n| {%- endif -%} | |\n| {{- render_tool_arguments(tool_call.arguments) -}} | |\n| {{- '</function>\\n</tool_call>' -}} | |\n| {%- endfor -%} | |\n| {%- endif -%} | |\n| {{- '<|im_end|>\\n' -}} | |\n| {%- elif message.role == 'tool' -%} | |\n| {%- if loop.index0 == 0 or messages[loop.index0 - 1].role != 'tool' -%} | |\n| {{- '<|im_start|>user' -}} | |\n| {%- endif -%} | |\n| {{- '\\n<tool_response>\\n' ~ content ~ '\\n</tool_response>' -}} | |\n| {%- if loop.last or messages[loop.index0 + 1].role != 'tool' -%} | |\n| {{- '\\n<|im_end|>\\n' -}} | |\n| {%- endif -%} | |\n| {%- else -%} | |\n| {{- raise_exception('Unexpected message role.') -}} | |\n| {%- endif -%} | |\n| {%- endfor -%} | |\n| {%- if add_generation_prompt -%} | |\n| {{- '<|im_start|>assistant\\n' -}} | |\n| {%- if ns_flags.enable_thinking is false -%} | |\n| {{- '<think>\\n\\n</think>\\n\\n' -}} | |\n| {%- else -%} | |\n| {{- '<think>\\n' -}} | |\n| {%- endif -%} | |\n| {%- endif -%} |", "url": "https://wpnews.pro/news/merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric", "canonical_source": "https://gist.github.com/fakezeta/9e8e039c60332fcb143c6e805558afe0", "published_at": "2026-05-05 10:56:19+00:00", "updated_at": "2026-05-28 03:24:07.831471+00:00", "lang": "en", "topics": ["large-language-models", "artificial-intelligence", "ai-tools", "ai-research", "natural-language-processing"], "entities": ["allanchan339", "froggeric", "Qwen", "vLLM", "Hugging Face"], "alternates": {"html": "https://wpnews.pro/news/merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric", "markdown": "https://wpnews.pro/news/merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric.md", "text": "https://wpnews.pro/news/merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric.txt", "jsonld": "https://wpnews.pro/news/merged-qwen-multimodal-chat-template-from-allanchan339-and-froggeric.jsonld"}}