[Bugfix][Tool Parser] Handle non-finite numbers in coerce_to_schema_type (#43984)
Signed-off-by: ashishpatel26 <shriganesh.patel@gmail.com> Co-authored-by: Ben Browning <bbrownin@redhat.com>
Ashish Patel committed
Jun 18, 2026 at 21:30 UTC
837db7605e240202c43577cfa4da65f3c8f506fb
2 files changed
+102
-3
tests/tool_parsers/test_utils.py
+67
@@ -1,6 +1,8 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+import json
5
+
6
import pytest
7
8
from vllm.tool_parsers.utils import (
@@ -91,6 +93,71 @@ class TestCoerceToSchemaType:
93
def test_invalid_number_fallback(self):
94
assert coerce_to_schema_type("abc", "number") == "abc"
95
96
+ class TestNonFiniteNumbers:
97
+ """Non-finite numeric strings must not crash and must coerce to a
98
+ JSON-serializable value.
99
+
100
+ Regression: ``int(float("inf"))`` raised an uncaught ``OverflowError``
101
+ (only ``ValueError``/``TypeError`` were handled), and ``"1e999"``
102
+ round-tripped through ``json.loads`` to a float ``inf`` that
103
+ ``json.dumps`` renders as invalid JSON ``Infinity``.
104
+ """
105
+
106
+ @pytest.mark.parametrize(
107
+ "value", ["inf", "-inf", "Infinity", "1e999", "nan", "-nan"]
108
+ )
109
+ def test_non_finite_number_does_not_crash(self, value):
110
+ # Must not raise (previously OverflowError for inf/1e999/Infinity).
111
+ result = coerce_to_schema_type(value, "number")
112
+ # Result must serialize to valid, finite JSON and round-trip.
113
+ assert json.loads(json.dumps(result)) == result
114
+
115
+ @pytest.mark.parametrize("value", ["inf", "-inf", "1e999"])
116
+ def test_non_finite_number_preserved_as_string(self, value):
117
+ assert coerce_to_schema_type(value, "number") == value
118
+
119
+ @pytest.mark.parametrize("value", ["inf", "1e999", "Infinity"])
120
+ def test_non_finite_integer_not_float_inf(self, value):
121
+ result = coerce_to_schema_type(value, "integer")
122
+ assert isinstance(result, str)
123
+ assert result == value
124
+
125
+ class TestNonFiniteContainers:
126
+ """Non-finite floats nested in object/array values must not produce
127
+ invalid JSON.
128
+
129
+ Regression: the ``object``/``array`` branch returned
130
+ ``json.loads(value)`` directly, so ``"[1e999]"`` became ``[inf]`` and
131
+ ``'{"x": Infinity}'`` became ``{"x": inf}`` -- values that
132
+ ``json.dumps`` later renders as invalid JSON (``Infinity``/``NaN``).
133
+ """
134
+
135
+ @pytest.mark.parametrize(
136
+ "value", ["[1e999]", "[1, 2, 1e999]", "[NaN]", "[-Infinity]"]
137
+ )
138
+ def test_array_with_non_finite_preserved_as_string(self, value):
139
+ result = coerce_to_schema_type(value, "array")
140
+ assert result == value
141
+ assert json.loads(json.dumps(result)) == result
142
+
143
+ @pytest.mark.parametrize(
144
+ "value", ['{"x": 1e999}', '{"x": Infinity}', '{"a": [1e999, 2]}']
145
+ )
146
+ def test_object_with_non_finite_preserved_as_string(self, value):
147
+ result = coerce_to_schema_type(value, "object")
148
+ assert result == value
149
+ assert json.loads(json.dumps(result)) == result
150
+
151
+ def test_finite_array_still_coerced(self):
152
+ assert coerce_to_schema_type("[1, 2, 3]", "array") == [1, 2, 3]
153
+
154
+ def test_finite_object_still_coerced(self):
155
+ assert coerce_to_schema_type('{"a": 1}', "object") == {"a": 1}
156
+
157
+ def test_unknown_type_non_finite_falls_back_to_string(self):
158
+ # Exercises the final json.loads fallback path.
159
+ assert coerce_to_schema_type("1e999", "unknown_type") == "1e999"
160
+
161
class TestBooleanType:
162
def test_true(self):
163
assert coerce_to_schema_type("true", "boolean") is True
vllm/tool_parsers/utils.py
+35
-3
@@ -3,6 +3,7 @@
3
4
import ast
5
import json
6
+import math
7
import warnings
8
from json import JSONDecodeError, JSONDecoder
9
from typing import Any, TypeAlias
@@ -145,6 +146,20 @@ def is_complete_json(input_str: str) -> bool:
146
return False
147
148
149
+def _is_json_finite(obj: Any) -> bool:
150
+ """Whether *obj* can be serialized to valid JSON.
151
+
152
+ ``json.dumps(..., allow_nan=False)`` raises ``ValueError`` on any
153
+ non-finite float (``inf``/``-inf``/``nan``) anywhere in the value, so this
154
+ detects non-finite floats nested inside parsed lists/dicts too.
155
+ """
156
+ try:
157
+ json.dumps(obj, allow_nan=False)
158
+ return True
159
+ except (ValueError, TypeError):
160
+ return False
161
+
162
+
163
def consume_space(i: int, s: str) -> int:
164
while i < len(s) and s[i].isspace():
165
i += 1
@@ -601,9 +616,15 @@ def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any:
616
if candidate_type == "number":
617
try:
618
val = float(value)
604
- return val if val != int(val) else int(val)
619
except (ValueError, TypeError):
620
continue
621
+ if not math.isfinite(val):
622
+ # inf/-inf/nan are not valid JSON numbers. Fall through so
623
+ # the value is preserved as a string instead of crashing
624
+ # (int(float("inf")) raises OverflowError) or emitting
625
+ # invalid JSON (json.dumps(inf) -> "Infinity").
626
+ continue
627
+ return val if val != int(val) else int(val)
628
if candidate_type == "boolean":
629
lower_val = value.lower().strip()
630
if lower_val in ("true", "1"):
@@ -613,14 +634,25 @@ def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any:
634
continue
635
if candidate_type in ("object", "array"):
636
try:
616
- return json.loads(value)
637
+ parsed = json.loads(value)
638
except (json.JSONDecodeError, ValueError, TypeError):
639
continue
640
+ if _is_json_finite(parsed):
641
+ return parsed
642
+ # Non-finite floats (e.g. "[1e999]" -> [inf]) cannot be
643
+ # serialized back to valid JSON; preserve the raw string.
644
+ continue
645
646
try:
621
- return json.loads(value)
647
+ parsed = json.loads(value)
648
except (json.JSONDecodeError, ValueError):
649
return value
650
+ # Reject non-finite results (e.g. json.loads("1e999") -> inf, or nested
651
+ # inf/nan inside a parsed list/dict) which json.dumps would render as
652
+ # invalid JSON (Infinity/NaN). Preserve the raw string instead.
653
+ if not _is_json_finite(parsed):
654
+ return value
655
+ return parsed
656
657
658
def compute_tool_delta(