[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>
This commit is contained in:
Ashish Patel
2026-06-18 16:00:20 +00:00
committed by GitHub
co-authored by Ben Browning
parent bf2a393034
commit 837db7605e
2 changed files with 102 additions and 3 deletions
+67
View File
@@ -1,6 +1,8 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import json
import pytest
from vllm.tool_parsers.utils import (
@@ -91,6 +93,71 @@ class TestCoerceToSchemaType:
def test_invalid_number_fallback(self):
assert coerce_to_schema_type("abc", "number") == "abc"
class TestNonFiniteNumbers:
"""Non-finite numeric strings must not crash and must coerce to a
JSON-serializable value.
Regression: ``int(float("inf"))`` raised an uncaught ``OverflowError``
(only ``ValueError``/``TypeError`` were handled), and ``"1e999"``
round-tripped through ``json.loads`` to a float ``inf`` that
``json.dumps`` renders as invalid JSON ``Infinity``.
"""
@pytest.mark.parametrize(
"value", ["inf", "-inf", "Infinity", "1e999", "nan", "-nan"]
)
def test_non_finite_number_does_not_crash(self, value):
# Must not raise (previously OverflowError for inf/1e999/Infinity).
result = coerce_to_schema_type(value, "number")
# Result must serialize to valid, finite JSON and round-trip.
assert json.loads(json.dumps(result)) == result
@pytest.mark.parametrize("value", ["inf", "-inf", "1e999"])
def test_non_finite_number_preserved_as_string(self, value):
assert coerce_to_schema_type(value, "number") == value
@pytest.mark.parametrize("value", ["inf", "1e999", "Infinity"])
def test_non_finite_integer_not_float_inf(self, value):
result = coerce_to_schema_type(value, "integer")
assert isinstance(result, str)
assert result == value
class TestNonFiniteContainers:
"""Non-finite floats nested in object/array values must not produce
invalid JSON.
Regression: the ``object``/``array`` branch returned
``json.loads(value)`` directly, so ``"[1e999]"`` became ``[inf]`` and
``'{"x": Infinity}'`` became ``{"x": inf}`` -- values that
``json.dumps`` later renders as invalid JSON (``Infinity``/``NaN``).
"""
@pytest.mark.parametrize(
"value", ["[1e999]", "[1, 2, 1e999]", "[NaN]", "[-Infinity]"]
)
def test_array_with_non_finite_preserved_as_string(self, value):
result = coerce_to_schema_type(value, "array")
assert result == value
assert json.loads(json.dumps(result)) == result
@pytest.mark.parametrize(
"value", ['{"x": 1e999}', '{"x": Infinity}', '{"a": [1e999, 2]}']
)
def test_object_with_non_finite_preserved_as_string(self, value):
result = coerce_to_schema_type(value, "object")
assert result == value
assert json.loads(json.dumps(result)) == result
def test_finite_array_still_coerced(self):
assert coerce_to_schema_type("[1, 2, 3]", "array") == [1, 2, 3]
def test_finite_object_still_coerced(self):
assert coerce_to_schema_type('{"a": 1}', "object") == {"a": 1}
def test_unknown_type_non_finite_falls_back_to_string(self):
# Exercises the final json.loads fallback path.
assert coerce_to_schema_type("1e999", "unknown_type") == "1e999"
class TestBooleanType:
def test_true(self):
assert coerce_to_schema_type("true", "boolean") is True
+35 -3
View File
@@ -3,6 +3,7 @@
import ast
import json
import math
import warnings
from json import JSONDecodeError, JSONDecoder
from typing import Any, TypeAlias
@@ -145,6 +146,20 @@ def is_complete_json(input_str: str) -> bool:
return False
def _is_json_finite(obj: Any) -> bool:
"""Whether *obj* can be serialized to valid JSON.
``json.dumps(..., allow_nan=False)`` raises ``ValueError`` on any
non-finite float (``inf``/``-inf``/``nan``) anywhere in the value, so this
detects non-finite floats nested inside parsed lists/dicts too.
"""
try:
json.dumps(obj, allow_nan=False)
return True
except (ValueError, TypeError):
return False
def consume_space(i: int, s: str) -> int:
while i < len(s) and s[i].isspace():
i += 1
@@ -601,9 +616,15 @@ def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any:
if candidate_type == "number":
try:
val = float(value)
return val if val != int(val) else int(val)
except (ValueError, TypeError):
continue
if not math.isfinite(val):
# inf/-inf/nan are not valid JSON numbers. Fall through so
# the value is preserved as a string instead of crashing
# (int(float("inf")) raises OverflowError) or emitting
# invalid JSON (json.dumps(inf) -> "Infinity").
continue
return val if val != int(val) else int(val)
if candidate_type == "boolean":
lower_val = value.lower().strip()
if lower_val in ("true", "1"):
@@ -613,14 +634,25 @@ def coerce_to_schema_type(value: str, schema_type: str | list[str]) -> Any:
continue
if candidate_type in ("object", "array"):
try:
return json.loads(value)
parsed = json.loads(value)
except (json.JSONDecodeError, ValueError, TypeError):
continue
if _is_json_finite(parsed):
return parsed
# Non-finite floats (e.g. "[1e999]" -> [inf]) cannot be
# serialized back to valid JSON; preserve the raw string.
continue
try:
return json.loads(value)
parsed = json.loads(value)
except (json.JSONDecodeError, ValueError):
return value
# Reject non-finite results (e.g. json.loads("1e999") -> inf, or nested
# inf/nan inside a parsed list/dict) which json.dumps would render as
# invalid JSON (Infinity/NaN). Preserve the raw string instead.
if not _is_json_finite(parsed):
return value
return parsed
def compute_tool_delta(