mirror of
https://github.com/usestrix/strix.git
synced 2026-10-15 04:18:10 +00:00
fix(models): treat NaN/Infinity tool-call arguments as malformed
This commit is contained in:
parent
6753a5610a
commit
b6f38bc90c
3 changed files with 29 additions and 6 deletions
|
|
@ -22,6 +22,14 @@ from openai.types.responses import ResponseFunctionToolCall
|
|||
MALFORMED_ARGUMENTS_KEY = "malformed_arguments"
|
||||
|
||||
|
||||
def _reject_constant(name: str) -> Any:
|
||||
raise ValueError(f"non-standard JSON constant {name}")
|
||||
|
||||
|
||||
def _loads_strict(text: str) -> Any:
|
||||
return json.loads(text, parse_constant=_reject_constant)
|
||||
|
||||
|
||||
def describe_malformed_arguments(tool_name: str, arguments: str) -> str | None:
|
||||
"""Return a model-facing recovery message if ``arguments`` is not a JSON object.
|
||||
|
||||
|
|
@ -33,7 +41,7 @@ def describe_malformed_arguments(tool_name: str, arguments: str) -> str | None:
|
|||
if not arguments.strip():
|
||||
return None
|
||||
try:
|
||||
parsed = json.loads(arguments)
|
||||
parsed = _loads_strict(arguments)
|
||||
except ValueError as exc:
|
||||
detail = str(exc)
|
||||
else:
|
||||
|
|
@ -52,7 +60,7 @@ def repair_arguments(arguments: object) -> str | None:
|
|||
if not isinstance(arguments, str) or not arguments.strip():
|
||||
return "{}"
|
||||
try:
|
||||
parsed = json.loads(arguments)
|
||||
parsed = _loads_strict(arguments)
|
||||
except ValueError:
|
||||
parsed = None
|
||||
if isinstance(parsed, dict):
|
||||
|
|
|
|||
|
|
@ -150,11 +150,12 @@ async def test_unknown_and_null_arguments_are_untouched() -> None:
|
|||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_non_object_payloads_are_reported_instead_of_invoked() -> None:
|
||||
@pytest.mark.parametrize("arguments", ["not json", '{"n": NaN}'])
|
||||
async def test_non_object_payloads_are_reported_instead_of_invoked(arguments: str) -> None:
|
||||
captured: dict[str, str] = {}
|
||||
wrapped = factory._with_coerced_arguments(_capturing_tool(captured, _ARRAY))
|
||||
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), "not json")
|
||||
result = await wrapped.on_invoke_tool(cast("Any", None), arguments)
|
||||
|
||||
assert result.startswith("probe: the tool call was not executed")
|
||||
assert "raw_input" not in captured
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ from openai.types.responses import ResponseFunctionToolCall
|
|||
from strix.config.models import _NonStreamingModel, _TurnGuardModel
|
||||
from strix.config.tool_call_arguments import (
|
||||
MALFORMED_ARGUMENTS_KEY,
|
||||
_loads_strict,
|
||||
describe_malformed_arguments,
|
||||
repair_arguments,
|
||||
repair_history_arguments,
|
||||
|
|
@ -213,11 +214,21 @@ def test_repair_arguments_rewrites_anything_but_a_json_object(
|
|||
assert repair_arguments(arguments) == expected
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", ["{}", '{"cmd": "ls -la"}', '{"nested": {"a": [1]}}'])
|
||||
@pytest.mark.parametrize(
|
||||
"arguments", ["{}", '{"cmd": "ls -la"}', '{"nested": {"a": [1]}}', '{"n": "NaN"}']
|
||||
)
|
||||
def test_repair_arguments_leaves_json_objects_alone(arguments: str) -> None:
|
||||
assert repair_arguments(arguments) is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", ['{"n": NaN}', '{"n": Infinity}', '{"n": -Infinity}'])
|
||||
def test_repair_arguments_wraps_non_standard_json_constants(arguments: str) -> None:
|
||||
repaired = repair_arguments(arguments)
|
||||
|
||||
assert repaired is not None
|
||||
assert _loads_strict(repaired) == {MALFORMED_ARGUMENTS_KEY: arguments}
|
||||
|
||||
|
||||
def test_history_repair_rewrites_only_malformed_calls() -> None:
|
||||
items = [
|
||||
{"role": "user", "content": "go"},
|
||||
|
|
@ -237,6 +248,7 @@ def test_history_repair_rewrites_only_malformed_calls() -> None:
|
|||
assert rebuilt[3] is items[3]
|
||||
assert isinstance(rebuilt[4], ResponseFunctionToolCall)
|
||||
assert rebuilt[4].arguments == "{}"
|
||||
assert isinstance(items[1], dict)
|
||||
assert items[1]["arguments"] == '{"cmd": "ls'
|
||||
|
||||
|
||||
|
|
@ -247,7 +259,9 @@ def test_repair_input_returns_same_object_when_nothing_changes() -> None:
|
|||
assert repair_input("plain prompt") == "plain prompt"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("arguments", ['{"cmd": "ls -la', "[1, 2]", "null", "not json"])
|
||||
@pytest.mark.parametrize(
|
||||
"arguments", ['{"cmd": "ls -la', "[1, 2]", "null", "not json", '{"n": NaN}']
|
||||
)
|
||||
def test_describe_malformed_arguments_tells_the_model_to_reissue(arguments: str) -> None:
|
||||
message = describe_malformed_arguments("exec_command", arguments)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue