[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:
co-authored by
Ben Browning
parent
bf2a393034
commit
837db7605e
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user