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:
@@ -17,10 +17,15 @@ from ipaddress import ip_address
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import json_repair
|
||||
from loguru import logger
|
||||
|
||||
from nanobot.providers.base import LLMProvider, LLMResponse, ToolCallRequest
|
||||
from nanobot.providers.base import (
|
||||
LLMProvider,
|
||||
LLMResponse,
|
||||
ToolCallRequest,
|
||||
parse_tool_arguments,
|
||||
tool_arguments_json_for_replay,
|
||||
)
|
||||
from nanobot.providers.openai_responses import (
|
||||
consume_sdk_stream,
|
||||
convert_messages,
|
||||
@@ -478,24 +483,6 @@ class OpenAICompatProvider(LLMProvider):
|
||||
"""Return True for providers that reject normal OpenAI tool call IDs."""
|
||||
return bool(self._spec and self._spec.name == "mistral")
|
||||
|
||||
@staticmethod
|
||||
def _normalize_tool_call_arguments(arguments: Any) -> str:
|
||||
"""Force function.arguments into a valid JSON object string."""
|
||||
if isinstance(arguments, str):
|
||||
stripped = arguments.strip()
|
||||
if not stripped:
|
||||
return "{}"
|
||||
try:
|
||||
parsed = json_repair.loads(stripped)
|
||||
except Exception:
|
||||
return "{}"
|
||||
if isinstance(parsed, dict):
|
||||
return json.dumps(parsed, ensure_ascii=False)
|
||||
return "{}"
|
||||
if isinstance(arguments, dict):
|
||||
return json.dumps(arguments, ensure_ascii=False)
|
||||
return "{}"
|
||||
|
||||
@staticmethod
|
||||
def _coerce_content_to_string(content: Any) -> str | None:
|
||||
"""Coerce block/list content into plain text for strict string-only APIs."""
|
||||
@@ -572,7 +559,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
if isinstance(function, dict):
|
||||
function_clean = dict(function)
|
||||
if "arguments" in function_clean:
|
||||
function_clean["arguments"] = self._normalize_tool_call_arguments(
|
||||
function_clean["arguments"] = tool_arguments_json_for_replay(
|
||||
function_clean.get("arguments")
|
||||
)
|
||||
else:
|
||||
@@ -1021,14 +1008,12 @@ class OpenAICompatProvider(LLMProvider):
|
||||
for tc in raw_tool_calls:
|
||||
tc_map = self._maybe_mapping(tc) or {}
|
||||
fn = self._maybe_mapping(tc_map.get("function")) or {}
|
||||
args = fn.get("arguments", {})
|
||||
if isinstance(args, str):
|
||||
args = json_repair.loads(args)
|
||||
args = parse_tool_arguments(fn.get("arguments", {}))
|
||||
ec, prov, fn_prov = _extract_tc_extras(tc)
|
||||
parsed_tool_calls.append(ToolCallRequest(
|
||||
id=str(tc_map.get("id") or _short_tool_id()),
|
||||
name=str(fn.get("name") or ""),
|
||||
arguments=args if isinstance(args, dict) else {},
|
||||
arguments=args,
|
||||
extra_content=ec,
|
||||
provider_specific_fields=prov,
|
||||
function_provider_specific_fields=fn_prov,
|
||||
@@ -1064,9 +1049,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
|
||||
tool_calls = []
|
||||
for tc in raw_tool_calls:
|
||||
args = tc.function.arguments
|
||||
if isinstance(args, str):
|
||||
args = json_repair.loads(args)
|
||||
args = parse_tool_arguments(tc.function.arguments)
|
||||
ec, prov, fn_prov = _extract_tc_extras(tc)
|
||||
tool_calls.append(ToolCallRequest(
|
||||
id=str(getattr(tc, "id", None) or _short_tool_id()),
|
||||
@@ -1207,7 +1190,7 @@ class OpenAICompatProvider(LLMProvider):
|
||||
ToolCallRequest(
|
||||
id=b["id"] or _short_tool_id(),
|
||||
name=b["name"],
|
||||
arguments=json_repair.loads(b["arguments"]) if b["arguments"] else {},
|
||||
arguments=parse_tool_arguments(b["arguments"]),
|
||||
extra_content=b.get("extra_content"),
|
||||
provider_specific_fields=b.get("prov"),
|
||||
function_provider_specific_fields=b.get("fn_prov"),
|
||||
|
||||
Reference in New Issue
Block a user