feat: Normalize alternative tool call formats (invoke/function_calls)
This commit is contained in:
+36
-7
@@ -3,6 +3,29 @@ import re
|
||||
from typing import Any
|
||||
|
||||
|
||||
_INVOKE_OPEN = re.compile(r'<invoke\s+name="([^"]+)"\s*>')
|
||||
_PARAM_NAME_ATTR = re.compile(r'<parameter\s+name="([^"]+)"\s*>')
|
||||
_FUNCTION_CALLS_TAG = re.compile(r"</?function_calls>")
|
||||
|
||||
|
||||
def normalize_tool_format(content: str) -> str:
|
||||
"""Convert alternative tool-call XML format to the expected one.
|
||||
|
||||
Handles:
|
||||
<function_calls>...</function_calls> → stripped
|
||||
<invoke name="X"> → <function=X>
|
||||
<parameter name="X"> → <parameter=X>
|
||||
</invoke> → </function>
|
||||
"""
|
||||
if "<invoke" not in content and "<function_calls" not in content:
|
||||
return content
|
||||
|
||||
content = _FUNCTION_CALLS_TAG.sub("", content)
|
||||
content = _INVOKE_OPEN.sub(r"<function=\1>", content)
|
||||
content = _PARAM_NAME_ATTR.sub(r"<parameter=\1>", content)
|
||||
return content.replace("</invoke>", "</function>")
|
||||
|
||||
|
||||
STRIX_MODEL_MAP: dict[str, str] = {
|
||||
"claude-sonnet-4.6": "anthropic/claude-sonnet-4-6",
|
||||
"claude-opus-4.6": "anthropic/claude-opus-4-6",
|
||||
@@ -41,7 +64,9 @@ def _truncate_to_first_function(content: str) -> str:
|
||||
if not content:
|
||||
return content
|
||||
|
||||
function_starts = [match.start() for match in re.finditer(r"<function=", content)]
|
||||
function_starts = [
|
||||
match.start() for match in re.finditer(r"<function=|<invoke\s+name=", content)
|
||||
]
|
||||
|
||||
if len(function_starts) >= 2:
|
||||
second_function_start = function_starts[1]
|
||||
@@ -52,6 +77,7 @@ def _truncate_to_first_function(content: str) -> str:
|
||||
|
||||
|
||||
def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
||||
content = normalize_tool_format(content)
|
||||
content = fix_incomplete_tool_call(content)
|
||||
|
||||
tool_invocations: list[dict[str, Any]] = []
|
||||
@@ -81,12 +107,14 @@ def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None:
|
||||
|
||||
|
||||
def fix_incomplete_tool_call(content: str) -> str:
|
||||
"""Fix incomplete tool calls by adding missing </function> tag."""
|
||||
if (
|
||||
"<function=" in content
|
||||
and content.count("<function=") == 1
|
||||
and "</function>" not in content
|
||||
):
|
||||
"""Fix incomplete tool calls by adding missing closing tag.
|
||||
|
||||
Handles both ``<function=…>`` and ``<invoke name="…">`` formats.
|
||||
"""
|
||||
has_open = "<function=" in content or "<invoke " in content
|
||||
count_open = content.count("<function=") + content.count("<invoke ")
|
||||
has_close = "</function>" in content or "</invoke>" in content
|
||||
if has_open and count_open == 1 and not has_close:
|
||||
content = content.rstrip()
|
||||
content = content + "function>" if content.endswith("</") else content + "\n</function>"
|
||||
return content
|
||||
@@ -107,6 +135,7 @@ def clean_content(content: str) -> str:
|
||||
if not content:
|
||||
return ""
|
||||
|
||||
content = normalize_tool_format(content)
|
||||
content = fix_incomplete_tool_call(content)
|
||||
|
||||
tool_pattern = r"<function=[^>]+>.*?</function>"
|
||||
|
||||
Reference in New Issue
Block a user