@@ -281,6 +281,8 @@ def __init__(
281281 self .min_reflect = extra_settings .get ('min_reflect' , 1 )
282282 self .reasoning_steps = extra_settings .get ('reasoning_steps' , False )
283283 self .metrics = extra_settings .get ('metrics' , False )
284+ # Auto-detect XML tool format for known models, or allow manual override
285+ self .xml_tool_format = extra_settings .get ('xml_tool_format' , 'auto' )
284286
285287 # Token tracking
286288 self .last_token_metrics : Optional [TokenMetrics ] = None
@@ -359,6 +361,32 @@ def _is_ollama_provider(self) -> bool:
359361
360362 return False
361363
364+ def _is_qwen_provider (self ) -> bool :
365+ """Detect if this is a Qwen provider"""
366+ if not self .model :
367+ return False
368+
369+ # Direct qwen/ prefix or Qwen in model name
370+ model_lower = self .model .lower ()
371+ if any (pattern in model_lower for pattern in ["qwen" , "qwen2" , "qwen2.5" ]):
372+ return True
373+
374+ # OpenAI-compatible API serving Qwen models
375+ if "openai/" in self .model and any (pattern in model_lower for pattern in ["qwen" , "qwen2" , "qwen2.5" ]):
376+ return True
377+
378+ return False
379+
380+ def _supports_xml_tool_format (self ) -> bool :
381+ """Check if the model should use XML tool format"""
382+ if self .xml_tool_format == 'auto' :
383+ # Auto-detect based on known models that use XML format
384+ return self ._is_qwen_provider ()
385+ elif self .xml_tool_format is True or self .xml_tool_format == 'true' :
386+ return True
387+ else :
388+ return False
389+
362390 def _generate_ollama_tool_summary (self , tool_results : List [Any ], response_text : str ) -> Optional [str ]:
363391 """
364392 Generate a summary from tool results for Ollama to prevent infinite loops.
@@ -658,6 +686,10 @@ def _supports_streaming_tools(self) -> bool:
658686 if any (self .model .startswith (prefix ) for prefix in ["gemini-" , "gemini/" ]):
659687 return True
660688
689+ # Models with XML tool format support streaming with tools
690+ if self ._supports_xml_tool_format ():
691+ return True
692+
661693 # For other providers, default to False to be safe
662694 # This ensures we make a single non-streaming call rather than risk
663695 # missing tool calls or making duplicate calls
@@ -1427,6 +1459,41 @@ def get_response(
14271459 except (json .JSONDecodeError , KeyError ) as e :
14281460 logging .debug (f"Could not parse Ollama tool call from response: { e } " )
14291461
1462+ # Parse tool calls from XML format in response text
1463+ # Try for known XML models first, or fallback for any model that might output XML
1464+ if not tool_calls and response_text and formatted_tools :
1465+ # Check if this model is known to use XML format, or try as fallback
1466+ should_try_xml = (self ._supports_xml_tool_format () or
1467+ # Fallback: try XML if response contains XML-like tool call tags
1468+ '<tool_call>' in response_text )
1469+
1470+ if should_try_xml :
1471+ # Look for <tool_call> XML tags
1472+ tool_call_pattern = r'<tool_call>\s*({.*?})\s*</tool_call>'
1473+ matches = re .findall (tool_call_pattern , response_text , re .DOTALL )
1474+
1475+ if matches :
1476+ tool_calls = []
1477+ for idx , match in enumerate (matches ):
1478+ try :
1479+ # Parse the JSON inside the XML tag
1480+ tool_json = json .loads (match .strip ())
1481+ if isinstance (tool_json , dict ) and "name" in tool_json :
1482+ tool_calls .append ({
1483+ "id" : f"tool_{ iteration_count } _{ idx } " ,
1484+ "type" : "function" ,
1485+ "function" : {
1486+ "name" : tool_json ["name" ],
1487+ "arguments" : json .dumps (tool_json .get ("arguments" , {}))
1488+ }
1489+ })
1490+ except (json .JSONDecodeError , KeyError ) as e :
1491+ logging .debug (f"Could not parse XML tool call: { e } " )
1492+ continue
1493+
1494+ if tool_calls :
1495+ logging .debug (f"Parsed { len (tool_calls )} tool call(s) from XML format" )
1496+
14301497 # For Ollama, if response is empty but we have tools, prompt for tool usage
14311498 if self ._is_ollama_provider () and (not response_text or response_text .strip () == "" ) and formatted_tools and iteration_count == 0 :
14321499 messages .append ({
0 commit comments