fix(agent): include name field on every role:tool message for Gemini compatibility (#16478)

Gemini's OpenAI-compatibility endpoint strictly requires the `name` field
on `role: tool` messages — it returns HTTP 400 ("Request contains an
invalid argument") when the function name is missing. OpenAI/Anthropic/
ollama tolerate the absence, so the gap stays invisible until the
conversation accumulates a tool turn and the user routes it through Gemini
(direct API or via ollama-cloud proxy).

Fix: add a `_get_tool_call_name_static()` helper alongside the existing
`_get_tool_call_id_static()`, and populate `name` at every site that
constructs a `role: tool` message — the pre-call sanitizer stub, the
tool-call args repair marker, both interrupt-skip paths, both
result-append paths (parallel + sequential), the invalid-tool-name
recovery, the invalid-JSON-args recovery, and the exception fallback.

Each call site was already in scope of the function name (`function_name`,
`skipped_name`, `name`, or a dict tool_call), so the change is local —
no new lookups, no behavior change for providers that already worked.

Fixes #16478
This commit is contained in:
0xsir0000 2026-04-27 17:51:11 +08:00 committed by Teknium
parent 0443484115
commit 52882dade6
3 changed files with 59 additions and 0 deletions

View file

@ -263,3 +263,34 @@ class TestGetToolCallIdStatic:
def test_object_without_id_attr(self):
tc = types.SimpleNamespace()
assert AIAgent._get_tool_call_id_static(tc) == ""
# ---------------------------------------------------------------------------
# _get_tool_call_name_static
# ---------------------------------------------------------------------------
class TestGetToolCallNameStatic:
def test_dict_with_valid_name(self):
assert AIAgent._get_tool_call_name_static(
{"id": "call_1", "function": {"name": "terminal", "arguments": "{}"}}
) == "terminal"
def test_dict_with_missing_function(self):
assert AIAgent._get_tool_call_name_static({"id": "call_1"}) == ""
def test_dict_with_none_function(self):
assert AIAgent._get_tool_call_name_static({"id": "call_1", "function": None}) == ""
def test_dict_with_none_name(self):
assert AIAgent._get_tool_call_name_static(
{"function": {"name": None, "arguments": "{}"}}
) == ""
def test_object_with_valid_name(self):
tc = make_tc("read_file")
assert AIAgent._get_tool_call_name_static(tc) == "read_file"
def test_object_without_function_attr(self):
tc = types.SimpleNamespace(id="call_1")
assert AIAgent._get_tool_call_name_static(tc) == ""