# NousResearch/hermes-agent · Fix missing function names in tool_calls

## Open now

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

## TL;DR

- 4 member(s): 0 open issue(s), 1 closed issue(s), 0 open PR(s), 1 merged PR(s), 2 PR(s) closed without merging.
- A fix was merged to main in PR #55922. Verify it addresses your case.
- **Recommended:** read PR #55922 (and any competing PRs below) before filing. Confirm the merged change covers your symptom.

## Maintainer resolution verdict

- Internal-tracker verdict over this cluster's issue members: `unresolved`. This is distinct from the fix status above. Weigh it yourself.

## How this cluster was grouped

- Link evidence (`STRUCTURAL_SPANNING`): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
- Pairwise link density `0.50`: the share of member 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.

## Closed / duplicate issues

- #49968: tool_calls missing function.name when using Xiaomi MiMo provider

## Prior & competing PRs

- **#49993: fix(agent): repair missing tool call function names (#49968)**. closed without merge · fix tier `suggested`
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `0cebf994c965672090a58a0dec3b78f565fbaac5`, `#49968`
  - _suggested-tier fix detail withheld; re-request with `?tier=all` to include it._
- **#55922: fix(agent): repair empty-name tool_calls in sanitizer to prevent Responses 400 (salvage #12807/#52893)**. merged to main · fix tier `verified`
  - maintainer direction: category `merged`
  - files: `agent/agent_runtime_helpers.py`, `tests/run_agent/test_run_agent.py`
  - fix hunks:
  - `agent/agent_runtime_helpers.py`

    ```diff
    @@ -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`

    ```diff
    @@ -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.
    ```
- **#49999: fix: repair empty function.name in tool_calls for strict providers**. closed without merge
  - maintainer direction: category `implemented_elsewhere`; implication `superseded`; refs `0cebf994c965672090a58a0dec3b78f565fbaac5`, `#55922`, `#47967`, `#12807`

## Fix status

- Merged to main in PR #55922 (cited above). No release data is tracked. Verify downstream.
- Fix last verified (merged) `2026-06-30`.
- Subsystem: `agent`
