[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(