{%- set template_version = "qwen3.8-safe-v3.1" %}
{#- -------------------------------------------------------------------------
Whitespace contract: every tag opener in this file carries a leading minus,
so it strips the whitespace and newlines that precede it. All emitted text
comes from string literals, which makes the rendered prompt byte-exact and
free of incidental newlines. Comment tags need the minus just as much as
block tags do; a plain comment tag breaks the strip chain and leaks the
surrounding indentation into the prompt.
------------------------------------------------------------------------- -#}
{#- The message list has to exist before anything else can inspect it. -#}
{%- if not messages %}
{{- raise_exception('No messages provided.') }}
{%- endif %}
{#- -------------------------------------------------------------------------
Configuration
------------------------------------------------------------------------- -#}
{%- set _requested_tool_format =
tool_call_format if tool_call_format is defined else 'xml'
%}
{%- if _requested_tool_format not in ('auto', 'xml', 'json') %}
{{- raise_exception(
'Unexpected tool_call_format ' ~ _requested_tool_format ~
'. Supported values are xml (default), auto, and json.'
) }}
{%- endif %}
{%- set add_vision_id = add_vision_id if add_vision_id is defined else false %}
{%- set enable_thinking = enable_thinking if enable_thinking is defined else true %}
{%- set auto_disable_thinking_with_tools =
auto_disable_thinking_with_tools
if auto_disable_thinking_with_tools is defined
else false
%}
{%- if preserve_reasoning is defined
and preserve_reasoning is not none
and preserve_thinking is defined
and preserve_thinking is not none
and preserve_reasoning != preserve_thinking %}
{{- raise_exception(
'preserve_reasoning and preserve_thinking were both provided with conflicting values.'
) }}
{%- endif %}
{%- if preserve_reasoning is defined and preserve_reasoning is not none %}
{%- set _preserve_thinking = preserve_reasoning %}
{%- elif preserve_thinking is defined and preserve_thinking is not none %}
{%- set _preserve_thinking = preserve_thinking %}
{%- else %}
{%- set _preserve_thinking = true %}
{%- endif %}
{%- set max_tool_arg_chars =
max_tool_arg_chars if max_tool_arg_chars is defined else 0
%}
{%- set max_tool_response_chars =
max_tool_response_chars if max_tool_response_chars is defined else 0
%}
{%- if max_tool_arg_chars is not number or max_tool_arg_chars < 0 %}
{{- raise_exception('max_tool_arg_chars must be a non-negative number.') }}
{%- endif %}
{%- if max_tool_response_chars is not number or max_tool_response_chars < 0 %}
{{- raise_exception('max_tool_response_chars must be a non-negative number.') }}
{%- endif %}
{%- if tools is defined and tools is not none and tools %}
{%- if tools is string or tools is mapping or tools is not iterable %}
{{- raise_exception('tools must be an iterable list of tool definitions.') }}
{%- endif %}
{%- set _has_tools = true %}
{%- else %}
{%- set _has_tools = false %}
{%- endif %}
{#- -------------------------------------------------------------------------
Resolve the tool-call wire format.
OpenAI-compatible servers hand assistant tool calls back with
`function.arguments` as a JSON *string*, not a mapping. The XML parameter
form needs the individual key/value pairs, and a chat template cannot
decode a JSON string: the standard chat-template environment provides
`tojson` but no inverse, and no codepoint-to-character primitive, so a
hand-rolled scanner could not decode \uXXXX escapes correctly. Guessing
would silently corrupt tool arguments, which is worse than any error.
The deployment-safe default is XML, matching Qwen's native tool-call wire
format and the qwen3_coder tool parser. A caller can explicitly request
tool_call_format='auto' to render the whole prompt in JSON form whenever a
historical tool call carries string arguments. That path is lossless,
needs no decoding, and keeps the system-prompt instructions and rendered
history in one consistent format. Histories whose arguments are all
mappings still render as XML, byte-identically to tool_call_format='xml'.
A caller that uses the default XML mode (or explicitly pins 'xml') still
gets a hard error for non-empty string arguments, because that request
cannot be honoured faithfully in the native XML parameter representation.
------------------------------------------------------------------------- -#}
{%- set format_scan = namespace(has_json_string_arguments=false) %}
{%- for message in messages %}
{%- if message.role == 'assistant'
and message.tool_calls is defined
and message.tool_calls is not none
and message.tool_calls
and message.tool_calls is not string
and message.tool_calls is not mapping
and message.tool_calls is iterable %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined
and tool_call.function is not none %}
{%- set _tc = tool_call.function %}
{%- else %}
{%- set _tc = tool_call %}
{%- endif %}
{%- if _tc.arguments is defined
and _tc.arguments is not none
and _tc.arguments is string
and _tc.arguments | trim
and _tc.arguments | trim != '{}' %}
{%- set format_scan.has_json_string_arguments = true %}
{%- endif %}
{%- endfor %}
{%- endif %}
{%- endfor %}
{%- if _requested_tool_format == 'auto' %}
{%- if format_scan.has_json_string_arguments %}
{%- set _tool_format = 'json' %}
{%- else %}
{%- set _tool_format = 'xml' %}
{%- endif %}
{%- else %}
{%- set _tool_format = _requested_tool_format %}
{%- endif %}
{%- set _thinking_enabled = enable_thinking %}
{%- if auto_disable_thinking_with_tools and _has_tools %}
{%- set _thinking_enabled = false %}
{%- endif %}
{#- Normalize and validate reasoning effort even when thinking is disabled.
Explicit null follows the template default; OpenAI-style `minimal` maps to
low, while `none` disables thinking for this render. -#}
{%- if reasoning_effort is undefined or reasoning_effort is none %}
{%- set _effort_raw = 'medium' %}
{%- else %}
{%- set _effort_raw = reasoning_effort %}
{%- endif %}
{%- if _effort_raw == 'none' %}
{%- set _thinking_enabled = false %}
{%- set _reasoning_effort = 'xhigh' %}
{%- elif _effort_raw == 'high' or _effort_raw == 'xhigh' %}
{%- set _reasoning_effort = 'xhigh' %}
{%- elif _effort_raw == 'medium' %}
{%- set _reasoning_effort = 'medium' %}
{%- elif _effort_raw == 'minimal' or _effort_raw == 'low' %}
{%- set _reasoning_effort = 'low' %}
{%- else %}
{{- raise_exception(
'Unexpected reasoning effort ' ~ _effort_raw ~
'. Supported values are none, minimal/low, medium, and high/xhigh; null uses the medium default.'
) }}
{%- endif %}
{%- set reasoning_instructions = '' %}
{%- if _thinking_enabled %}
{%- if _reasoning_effort == 'xhigh' %}
{%- set reasoning_instructions =
'Reasoning effort is set to xhigh. Please think carefully through the task, validate key assumptions, consider plausible alternatives, and prioritize correctness, consistency, and clarity in the final answer.'
%}
{%- elif _reasoning_effort == 'low' %}
{%- set reasoning_instructions =
'Reasoning effort is set to low. Keep your thinking brief and focused, moving directly to the conclusion without unnecessary elaboration.'
%}
{%- endif %}
{%- endif %}
{#- -------------------------------------------------------------------------
Content rendering
------------------------------------------------------------------------- -#}
{%- set image_count = namespace(value=0) %}
{%- set video_count = namespace(value=0) %}
{%- macro render_content(content, do_vision_count, is_system_content=false) %}
{%- if content is string %}
{{- content }}
{%- elif content is iterable and content is not mapping %}
{%- for item in content %}
{%- if item is not mapping %}
{{- raise_exception(
'Content-list items must be mappings with a supported text, image, or video type.'
) }}
{%- endif %}
{%- set _item_type = item.type if 'type' in item else none %}
{%- if _item_type is not none
and _item_type not in ('text', 'image', 'image_url', 'video') %}
{{- raise_exception(
'Unexpected content item type ' ~ _item_type ~ '.'
) }}
{%- endif %}
{%- set _is_image =
_item_type == 'image'
or _item_type == 'image_url'
or 'image' in item
or 'image_url' in item
%}
{%- set _is_video =
_item_type == 'video'
or 'video' in item
%}
{%- if _is_image and _is_video %}
{{- raise_exception(
'A content item cannot contain both image and video data.'
) }}
{%- elif _item_type == 'text' and (_is_image or _is_video) %}
{{- raise_exception(
'A text content item cannot also contain image or video data.'
) }}
{%- elif _is_image %}
{%- if is_system_content %}
{{- raise_exception(
'System and developer messages cannot contain images.'
) }}
{%- endif %}
{%- if do_vision_count %}
{%- set image_count.value = image_count.value + 1 %}
{%- endif %}
{%- if add_vision_id %}
{{- 'Picture ' ~ image_count.value ~ ': ' }}
{%- endif %}
{{- '<|vision_start|><|image_pad|><|vision_end|>' }}
{%- elif _is_video %}
{%- if is_system_content %}
{{- raise_exception(
'System and developer messages cannot contain videos.'
) }}
{%- endif %}
{%- if do_vision_count %}
{%- set video_count.value = video_count.value + 1 %}
{%- endif %}
{%- if add_vision_id %}
{{- 'Video ' ~ video_count.value ~ ': ' }}
{%- endif %}
{{- '<|vision_start|><|video_pad|><|vision_end|>' }}
{%- elif _item_type == 'text' or 'text' in item %}
{%- if 'text' not in item or item.text is not string %}
{{- raise_exception(
'Text content items must contain a string text field.'
) }}
{%- endif %}
{{- item.text }}
{%- else %}
{{- raise_exception(
'Unexpected item in content list. Expected text, image, or video content.'
) }}
{%- endif %}
{%- endfor %}
{%- elif content is none or content is undefined %}
{{- '' }}
{%- else %}
{{- raise_exception('Unexpected content type.') }}
{%- endif %}
{%- endmacro %}
{#- -------------------------------------------------------------------------
Validate and merge leading system/developer messages
------------------------------------------------------------------------- -#}
{%- set head = namespace(count=0, seen_non_system=false) %}
{%- for message in messages %}
{%- set _is_system =
message.role == 'system' or message.role == 'developer'
%}
{%- if _is_system %}
{%- if head.seen_non_system %}
{{- raise_exception(
'System and developer messages must appear before all user, assistant, and tool messages.'
) }}
{%- endif %}
{%- set head.count = head.count + 1 %}
{%- else %}
{%- set head.seen_non_system = true %}
{%- endif %}
{%- endfor %}
{%- set system_state = namespace(content='') %}
{%- for message in messages[:head.count] %}
{%- set _part =
render_content(message.content, false, true) | trim
%}
{%- if _part %}
{%- if system_state.content %}
{%- set system_state.content =
system_state.content ~ '\n\n' ~ _part
%}
{%- else %}
{%- set system_state.content = _part %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- set _system_content = system_state.content %}
{%- set _msgs = messages[head.count:] %}
{#- -------------------------------------------------------------------------
System prompt and tool instructions
------------------------------------------------------------------------- -#}
{%- if _has_tools %}
{{- '<|im_start|>system\n' }}
{%- if reasoning_instructions %}
{{- reasoning_instructions ~ '\n\n' }}
{%- endif %}
{{- '# Tools\n\nYou have access to the following functions:\n\n' }}
{%- for tool in tools %}
{{- '\n' }}
{{- tool | tojson }}
{%- endfor %}
{{- '\n' }}
{%- if _tool_format == 'json' %}
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n\n{"name": "example_function_name", "arguments": {"example_parameter_1": "value_1", "example_parameter_2": "This is the value for the second parameter"}}\n\n\n\nReminder:\n- Function calls MUST follow the specified format: a single JSON object with "name" and "arguments" keys must be nested within XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n' }}
{%- else %}
{{- '\n\nIf you choose to call a function ONLY reply in the following format with NO suffix:\n\n\n\n\nvalue_1\n\n\nThis is the value for the second parameter\nthat can span\nmultiple lines\n\n\n\n\n\nReminder:\n- Function calls MUST follow the specified format: an inner block must be nested within XML tags\n- Required parameters MUST be specified\n- You may provide optional reasoning for your function call in natural language BEFORE the function call, but NOT after\n- If there is no function call available, answer the question like normal with your current knowledge and do not tell the user about function calls\n' }}
{%- endif %}
{%- if _system_content %}
{{- '\n\n' ~ _system_content }}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- else %}
{%- if _system_content %}
{{- '<|im_start|>system\n'
~ (reasoning_instructions ~ '\n\n' if reasoning_instructions else '')
~ _system_content
~ '<|im_end|>\n'
}}
{%- elif reasoning_instructions %}
{{- '<|im_start|>system\n'
~ reasoning_instructions
~ '<|im_end|>\n'
}}
{%- endif %}
{%- endif %}
{#- -------------------------------------------------------------------------
Find the last real user query
------------------------------------------------------------------------- -#}
{%- set ns = namespace(
multi_step_tool=true,
last_query_index=(_msgs | length) - 1
) %}
{%- for message in _msgs[::-1] %}
{%- set index = (_msgs | length - 1) - loop.index0 %}
{%- if ns.multi_step_tool and message.role == 'user' %}
{%- set _rendered_user =
render_content(message.content, false) | trim
%}
{%- if not (
_rendered_user.startswith('')
and _rendered_user.endswith('')
) %}
{%- set ns.multi_step_tool = false %}
{%- set ns.last_query_index = index %}
{%- endif %}
{%- endif %}
{%- endfor %}
{%- if ns.multi_step_tool %}
{{- raise_exception('No user query found in messages.') }}
{%- endif %}
{#- -------------------------------------------------------------------------
Conversation rendering
------------------------------------------------------------------------- -#}
{%- set history_state = namespace(previous_role='') %}
{%- for message in _msgs %}
{%- set content =
render_content(message.content, true) | trim
%}
{%- if message.role == 'user' %}
{{- '<|im_start|>user\n'
~ content
~ '<|im_end|>\n'
}}
{%- elif message.role == 'assistant' %}
{%- set reasoning_content = '' %}
{#- Prefer current vLLM's `reasoning` field, then retain compatibility
with older `reasoning_content` and other servers' `thinking`. -#}
{%- if message.reasoning is defined
and message.reasoning is not none %}
{%- if message.reasoning is string %}
{%- set reasoning_content = message.reasoning %}
{%- else %}
{%- set reasoning_content =
message.reasoning | string
%}
{%- endif %}
{%- elif message.reasoning_content is defined
and message.reasoning_content is not none %}
{%- if message.reasoning_content is string %}
{%- set reasoning_content = message.reasoning_content %}
{%- else %}
{%- set reasoning_content =
message.reasoning_content | string
%}
{%- endif %}
{%- elif message.thinking is defined
and message.thinking is not none %}
{%- if message.thinking is string %}
{%- set reasoning_content = message.thinking %}
{%- else %}
{%- set reasoning_content =
message.thinking | string
%}
{%- endif %}
{%- endif %}
{%- set reasoning_content = reasoning_content | trim %}
{#- Preserve the original invariant: when thinking is preserved,
always reconstruct the thinking block, even when it is empty. -#}
{%- if _preserve_thinking
or loop.index0 > ns.last_query_index %}
{{- '<|im_start|>assistant\n\n'
~ reasoning_content
~ '\n\n\n'
~ content
}}
{%- else %}
{{- '<|im_start|>assistant\n' ~ content }}
{%- endif %}
{%- if message.tool_calls is defined
and message.tool_calls is not none
and message.tool_calls %}
{%- if message.tool_calls is string
or message.tool_calls is mapping
or message.tool_calls is not iterable %}
{{- raise_exception(
'assistant.tool_calls must be an iterable list of tool calls.'
) }}
{%- endif %}
{%- for tool_call in message.tool_calls %}
{%- if tool_call.function is defined
and tool_call.function is not none %}
{%- set tc = tool_call.function %}
{%- else %}
{%- set tc = tool_call %}
{%- endif %}
{%- if tc.name is not defined
or tc.name is none
or tc.name is not string
or not tc.name %}
{{- raise_exception(
'Every tool call must have a non-empty string name.'
) }}
{%- endif %}
{%- set tc_name = tc.name %}
{%- if _tool_format == 'json' %}
{%- if loop.first %}
{%- if content | trim %}
{{- '\n\n\n' }}
{%- else %}
{{- '\n' }}
{%- endif %}
{%- else %}
{{- '\n\n' }}
{%- endif %}
{%- set _json_arguments = '{}' %}
{%- if tc.arguments is defined
and tc.arguments is not none %}
{%- if tc.arguments is mapping %}
{%- set _json_arguments =
tc.arguments | tojson
%}
{%- elif tc.arguments is string %}
{%- set _raw_arguments =
tc.arguments | trim
%}
{%- if _raw_arguments %}
{%- if not (
_raw_arguments.startswith('{')
and _raw_arguments.endswith('}')
) %}
{{- raise_exception(
'JSON tool-call arguments must be a JSON object string.'
) }}
{%- endif %}
{%- set _json_arguments =
_raw_arguments
%}
{%- endif %}
{%- else %}
{{- raise_exception(
'JSON tool-call arguments must be a mapping or a JSON object string.'
) }}
{%- endif %}
{%- endif %}
{{- '{"name": ' }}
{{- tc_name | tojson }}
{{- ', "arguments": ' }}
{{- _json_arguments }}
{{- '}\n' }}
{%- else %}
{%- if loop.first %}
{%- if content | trim %}
{{- '\n\n\n\n'
}}
{%- else %}
{{- '\n\n'
}}
{%- endif %}
{%- else %}
{{- '\n\n\n'
}}
{%- endif %}
{%- if tc.arguments is defined
and tc.arguments is not none %}
{%- if tc.arguments is mapping %}
{%- for args_name, args_value
in tc.arguments | items %}
{%- set _argument_name =
args_name | string
%}
{{- '\n'
}}
{#- Preserve strings verbatim. Serialize every
other type as JSON so booleans and null stay
true/false/null instead of True/False/None. -#}
{%- if args_value is string %}
{%- set _argument_value =
args_value
%}
{%- else %}
{%- set _argument_value =
args_value | tojson
%}
{%- endif %}
{%- if max_tool_arg_chars > 0
and _argument_value | length
> max_tool_arg_chars %}
{{- _argument_value[:max_tool_arg_chars] }}
{{- '\n[TRUNCATED — original length '
~ (_argument_value | length | string)
~ ' chars]'
}}
{%- else %}
{{- _argument_value }}
{%- endif %}
{{- '\n\n' }}
{%- endfor %}
{%- elif tc.arguments is string %}
{%- set _raw_arguments =
tc.arguments | trim
%}
{%- if _raw_arguments
and _raw_arguments != '{}' %}
{{- raise_exception(
'tool_call_format="xml" requires tool-call arguments to be mappings, but received a JSON string. Parse the JSON string into a mapping before applying the template, or set tool_call_format="auto" (or "json") to use the lossless JSON tool-call representation.'
) }}
{%- endif %}
{%- else %}
{{- raise_exception(
'XML tool-call arguments must be a mapping. Parse JSON-string arguments before applying the template.'
) }}
{%- endif %}
{%- endif %}
{{- '\n' }}
{%- endif %}
{%- endfor %}
{%- endif %}
{{- '<|im_end|>\n' }}
{%- elif message.role == 'tool' %}
{%- if history_state.previous_role != 'tool' %}
{{- '<|im_start|>user' }}
{%- endif %}
{%- if max_tool_response_chars > 0
and content | length > max_tool_response_chars %}
{%- set content =
content[:max_tool_response_chars]
~ '\n[TRUNCATED — original length '
~ (content | length | string)
~ ' chars]'
%}
{%- endif %}
{{- '\n\n' }}
{{- content }}
{{- '\n' }}
{%- if loop.last %}
{{- '<|im_end|>\n' }}
{%- elif _msgs[loop.index0 + 1].role != 'tool' %}
{{- '<|im_end|>\n' }}
{%- endif %}
{%- elif message.role == 'system'
or message.role == 'developer' %}
{{- raise_exception(
'System and developer messages must appear only at the beginning of the conversation.'
) }}
{%- else %}
{{- raise_exception(
'Unexpected message role ' ~ message.role ~ '.'
) }}
{%- endif %}
{%- set history_state.previous_role = message.role %}
{%- endfor %}
{#- -------------------------------------------------------------------------
Generation prompt
------------------------------------------------------------------------- -#}
{%- if add_generation_prompt %}
{{- '<|im_start|>assistant\n' }}
{%- if not _thinking_enabled %}
{{- '\n\n\n\n' }}
{%- else %}
{{- '\n' }}
{%- endif %}
{%- endif %}