diff --git a/mlx_lm/tool_parsers/qwen3_coder.py b/mlx_lm/tool_parsers/qwen3_coder.py index b9a6f9ffd..522408af5 100644 --- a/mlx_lm/tool_parsers/qwen3_coder.py +++ b/mlx_lm/tool_parsers/qwen3_coder.py @@ -45,7 +45,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 35f71aa64..f55f70f48 100644 --- a/tests/test_tool_parsing.py +++ b/tests/test_tool_parsing.py @@ -240,6 +240,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_pythonic_nested_args(self): # Containers are rendered with tojson, so they hold true/false/null. test_case = (