fix: normalize malformed <parameter> tags (Qwen3) (#10539)

This commit is contained in:
Engel Nyst 2025-08-21 19:03:20 +02:00 committed by GitHub
parent adb773789a
commit b311ae6e15
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 1 deletions

View File

@ -705,6 +705,25 @@ def _fix_stopword(content: str) -> str:
return content
def _normalize_parameter_tags(fn_body: str) -> str:
"""Normalize malformed parameter tags to the canonical format.
Some models occasionally emit malformed parameter tags like:
<parameter=command=str_replace</parameter>
instead of the correct:
<parameter=command>str_replace</parameter>
This function rewrites the malformed form into the correct one to allow
downstream parsing to succeed.
"""
# Replace '<parameter=name=value</parameter>' with '<parameter=name>value</parameter>'
return re.sub(
r'<parameter=([a-zA-Z0-9_]+)=([^<]*)</parameter>',
r'<parameter=\1>\2</parameter>',
fn_body,
)
def convert_non_fncall_messages_to_fncall_messages(
messages: list[dict],
tools: list[ChatCompletionToolParam],
@ -852,7 +871,7 @@ def convert_non_fncall_messages_to_fncall_messages(
if fn_match:
fn_name = fn_match.group(1)
fn_body = fn_match.group(2)
fn_body = _normalize_parameter_tags(fn_match.group(2))
matching_tool = next(
(
tool['function']

View File

@ -96,6 +96,52 @@ FNCALL_TOOLS: list[ChatCompletionToolParam] = [
]
def test_malformed_parameter_parsing_recovery():
"""Ensure we can recover when models emit malformed parameter tags like <parameter=command=str_replace</parameter>.
This simulates a tool call to str_replace_editor where the 'command' parameter is malformed.
"""
from openhands.llm.fn_call_converter import (
convert_non_fncall_messages_to_fncall_messages,
)
# Construct an assistant message with malformed parameter tag for 'command'
assistant_message = {
'role': 'assistant',
'content': (
'<function=str_replace_editor>\n'
'<parameter=command=str_replace</parameter>\n' # malformed form
'<parameter=path>/repo/app.py</parameter>\n'
'<parameter=old_str>foo</parameter>\n'
'<parameter=new_str>bar</parameter>\n'
'</function>'
),
}
messages = [
{'role': 'system', 'content': 'test'},
{'role': 'user', 'content': 'do edit'},
assistant_message,
]
converted = convert_non_fncall_messages_to_fncall_messages(messages, FNCALL_TOOLS)
# The last message should be assistant with a parsed tool call
last = converted[-1]
assert last['role'] == 'assistant'
assert 'tool_calls' in last and len(last['tool_calls']) == 1
tool_call = last['tool_calls'][0]
assert tool_call['type'] == 'function'
assert tool_call['function']['name'] == 'str_replace_editor'
# Arguments must be a valid JSON with command=str_replace and proper params
args = json.loads(tool_call['function']['arguments'])
assert args['command'] == 'str_replace'
assert args['path'] == '/repo/app.py'
assert args['old_str'] == 'foo'
assert args['new_str'] == 'bar'
def test_convert_tools_to_description():
formatted_tools = convert_tools_to_description(FNCALL_TOOLS)
print(formatted_tools)