NousResearch/hermes-agent

Fix missing function names in tool_calls

Cluster 49968 · agent. Data as of

Nothing open on GitHub. Fix merged in #55922 on 30 Jun 2026. Check it covers your case.

The short answer

0open issues
0open PRs
4items grouped
Known?
Yes. 4 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #55922. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this unresolved. That is separate from the fix event.
Still open
Nothing open on GitHub.

A bug where tool_call objects are missing the function.name field causes errors with certain providers. Multiple fix attempts address sanitizing or repairing these empty names before requests are sent.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #55922 fix(agent): repair empty-name tool_calls in sanitizer to prevent Responses 400 (salvage #12807/#52893)

    merged to main

    files: agent/agent_runtime_helpers.py, tests/run_agent/test_run_agent.py

    Show the diff
    • agent/agent_runtime_helpers.py
      @@ -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":
    • tests/run_agent/test_run_agent.py
      @@ -3659,6 +3659,48 @@ def test_api_sanitizer_matches_responses_call_id_when_id_differs(self, agent):
                   "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.

2 PRs closed without merging

From the close comments: done elsewhere 2

  • #49993 fix(agent): repair missing tool call function names (#49968) · done elsewhere → 0cebf99, #49968
  • #49999 fix: repair empty function.name in tool_calls for strict providers · done elsewhere → 0cebf99, #55922, #47967, #12807
Closed issues · 1
  • Closed#49968tool_calls missing function.name when using Xiaomi MiMo provider
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.50: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Subsystem
agent
Merged PRs
#55922
Closed issues
1
Attempts
2 PRs closed without merging
Fix link checked
30 Jun 2026

For your agent

The same facts, typed.