fix(tools): tool_call bridge treats blank arguments as no arguments
Some OpenAI-compatible gateways emit `arguments: ""` (or whitespace) for a tool whose schema has no required parameters. On the tool_search bridge path (`normalize_tool_call_entries`) that string went straight into json.loads and every attempt failed with "arguments is not valid JSON: Expecting value ...", so the model retried the same call forever and the tool never ran (#83937). Treat a blank string like the already-accepted `None`: it means {}. The outer-loop already does this for the top-level tool_call arguments string (turn_tool_validation), so the bridge was the only surface left rejecting the shape; a tool with required params still gets the proper "missing required" hint from validate_deferred_call_args, and malformed non-blank JSON keeps failing closed (#61784 intent intact). Co-authored-by: webtecnica <webtecnica@gmail.com>
This commit is contained in:
@@ -471,6 +471,17 @@ class TestBridgeDispatch:
|
||||
assert err is not None
|
||||
assert "bridge tool" in err.lower()
|
||||
|
||||
@pytest.mark.parametrize("raw_args", ["", " \n", None])
|
||||
def test_resolve_underlying_call_treats_blank_arguments_as_no_arguments(self, raw_args):
|
||||
"""An OpenAI-compatible gateway emitting ``arguments: ""`` for a parameterless deferred tool
|
||||
must execute with {} instead of looping on a JSON parse error (#83937); malformed
|
||||
non-blank arguments still fail closed."""
|
||||
from tools.tool_search import resolve_underlying_call
|
||||
name, args, err = resolve_underlying_call({"calls": [{"name": "todo_list", "arguments": raw_args}]})
|
||||
assert (name, args, err) == ("todo_list", {}, None)
|
||||
_, _, err = resolve_underlying_call({"calls": [{"name": "todo_list", "arguments": '{"todos": ['}]})
|
||||
assert err and "not valid JSON" in err
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# End-to-end via the real handle_function_call (smoke test).
|
||||
|
||||
@@ -173,7 +173,11 @@ def normalize_tool_call_entries(args: Dict[str, Any]) -> Tuple[List[Dict[str, An
|
||||
if name in BRIDGE_TOOL_NAMES:
|
||||
return [], f"tool_call cannot invoke '{name}' (it is itself a bridge tool)"
|
||||
raw_args = raw.get("arguments")
|
||||
if raw_args is None:
|
||||
if raw_args is None or (isinstance(raw_args, str) and not raw_args.strip()):
|
||||
# "" / whitespace is how some OpenAI-compatible gateways spell "no arguments" for a
|
||||
# parameterless tool (#83937); the loop already treats an empty outer arguments string
|
||||
# as {} (turn_tool_validation), and a missing required param still surfaces below via
|
||||
# validate_deferred_call_args instead of an opaque JSON parse error.
|
||||
raw_args = {}
|
||||
if isinstance(raw_args, str):
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user