From c10e31b54f3ea3076acf57c29fa7352cae81f7ee Mon Sep 17 00:00:00 2001 From: Leonccaa <166551845+Leonccaa@users.noreply.github.com> Date: Tue, 27 Oct 2026 17:17:50 -0700 Subject: [PATCH] tool parsers: keep large integer arguments exact for number parameters coerce_to_schema_type converted a `number` parameter through float() and back, so an integer that binary64 cannot represent exactly changed value without an error: 9007199254740993 became 9007199254740992. A 64-bit ID declared as `number` in a tool schema reached the tool with a different value. The qwen3_coder parser, used by the reference deployment, calls this helper for every parameter. Integers are now parsed with int() first; everything else, including decimals, exponents and the non-finite guard (inf and nan stay strings), takes the existing path. Results for integers that binary64 represents exactly are unchanged. Port of vllm-project/vllm#55743 by @Woolgathererer (runtime change and its test_utils test; the parser test that file adds does not exist in this tree). Adds a qwen3_coder test that checks non-streaming and streaming output for a 2**53 + 1 ID, a negative 63-bit value in a number-or-string union, and that 5.0 and 0.25 keep their previous results. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Leonccaa <166551845+Leonccaa@users.noreply.github.com> --- .../test_qwen3coder_tool_parser.py | 63 +++++++++++++++++++ tests/tool_parsers/test_utils.py | 9 +++ vllm/tool_parsers/utils.py | 4 ++ 3 files changed, 76 insertions(+) diff --git a/tests/tool_parsers/test_qwen3coder_tool_parser.py b/tests/tool_parsers/test_qwen3coder_tool_parser.py index dbeeb61e1d..d0a154fc37 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 3276fa9ddd..2faf88a563 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 f078fdbeb4..3788f0a2af 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):