Improve tool call validation strictness (#4190)

* Improve tool call validation strictness

Reject near-miss tool names without executing suggested tools. Require object-shaped tool parameters while preserving only lossless JSON wire-shape normalization.

* Tighten tool call argument validation

* Simplify tool argument validation tests

* Improve tool name suggestions

* Simplify tool suggestion helpers

* Limit tool suggestions to canonical matches

* Allow repair only for tool history replay

* Clarify non-object tool argument errors

* Inline replay tool argument normalization

* Track only successful tool executions

* Reject JSON null tool arguments
This commit is contained in:
chengyongru
2026-06-09 14:50:40 +08:00
committed by GitHub
parent f3eb2aa08b
commit 0a396aa6e2
17 changed files with 769 additions and 142 deletions
+28 -1
View File
@@ -54,6 +54,15 @@ def _fake_tool_call_response() -> SimpleNamespace:
return SimpleNamespace(choices=[choice], usage=usage)
def _fake_tool_call_response_with_arguments(arguments) -> SimpleNamespace:
"""Build a minimal chat response with caller-supplied tool arguments."""
function = SimpleNamespace(name="optional_tool", arguments=arguments)
tool_call = SimpleNamespace(id="call_123", type="function", function=function)
message = SimpleNamespace(content=None, tool_calls=[tool_call], reasoning_content=None)
choice = SimpleNamespace(message=message, finish_reason="tool_calls")
return SimpleNamespace(choices=[choice], usage=SimpleNamespace())
def _fake_responses_response(content: str = "ok") -> MagicMock:
"""Build a minimal Responses API response object."""
resp = MagicMock()
@@ -611,6 +620,24 @@ async def test_openai_compat_preserves_extra_content_on_tool_calls() -> None:
assert serialized["function"]["provider_specific_fields"] == {"inner": "value"}
def test_openai_compat_parse_preserves_malformed_tool_arguments() -> None:
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
provider = OpenAICompatProvider()
result = provider._parse(_fake_tool_call_response_with_arguments('{path:"foo.txt"}'))
assert result.tool_calls[0].arguments == '{path:"foo.txt"}'
def test_openai_compat_parse_preserves_array_tool_arguments() -> None:
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
provider = OpenAICompatProvider()
result = provider._parse(_fake_tool_call_response_with_arguments('["foo.txt"]'))
assert result.tool_calls[0].arguments == ["foo.txt"]
def test_openai_model_passthrough() -> None:
"""OpenAI models pass through unchanged."""
spec = find_by_name("openai")
@@ -1110,7 +1137,7 @@ def test_openai_compat_stringifies_dict_tool_arguments() -> None:
assert sanitized[1]["tool_calls"][0]["function"]["arguments"] == '{"cmd": "ls -la"}'
def test_openai_compat_repairs_non_json_tool_arguments_string() -> None:
def test_openai_compat_repairs_object_like_history_tool_arguments_string() -> None:
with patch("nanobot.providers.openai_compat_provider.AsyncOpenAI"):
provider = OpenAICompatProvider()