diff --git a/tests/tool_parsers/test_qwen3coder_tool_parser.py b/tests/tool_parsers/test_qwen3coder_tool_parser.py index dbeeb61e1..d0a154fc3 100644 --- a/tests/tool_parsers/test_qwen3coder_tool_parser.py +++ b/tests/tool_parsers/test_qwen3coder_tool_parser.py @@ -691,6 +691,69 @@ def test_extract_tool_calls_type_conversion(qwen3_tokenizer): assert args["obj_param"] == {"key": "value"} +def test_large_integer_number_arguments_preserved(qwen3_tokenizer): + """Integers beyond 2**53 in `number` parameters keep their exact value. + + Converting through float changed e.g. 9007199254740993 to 9007199254740992 + without an error (vllm-project/vllm#55743), so a 64-bit ID declared as + `number` reached the tool with a different value. + """ + tools = [ + ChatCompletionToolsParam( + type="function", + function={ + "name": "get_order", + "parameters": { + "type": "object", + "properties": { + "order_id": {"type": "number"}, + "account_id": {"type": ["number", "string"]}, + "offset": {"type": "number"}, + "ratio": {"type": "number"}, + }, + }, + }, + ) + ] + model_output = """ + + +9007199254740993 + + +-9223372036854775807 + + +5.0 + + +0.25 + + +""" + expected = { + "order_id": 2**53 + 1, + "account_id": -(2**63 - 1), + "offset": 5, + "ratio": 0.25, + } + request = ChatCompletionRequest(model=MODEL, messages=[], tools=tools) + + parser = Qwen3CoderToolParser(qwen3_tokenizer, tools=tools) + extracted = parser.extract_tool_calls(model_output, request=request) + assert json.loads(extracted.tool_calls[0].function.arguments) == expected + + parser = Qwen3CoderToolParser(qwen3_tokenizer, tools=tools) + arguments = "" + for delta_message in stream_delta_message_generator( + parser, qwen3_tokenizer, model_output, request + ): + for tool_call in delta_message.tool_calls or []: + if tool_call.function and tool_call.function.arguments is not None: + arguments += tool_call.function.arguments + assert json.loads(arguments) == expected + + def test_extract_tool_calls_anyof_type_conversion(qwen3_tokenizer): """Test type conversion for anyOf/oneOf nullable schemas (Pydantic v2). diff --git a/tests/tool_parsers/test_utils.py b/tests/tool_parsers/test_utils.py index 3276fa9dd..2faf88a56 100644 --- a/tests/tool_parsers/test_utils.py +++ b/tests/tool_parsers/test_utils.py @@ -90,6 +90,15 @@ def test_whole_float_returns_int(self): assert coerce_to_schema_type("5.0", "number") == 5 assert isinstance(coerce_to_schema_type("5.0", "number"), int) + @pytest.mark.parametrize("value", [2**53 + 1, -(2**53 + 1), 2**63 - 1]) + @pytest.mark.parametrize( + "schema_type", ["number", "double", ["number", "string"]] + ) + def test_large_integer_preserved(self, value, schema_type): + result = coerce_to_schema_type(str(value), schema_type) + assert isinstance(result, int) + assert json.loads(json.dumps(result)) == value + def test_invalid_number_fallback(self): assert coerce_to_schema_type("abc", "number") == "abc" diff --git a/vllm/tool_parsers/utils.py b/vllm/tool_parsers/utils.py index f078fdbeb..3788f0a2a 100644 --- a/vllm/tool_parsers/utils.py +++ b/vllm/tool_parsers/utils.py @@ -594,6 +594,10 @@ def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any: except (ValueError, TypeError): continue if candidate_type == "number": + try: + return int(value) + except (ValueError, TypeError): + pass try: val = float(value) except (ValueError, TypeError):