diff --git a/evalharness/model/adapter.py b/evalharness/model/adapter.py index d70eb49..b4f8925 100644 --- a/evalharness/model/adapter.py +++ b/evalharness/model/adapter.py @@ -108,9 +108,25 @@ def _key_for(api_base: str) -> str: def _parse_text_tool_calls(text: str) -> list: - """Extract [{"name":..,"arguments":{..}}] JSON from a text-protocol reply.""" + """Extract tool calls from a text reply. Handles both shapes: + - JSON array: [{"name":..,"arguments":{..}}] + - Qwen3 native XML: {"name":..,"arguments":{..}} + (for vLLM builds whose qwen3_xml parser doesn't convert to tool_calls) + """ import re as _re + out = [] + for m in _re.finditer(r'\s*(\{.*?\})\s*', text, _re.S): + try: + obj = json.loads(m.group(1)) + if isinstance(obj, dict) and obj.get('name'): + out.append({'id': '', 'type': 'function', + 'function': {'name': obj['name'], + 'arguments': json.dumps(obj.get('arguments', {}))}}) + except (ValueError, TypeError): + continue + if out: + return out candidates = _re.findall(r'\[[\s\S]*?\]', text) or [] for cand in candidates: try: @@ -196,7 +212,7 @@ class OpenAICompatible(ModelAdapter): msg = choice.get('message') or {} calls = [] raw_calls = list(msg.get('tool_calls') or []) - if not raw_calls and self.extra.get('tools_mode') == 'text': + if not raw_calls: raw_calls = _parse_text_tool_calls(msg.get('content') or '') for c in raw_calls: fn = c.get('function') or {}