From 13a1e8d2481c26dfdd344ea06833c8a44e65bcc9 Mon Sep 17 00:00:00 2001
From: zmylol <84814703+zmylol@users.noreply.github.com>
Date: Tue, 22 Sep 2026 00:23:44 +0800
Subject: [PATCH] Fix Qwen3 Coder argument parsing when the top-level type is
missing
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This change updates Qwen3 Coder’s tool-call argument parser, which converts argument text generated by the model into Python values such as strings, dictionaries, and lists. When the top-level `type` is missing, the parser first attempts to parse the text as JSON.
- If the parsed value is an object or array, it returns a dictionary or list.
- If parsing fails, or the parsed value is a number, boolean, or string, it preserves the original text.
Only objects and arrays are converted to avoid accidentally turning text such as `"123"` into a number. The handling of explicitly specified types is unchanged, and the existing `null` handling is preserved.
---
mlx_lm/tool_parsers/qwen3_coder.py | 6 ++-
tests/test_tool_parsing.py | 83 ++++++++++++++++++++++++++++++
2 files changed, 88 insertions(+), 1 deletion(-)
diff --git a/mlx_lm/tool_parsers/qwen3_coder.py b/mlx_lm/tool_parsers/qwen3_coder.py
index 987e0fab3..cfca9de76 100644
--- a/mlx_lm/tool_parsers/qwen3_coder.py
+++ b/mlx_lm/tool_parsers/qwen3_coder.py
@@ -44,7 +44,11 @@ def _convert_param_value(param_value: str, param_name: str, param_config: dict)
if "type" in param:
param_type = str(param["type"]).strip().lower()
else:
- param_type = "string"
+ try:
+ value = json.loads(param_value)
+ except json.JSONDecodeError:
+ return param_value
+ return value if isinstance(value, (dict, list)) else param_value
if param_type in _string_types:
return param_value
elif (
diff --git a/tests/test_tool_parsing.py b/tests/test_tool_parsing.py
index 11a0eaf32..ec89ce894 100644
--- a/tests/test_tool_parsing.py
+++ b/tests/test_tool_parsing.py
@@ -212,6 +212,89 @@ def test_qwen3_coder_single_quoted_params(self):
self.assertEqual(tool_call["arguments"]["filters"], {"category": "books"})
self.assertEqual(tool_call["arguments"]["tags"], ["fiction", "new"])
+ def test_qwen3_coder_composed_param_schema(self):
+ branches = [
+ {
+ "type": "object",
+ "properties": {
+ "kind": {"const": "new"},
+ "idPrefix": {"type": "string"},
+ },
+ "required": ["kind", "idPrefix"],
+ },
+ {
+ "type": "object",
+ "properties": {
+ "kind": {"const": "existing"},
+ "pluginId": {"type": "string"},
+ },
+ "required": ["kind", "pluginId"],
+ },
+ ]
+ test_case = (
+ ""
+ '{"kind": "new", "idPrefix": "abc"}'
+ ""
+ )
+ expected = {
+ "name": "f",
+ "arguments": {"plugin": {"kind": "new", "idPrefix": "abc"}},
+ }
+
+ for keyword in ("oneOf", "anyOf"):
+ with self.subTest(keyword=keyword):
+ tools = [
+ {
+ "type": "function",
+ "function": {
+ "name": "f",
+ "parameters": {
+ "type": "object",
+ "properties": {"plugin": {keyword: branches}},
+ },
+ },
+ }
+ ]
+ tool_call = qwen3_coder.parse_tool_call(test_case, tools)
+ self.assertEqual(tool_call, expected)
+
+ def test_qwen3_coder_param_conversion(self):
+ schema = {"anyOf": [{"type": "array"}, {"type": "string"}]}
+ test_cases = [
+ (schema, '["fiction", "new"]', ["fiction", "new"]),
+ (schema, "plain text", "plain text"),
+ (schema, '["unfinished"', '["unfinished"'),
+ (schema, "123", "123"),
+ (schema, "true", "true"),
+ (schema, '"quoted"', '"quoted"'),
+ ({"enum": ["123", "true"]}, "123", "123"),
+ ({"enum": ["123", "true"]}, "true", "true"),
+ ({"type": "string"}, '{"kind": "new"}', '{"kind": "new"}'),
+ ({"type": "string"}, "123", "123"),
+ ({"type": "string"}, "true", "true"),
+ ]
+ for param_schema, value, expected in test_cases:
+ with self.subTest(schema=param_schema, value=value):
+ tools = [
+ {
+ "type": "function",
+ "function": {
+ "name": "f",
+ "parameters": {
+ "type": "object",
+ "properties": {"value": param_schema},
+ },
+ },
+ }
+ ]
+ test_case = (
+ "" f"{value}" ""
+ )
+ tool_call = qwen3_coder.parse_tool_call(test_case, tools)
+ self.assertEqual(
+ tool_call, {"name": "f", "arguments": {"value": expected}}
+ )
+
def test_gemma4(self):
# Nested object
test_case = 'call:configure{settings:{enabled:true,name:<|"|>test<|"|>}}'