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:
teknium1
2026-09-19 00:04:49 -07:00
committed by Teknium
parent 4b4e8ed4bd
commit 462c1c14dd
2 changed files with 16 additions and 1 deletions

View File

@@ -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).

View File

@@ -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: