diff --git a/llama_cpp/llama_chat_format.py b/llama_cpp/llama_chat_format.py
index 99c4bfb46..f1a6f513c 100644
--- a/llama_cpp/llama_chat_format.py
+++ b/llama_cpp/llama_chat_format.py
@@ -4421,6 +4421,231 @@ def __call__(self, **kwargs):
# Use parent implementation
return super().__call__(**kwargs)
+class Qwen35ChatHandler(Llava15ChatHandler):
+ CHAT_FORMAT = (
+ "{%- 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 'image' in item or 'image_url' in item -%}"
+ " {%- if is_system_content -%}"
+ " {{- raise_exception('System message 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|>' -}}"
+ " {%- if 'image' in item -%}"
+ " {%- if item.image is string -%}"
+ " {{- item.image -}}"
+ " {%- else -%}"
+ " {{- item.image.url -}}"
+ " {%- endif -%}"
+ " {%- elif 'image_url' in item -%}"
+ " {%- if item.image_url is string -%}"
+ " {{- item.image_url -}}"
+ " {%- else -%}"
+ " {{- item.image_url.url -}}"
+ " {%- endif -%}"
+ " {%- endif -%}"
+ " {{- '<|vision_end|>' -}}"
+ " {%- elif 'video' in item -%}"
+ " {%- if is_system_content -%}"
+ " {{- raise_exception('System message 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|>' -}}"
+ " {{- item.video -}}"
+ " {{- '<|vision_end|>' -}}"
+ " {%- elif 'text' in item -%}"
+ " {{- item.text -}}"
+ " {%- else -%}"
+ " {{- raise_exception('Unexpected item type in content.') -}}"
+ " {%- endif -%}"
+ " {%- endfor -%}"
+ " {%- elif content is none or content is undefined -%}"
+ " {{- '' -}}"
+ " {%- else -%}"
+ " {{- raise_exception('Unexpected content type.') -}}"
+ " {%- endif -%}"
+ "{%- endmacro -%}"
+ "{%- if not messages -%}"
+ " {{- raise_exception('No messages provided.') -}}"
+ "{%- endif -%}"
+ "{%- if tools and tools is iterable and tools is not mapping -%}"
+ " {{- '<|im_start|>system\n' -}}"
+ " {{- '# Tools\n\nYou have access to the following functions:\n\n' -}}"
+ " {%- for tools in tools -%}"
+ " {{- '\n' -}}"
+ " {{- tool | tojson -}}"
+ " {%- endfor -%}"
+ " {{- '\n' -}}"
+ " {{- '\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' -}}"
+ " {%- if messages[0].role == 'system' -%}"
+ " {%- set content = render_content(messages[0].content, false, true) | trim -%}"
+ " {%- if content -%}"
+ " {{- '\n\n' + content -}}"
+ " {%- endif -%}"
+ " {%- endif -%}"
+ " {{- '<|im_end|>\n' -}}"
+ "{%- elif messages[0].role == 'system' -%}"
+ " {%- set content = render_content(messages[0].content, false, true) -%}"
+ " {{- '<|im_start|>system\n' + content + '<|im_end|>\n' -}}"
+ "{%- endif -%}"
+ "{%- set ns = namespace(multi_step_tool=true, last_query_index=messages | length - 1) -%}"
+ "{%- for message in messages[::-1] -%}"
+ " {%- set index = messages | length - 1 - loop.index0 -%}"
+ " {%- if ns.multi_step_tool and message.role == 'user' -%}"
+ " {%- set content = render_content(message.content, false) | trim -%}"
+ " {%- if not (content.startswith('') and content.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 -%}"
+ "{%- for message in messages -%}"
+ " {%- set content = render_content(message.content, true) | trim -%}"
+ " {%- if message.role == 'system' -%}"
+ " {%- if not loop.first -%}"
+ " {{- raise_exception('System message must be at the beginning.') -}}"
+ " {%- endif -%}"
+ " {%- elif message.role == 'user' -%}"
+ " {{- '<|im_start|>' + message.role + '\n' + content + '<|im_end|>\n' -}}"
+ " {%- elif message.role == 'assistant' -%}"
+ " {%- set reasoning_content = '' -%}"
+ " {%- if message.reasoning_content is string -%}"
+ " {%- set reasoning_content = message.reasoning_content -%}"
+ " {%- elif '' in content -%}"
+ " {%- set reasoning_content = content.split('')[0].rstrip('\n').split('')[-1].lstrip('\n') -%}"
+ " {%- set content = content.split('')[-1].lstrip('\n') -%}"
+ " {%- endif -%}"
+ " {%- set reasoning_content = reasoning_content | trim -%}"
+ " {%- if loop.index0 > ns.last_query_index -%}"
+ " {{- '<|im_start|>' + message.role + '\n\n' + reasoning_content + '\n\n\n' + content -}}"
+ " {%- else -%}"
+ " {{- '<|im_start|>' + message.role + '\n' + content -}}"
+ " {%- endif -%}"
+ " {%- if message.tool_calls and message.tool_calls is iterable and message.tool_calls is not mapping -%}"
+ " {%- for tool_call in message.tool_call -%}"
+ " {%- if tool_call.function is defined -%}"
+ " {%- set tool_call = tool_call.function -%}"
+ " {%- endif -%}"
+ " {%- if loop.first -%}"
+ " {%- if content | trim -%}"
+ " {{- '\n\n\n\n' -}}"
+ " {%- else -%}"
+ " {{- '\n\n' -}}"
+ " {%- endif -%}"
+ " {%- else -%}"
+ " {{- '\n\n\n' -}}"
+ " {%- endif -%}"
+ " {%- if tool_call.arguments is defined -%}"
+ " {%- for (args_name, args_value) in tool_calls.arguments | items -%}"
+ " {{- '\n' -}}"
+ " {%- set args_value = args_value | tojson | safe if args_value is mapping or args_value is sequence and args_value is not string else args_value | string -%}"
+ " {{- args_value -}}"
+ " {{- '\n' -}}"
+ " {%- endfor -%}"
+ " {%- endif -%}"
+ " {{- '\n' -}}"
+ " {%- endfor -%}"
+ " {%- endif -%}"
+ " {{- '<|im_end|>\n' -}}"
+ " {%- elif message.role == 'tool' -%}"
+ " {%- if loop.previtem and loop.previtem.role != 'tool' -%}"
+ " {{- '<|im_start|>user' -}}"
+ " {%- endif -%}"
+ " {{- '\n\n' -}}"
+ " {{- content -}}"
+ " {{- '\n' -}}"
+ " {%- if not loop.last and loop.nextitem.role != 'tool' -%}"
+ " {{- '<|im_end|>\n' -}}"
+ " {%- elif loop.last -%}"
+ " {{- '<|im_end|>\n' -}}"
+ " {%- endif -%}"
+ " {%- else -%}"
+ " {{- raise_exception('Unexpected message role.') -}}"
+ " {%- endif -%}"
+ "{%- endfor -%}"
+ "{%- if add_generation_prompt -%}"
+ " {{- '<|im_start|>assistant\n' -}}"
+ " {%- if enable_thinking is defined and enable_thinking is false -%}"
+ " {{- '\n\n\n\n' -}}"
+ " {%- else -%}"
+ " {{- '\n' -}}"
+ " {%- endif -%}"
+ "{%- endif -%}"
+ )
+
+ def __init__(
+ self,
+ reasoning: bool = True,
+ force_reasoning: bool = False,
+ add_vision_id: bool = True,
+ **kwargs,
+ ):
+ """
+ Parameters:
+ - reasoning (bool):
+ - True (default): Enables reasoning for better results.
+ - False: Disables reasoning for faster results.
+ - force_reasoning (bool):
+ - True: Force the reasoning in the model by adding to the chat template.
+ - False (default): Don't force the reasoning.
+ - add_vision_id (bool):
+ - True (default): Count all the images. Recommended for multi-image.
+ - False: Doesn't count the images. Can save tokens with single-image.
+ """
+ self.reasoning = reasoning
+ self.force_reasoning = force_reasoning
+ self.add_vision_id = add_vision_id
+
+ super().__init__(image_min_tokens=self.image_min_tokens, **kwargs)
+
+ def __call__(self, **kwargs):
+ self.extra_template_arguments["enable_thinking"] = self.reasoning
+ self.extra_template_arguments["force_reasoning"] = self.force_reasoning
+ self.extra_template_arguments["add_vision_id"] = self.add_vision_id
+
+ llama = kwargs['llama']
+
+ # Clear state for multiple runs
+ llama.reset()
+ llama._ctx.memory_clear(True)
+ llama.n_tokens = 0
+
+ if hasattr(llama, 'input_ids'):
+ llama.input_ids.fill(0)
+
+ # Clear any handler state
+ if hasattr(self, '_last_image_embed'):
+ self._last_image_embed = None
+ self._last_image_hash = None
+
+ if self.verbose:
+ messages = kwargs.get('messages', [])
+ try:
+ image_count = len(self.get_image_urls(messages))
+ print(f"Qwen35ChatHandler(reasoning={self.reasoning}) - Cleared state, processing {image_count} images", file=sys.stderr)
+ except Exception:
+ print(f"Qwen35ChatHandler(reasoning={self.reasoning}) - Cleared state", file=sys.stderr)
+
+ # Use parent implementation
+ return super().__call__(**kwargs)
@register_chat_completion_handler("chatml-function-calling")
def chatml_function_calling(