Add 'anyOf' schema support for qwen3_coder tool call parser (#30832)

Co-authored-by: Xinyuan Tong <115166877+JustinTong0323@users.noreply.github.com>
Co-authored-by: Liangsheng Yin <hnyls2002@gmail.com>
Co-authored-by: Alex Nails <alex.nails@radixark.ai>
This commit is contained in:
ilyasher-harmonic
2026-07-22 14:14:47 -07:00
committed by GitHub
co-authored by Xinyuan Tong Liangsheng Yin Alex Nails
parent e7511141ea
commit c20c48b8fd
3 changed files with 159 additions and 7 deletions
@@ -2209,6 +2209,27 @@ class TestQwen3CoderDetector(unittest.TestCase):
},
),
),
Tool(
type="function",
function=Function(
name="get_current_time",
parameters={
"type": "object",
"properties": {
"cities": {
"anyOf": [
{
"type": "array",
"items": {"type": "string"},
},
{"type": "null"},
],
"default": None,
}
},
},
),
),
]
self.detector = Qwen3CoderDetector()
@@ -2413,6 +2434,75 @@ class TestQwen3CoderDetector(unittest.TestCase):
self.assertEqual(params["todos"][0]["content"], "Buy groceries")
self.assertEqual(params["todos"][1]["status"], "completed")
def test_anyof_array_parameter_conversion(self):
"""
Test array parameter conversion for nullable anyOf schemas.
Scenario: A Pydantic-style nullable list schema is represented by anyOf.
Purpose: Verify array values are parsed as arrays, not JSON-looking strings.
"""
text = """<tool_call>
<function=get_current_time>
<parameter=cities>
["NYC"]
</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(text, self.tools)
params = json.loads(result.calls[0].parameters)
self.assertIsInstance(params["cities"], list)
self.assertEqual(params["cities"], ["NYC"])
def test_anyof_array_parameter_conversion_null(self):
"""
Test 'null' is converted correctly for nullable anyOf schemas.
Scenario: A Pydantic-style nullable list schema is represented by anyOf.
Purpose: Verify null values are parsed as 'None', not as strings.
"""
text = """<tool_call>
<function=get_current_time>
<parameter=cities>
null
</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(text, self.tools)
params = json.loads(result.calls[0].parameters)
self.assertEqual(params["cities"], None)
def test_streaming_anyof_array_parameter_conversion(self):
"""
Test streaming array parameter conversion for nullable anyOf schemas.
Scenario: A Pydantic-style nullable list schema is streamed in Qwen3 Coder format.
Purpose: Verify the streamed JSON fragments encode an array value, not a string value.
"""
chunks = [
"<tool_call>",
"<function=get_current_time>",
"<parameter=cities>",
'["NYC"]',
"</parameter>",
"</function>",
"</tool_call>",
]
detector = Qwen3CoderDetector()
collected_params = ""
for chunk in chunks:
result = detector.parse_streaming_increment(chunk, self.tools)
for call in result.calls:
if call.parameters:
collected_params += call.parameters
params = json.loads(collected_params)
self.assertIsInstance(params["cities"], list)
self.assertEqual(params["cities"], ["NYC"])
# ==================== Edge Cases ====================
def test_empty_parameter_value(self):
@@ -2466,6 +2556,63 @@ class TestQwen3CoderDetector(unittest.TestCase):
result = self.detector.detect_and_parse(text, self.tools)
self.assertIsInstance(result, StreamingParseResult)
def test_nested_anyof_array_with_multiple_types_parameter_conversion(self):
"""
Test several edge cases of parameter conversion for nullable anyOf schemas.
1) Test nested anyOf 'T | None' extracts 'T' correctly.
2) Test that order of null and non-null type doesn't affect schema parsing.
3) Test that list of multiple types (including dict) is parsed correctly.
"""
tool = Tool(
type="function",
function=Function(
name="process_optional_list",
parameters={
"type": "object",
"properties": {
"optional_items_to_process": {
"anyOf": [
{
"anyOf": [
# Note: here "null" is listed before the non-null type.
{"type": "null"},
{
"anyOf": [
{
"type": "array",
"items": {},
},
{"type": "null"},
],
},
],
},
{"type": "null"},
],
"default": None,
}
},
},
),
)
text = """<tool_call>
<function=process_optional_list>
<parameter=optional_items_to_process>
[true, null, {"enabled": false}]
</parameter>
</function>
</tool_call>"""
result = self.detector.detect_and_parse(text, [tool])
params = json.loads(result.calls[0].parameters)
self.assertIsInstance(params["optional_items_to_process"], list)
self.assertEqual(
params["optional_items_to_process"], [True, None, {"enabled": False}]
)
# ==================== Structural tag (xgrammar builtin) ====================
# Qwen3 Coder uses the new builtin structural tag path. supports_structural_tag()
# is True so required/named tool_choice routes through FunctionCallParser