mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
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:
parent
0443484115
commit
52882dade6
3 changed files with 59 additions and 0 deletions
27
run_agent.py
27
run_agent.py
|
|
@ -5064,6 +5064,23 @@ class AIAgent:
|
||||||
return tc.get("call_id", "") or tc.get("id", "") or ""
|
return tc.get("call_id", "") or tc.get("id", "") or ""
|
||||||
return getattr(tc, "call_id", "") or getattr(tc, "id", "") or ""
|
return getattr(tc, "call_id", "") or getattr(tc, "id", "") or ""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_tool_call_name_static(tc) -> str:
|
||||||
|
"""Extract function name from a tool_call entry (dict or object).
|
||||||
|
|
||||||
|
Gemini's OpenAI-compatibility endpoint requires every `role: tool`
|
||||||
|
message to carry the matching function name. OpenAI/Anthropic/ollama
|
||||||
|
tolerate its absence, so the field is best-effort: callers fall back
|
||||||
|
to "" and the message still works elsewhere.
|
||||||
|
"""
|
||||||
|
if isinstance(tc, dict):
|
||||||
|
fn = tc.get("function")
|
||||||
|
if isinstance(fn, dict):
|
||||||
|
return fn.get("name", "") or ""
|
||||||
|
return ""
|
||||||
|
fn = getattr(tc, "function", None)
|
||||||
|
return getattr(fn, "name", "") or ""
|
||||||
|
|
||||||
_VALID_API_ROLES = frozenset({"system", "user", "assistant", "tool", "function", "developer"})
|
_VALID_API_ROLES = frozenset({"system", "user", "assistant", "tool", "function", "developer"})
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -5126,6 +5143,7 @@ class AIAgent:
|
||||||
if cid in missing_results:
|
if cid in missing_results:
|
||||||
patched.append({
|
patched.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": AIAgent._get_tool_call_name_static(tc),
|
||||||
"content": "[Result unavailable — see context summary above]",
|
"content": "[Result unavailable — see context summary above]",
|
||||||
"tool_call_id": cid,
|
"tool_call_id": cid,
|
||||||
})
|
})
|
||||||
|
|
@ -9030,6 +9048,7 @@ class AIAgent:
|
||||||
insert_at,
|
insert_at,
|
||||||
{
|
{
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": function_name if function_name != "?" else "",
|
||||||
"tool_call_id": tool_call_id,
|
"tool_call_id": tool_call_id,
|
||||||
"content": marker,
|
"content": marker,
|
||||||
},
|
},
|
||||||
|
|
@ -9434,6 +9453,7 @@ class AIAgent:
|
||||||
for tc in tool_calls:
|
for tc in tool_calls:
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": tc.function.name,
|
||||||
"content": f"[Tool execution cancelled — {tc.function.name} was skipped due to user interrupt]",
|
"content": f"[Tool execution cancelled — {tc.function.name} was skipped due to user interrupt]",
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
})
|
})
|
||||||
|
|
@ -9775,6 +9795,7 @@ class AIAgent:
|
||||||
|
|
||||||
tool_msg = {
|
tool_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": name,
|
||||||
"content": function_result,
|
"content": function_result,
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
}
|
}
|
||||||
|
|
@ -9812,6 +9833,7 @@ class AIAgent:
|
||||||
skipped_name = skipped_tc.function.name
|
skipped_name = skipped_tc.function.name
|
||||||
skip_msg = {
|
skip_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": skipped_name,
|
||||||
"content": f"[Tool execution cancelled — {skipped_name} was skipped due to user interrupt]",
|
"content": f"[Tool execution cancelled — {skipped_name} was skipped due to user interrupt]",
|
||||||
"tool_call_id": skipped_tc.id,
|
"tool_call_id": skipped_tc.id,
|
||||||
}
|
}
|
||||||
|
|
@ -10162,6 +10184,7 @@ class AIAgent:
|
||||||
|
|
||||||
tool_msg = {
|
tool_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": function_name,
|
||||||
"content": function_result,
|
"content": function_result,
|
||||||
"tool_call_id": tool_call.id
|
"tool_call_id": tool_call.id
|
||||||
}
|
}
|
||||||
|
|
@ -10188,6 +10211,7 @@ class AIAgent:
|
||||||
skipped_name = skipped_tc.function.name
|
skipped_name = skipped_tc.function.name
|
||||||
skip_msg = {
|
skip_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": skipped_name,
|
||||||
"content": f"[Tool execution skipped — {skipped_name} was not started. User sent a new message]",
|
"content": f"[Tool execution skipped — {skipped_name} was not started. User sent a new message]",
|
||||||
"tool_call_id": skipped_tc.id
|
"tool_call_id": skipped_tc.id
|
||||||
}
|
}
|
||||||
|
|
@ -13110,6 +13134,7 @@ class AIAgent:
|
||||||
content = "Skipped: another tool call in this turn used an invalid name. Please retry this tool call."
|
content = "Skipped: another tool call in this turn used an invalid name. Please retry this tool call."
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": tc.function.name,
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
"content": content,
|
"content": content,
|
||||||
})
|
})
|
||||||
|
|
@ -13201,6 +13226,7 @@ class AIAgent:
|
||||||
tool_result = "Skipped: other tool call in this response had invalid JSON."
|
tool_result = "Skipped: other tool call in this response had invalid JSON."
|
||||||
messages.append({
|
messages.append({
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": tc.function.name,
|
||||||
"tool_call_id": tc.id,
|
"tool_call_id": tc.id,
|
||||||
"content": tool_result,
|
"content": tool_result,
|
||||||
})
|
})
|
||||||
|
|
@ -13717,6 +13743,7 @@ class AIAgent:
|
||||||
if tc["id"] not in answered_ids:
|
if tc["id"] not in answered_ids:
|
||||||
err_msg = {
|
err_msg = {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": AIAgent._get_tool_call_name_static(tc),
|
||||||
"tool_call_id": tc["id"],
|
"tool_call_id": tc["id"],
|
||||||
"content": f"Error executing tool: {error_msg}",
|
"content": f"Error executing tool: {error_msg}",
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -263,3 +263,34 @@ class TestGetToolCallIdStatic:
|
||||||
def test_object_without_id_attr(self):
|
def test_object_without_id_attr(self):
|
||||||
tc = types.SimpleNamespace()
|
tc = types.SimpleNamespace()
|
||||||
assert AIAgent._get_tool_call_id_static(tc) == ""
|
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) == ""
|
||||||
|
|
|
||||||
|
|
@ -96,6 +96,7 @@ def test_marker_message_inserted_when_missing():
|
||||||
assert repaired == 1
|
assert repaired == 1
|
||||||
assert messages[1] == {
|
assert messages[1] == {
|
||||||
"role": "tool",
|
"role": "tool",
|
||||||
|
"name": "read_file",
|
||||||
"tool_call_id": "call_1",
|
"tool_call_id": "call_1",
|
||||||
"content": marker,
|
"content": marker,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue