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<|"|>}}'