mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-08 13:12:08 +00:00
fix(agent): repair empty-name tool_calls in sanitizer to prevent Responses 400 (salvage #12807/#52893) (#55922)
* fix(agent): drop tool_calls with empty function.name to prevent orphan 400 Salvage of #12807 by @melonboy312 — rebased onto current main (sanitizer moved to agent_runtime_helpers), scoped to the sanitizer fix, with a regression test that fails without it. * fix(agent): repair (not drop) empty-name tool_calls to preserve anti-priming + prevent 400 Dropping empty-name tool_calls in the pre-call sanitizer collided with #47967, which intentionally keeps an empty-name call paired with a synthesized 'tool name was empty' anti-priming result so weak models self-correct without a full catalog dump. Dropping the call orphaned that result and stripped the signal (breaking tests/agent/test_empty_tool_name_loop_dampening.py). The actual HTTP 400 cause is an ORPHANED function_call_output (adapter drops the empty-name function_call but keeps its output). Rename the blank name to a non-empty sentinel instead: the call and its result stay paired, the adapter no longer drops the function_call, no orphan, no 400 — and the anti-priming result content the model needs is preserved. --------- Co-authored-by: Bartok9 <danielrpike9@gmail.com>
This commit is contained in:
parent
638d2e7bfc
commit
0cebf994c9
2 changed files with 90 additions and 0 deletions
|
|
@ -2257,6 +2257,54 @@ def sanitize_api_messages(messages: List[Dict[str, Any]]) -> List[Dict[str, Any]
|
|||
filtered.append(msg)
|
||||
messages = filtered
|
||||
|
||||
# --- Repair tool_calls whose function.name is empty/missing ---
|
||||
# Some providers (and partially-streamed responses) emit a tool_call with
|
||||
# id="call_xxx" but function.name="". Downstream Responses-API adapters
|
||||
# silently DROP such function_call items while still emitting the matching
|
||||
# function_call_output, producing the gateway's HTTP 400
|
||||
# "No tool call found for function call output with call_id ...".
|
||||
#
|
||||
# We do NOT drop the call: hermes' own dispatch loop intentionally keeps an
|
||||
# empty-name call paired with a synthesized anti-priming tool result
|
||||
# ("tool name was empty", see #47967) so weak models self-correct instead of
|
||||
# being fed the full tool catalog. Dropping the call here would (a) orphan
|
||||
# that result and strip the anti-priming signal, and (b) still leave any
|
||||
# provider-side orphan. Instead, rename the blank name to a non-empty
|
||||
# sentinel so the call and its result stay PAIRED — the adapter no longer
|
||||
# drops the function_call, so there is no orphaned output and no 400, while
|
||||
# the result content the model needs is preserved.
|
||||
_EMPTY_NAME_SENTINEL = "invalid_tool_call"
|
||||
for msg in messages:
|
||||
if msg.get("role") != "assistant":
|
||||
continue
|
||||
tcs = msg.get("tool_calls") or []
|
||||
if not tcs:
|
||||
continue
|
||||
for tc in tcs:
|
||||
if isinstance(tc, dict):
|
||||
fn = tc.get("function")
|
||||
name = fn.get("name") if isinstance(fn, dict) else getattr(fn, "name", None)
|
||||
else:
|
||||
fn = getattr(tc, "function", None)
|
||||
name = getattr(fn, "name", None) if fn else None
|
||||
if isinstance(name, str) and name.strip():
|
||||
continue
|
||||
_ra().logger.warning(
|
||||
"Pre-call sanitizer: repairing tool_call with empty "
|
||||
"function.name -> %r (id=%s)",
|
||||
_EMPTY_NAME_SENTINEL,
|
||||
_ra().AIAgent._get_tool_call_id_static(tc),
|
||||
)
|
||||
if isinstance(fn, dict):
|
||||
fn["name"] = _EMPTY_NAME_SENTINEL
|
||||
elif fn is not None and hasattr(fn, "name"):
|
||||
try:
|
||||
fn.name = _EMPTY_NAME_SENTINEL
|
||||
except Exception:
|
||||
pass
|
||||
elif isinstance(tc, dict):
|
||||
tc["function"] = {"name": _EMPTY_NAME_SENTINEL, "arguments": "{}"}
|
||||
|
||||
surviving_call_ids: set = set()
|
||||
for msg in messages:
|
||||
if msg.get("role") == "assistant":
|
||||
|
|
|
|||
|
|
@ -3659,6 +3659,48 @@ class TestHandleMaxIterations:
|
|||
"call_123"
|
||||
]
|
||||
|
||||
def test_api_sanitizer_repairs_tool_call_with_empty_function_name(self, agent):
|
||||
"""A tool_call with id but empty function.name makes the Responses-API
|
||||
adapter drop the function_call while keeping its function_call_output,
|
||||
causing the gateway's HTTP 400 'No tool call found for function call
|
||||
output ...'. The sanitizer renames the blank name to a non-empty
|
||||
sentinel so the call and its result stay PAIRED (no orphaned output,
|
||||
no 400) while the result content is preserved — it must NOT drop the
|
||||
call, because hermes' dispatch loop keeps empty-name calls paired with
|
||||
an anti-priming result for self-correction (#47967). (#12807)"""
|
||||
messages = [
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_good",
|
||||
"type": "function",
|
||||
"function": {"name": "web_search", "arguments": "{}"},
|
||||
},
|
||||
{
|
||||
"id": "call_bad",
|
||||
"type": "function",
|
||||
"function": {"name": "", "arguments": "{}"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_good", "content": "ok"},
|
||||
{"role": "tool", "tool_call_id": "call_bad", "content": "orphan"},
|
||||
]
|
||||
|
||||
sanitized = agent._sanitize_api_messages(messages)
|
||||
|
||||
# The good call is untouched; the malformed call is repaired in place
|
||||
# (renamed to a non-empty sentinel) rather than dropped.
|
||||
assistant = next(m for m in sanitized if m.get("role") == "assistant")
|
||||
names = [tc["function"]["name"] for tc in assistant["tool_calls"]]
|
||||
assert names == ["web_search", "invalid_tool_call"]
|
||||
# Both calls now have non-empty names, so neither output is orphaned
|
||||
# and both tool results survive — this is what prevents the 400.
|
||||
tool_ids = [m.get("tool_call_id") for m in sanitized if m.get("role") == "tool"]
|
||||
assert tool_ids == ["call_good", "call_bad"]
|
||||
|
||||
|
||||
class TestRunConversation:
|
||||
"""Tests for the main run_conversation method.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue