hermes - 💡(How to fix) Fix DeepSeek tool-calling turns missing reasoning_content cause HTTP 400 — Kimi-only compensation should generalise [4 comments, 5 participants]

Official PRs (…)
ON THIS PAGE

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#16388Fetched 2026-04-28 06:53:42
View on GitHub
Comments
4
Participants
5
Timeline
11
Reactions
0
Author
Timeline (top)
commented ×4labeled ×4closed ×1mentioned ×1

run_agent.py::_copy_reasoning_content_for_api includes a special case that injects an empty reasoning_content string on prior assistant turns that contain tool_calls but no reasoning field, but only when the active provider is Kimi/Moonshot. DeepSeek's reasoning-capable models (deepseek-reasoner, deepseek-chat with thinking on) require the same compensation — without it the next turn is rejected with HTTP 400 ("messages with tool_calls must include reasoning_content").

Tested on Hermes Agent v0.11.0.

Root Cause

run_agent.py::_copy_reasoning_content_for_api (around the version shipped in v0.11.0):

def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> None:
    """Copy provider-facing reasoning fields onto an API replay message."""
    if source_msg.get("role") != "assistant":
        return

    explicit_reasoning = source_msg.get("reasoning_content")
    if isinstance(explicit_reasoning, str):
        api_msg["reasoning_content"] = explicit_reasoning
        return

    normalized_reasoning = source_msg.get("reasoning")
    if isinstance(normalized_reasoning, str) and normalized_reasoning:
        api_msg["reasoning_content"] = normalized_reasoning
        return

    kimi_requires_reasoning = (
        self.provider in {"kimi-coding", "kimi-coding-cn"}
        or base_url_host_matches(self.base_url, "api.kimi.com")
        or base_url_host_matches(self.base_url, "moonshot.ai")
        or base_url_host_matches(self.base_url, "moonshot.cn")
    )
    if kimi_requires_reasoning and source_msg.get("tool_calls"):
        api_msg["reasoning_content"] = ""

The compensation block only fires for Kimi/Moonshot endpoints. DeepSeek imposes the same constraint when reasoning mode is active but is not in the allowlist, so the missing-reasoning_content assistant turn slips through and the upstream rejects it.

Fix Action

Workaround

Switch reasoning off (use deepseek-chat without thinking), or route DeepSeek through OpenRouter / a relay that strips the reasoning_content requirement upstream. Both lose the value DeepSeek offers in the first place.

Code Example

model:
  default: deepseek-reasoner
  provider: deepseek

---

HTTP 400 - messages with tool_calls must include reasoning_content

---

def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> None:
    """Copy provider-facing reasoning fields onto an API replay message."""
    if source_msg.get("role") != "assistant":
        return

    explicit_reasoning = source_msg.get("reasoning_content")
    if isinstance(explicit_reasoning, str):
        api_msg["reasoning_content"] = explicit_reasoning
        return

    normalized_reasoning = source_msg.get("reasoning")
    if isinstance(normalized_reasoning, str) and normalized_reasoning:
        api_msg["reasoning_content"] = normalized_reasoning
        return

    kimi_requires_reasoning = (
        self.provider in {"kimi-coding", "kimi-coding-cn"}
        or base_url_host_matches(self.base_url, "api.kimi.com")
        or base_url_host_matches(self.base_url, "moonshot.ai")
        or base_url_host_matches(self.base_url, "moonshot.cn")
    )
    if kimi_requires_reasoning and source_msg.get("tool_calls"):
        api_msg["reasoning_content"] = ""

---

deepseek_requires_reasoning = (
    self.provider == "deepseek"
    or base_url_host_matches(self.base_url, "api.deepseek.com")
)

if (kimi_requires_reasoning or deepseek_requires_reasoning) and source_msg.get("tool_calls"):
    api_msg["reasoning_content"] = ""
RAW_BUFFERClick to expand / collapse

DeepSeek tool-calling assistant turns missing reasoning_content cause HTTP 400 — Kimi-only compensation should generalise

Summary

run_agent.py::_copy_reasoning_content_for_api includes a special case that injects an empty reasoning_content string on prior assistant turns that contain tool_calls but no reasoning field, but only when the active provider is Kimi/Moonshot. DeepSeek's reasoning-capable models (deepseek-reasoner, deepseek-chat with thinking on) require the same compensation — without it the next turn is rejected with HTTP 400 ("messages with tool_calls must include reasoning_content").

Tested on Hermes Agent v0.11.0.

Reproduction

config.yaml:

model:
  default: deepseek-reasoner
  provider: deepseek

Send a message that triggers a tool call. On the second turn (when the model would normally see its previous tool-call assistant message in the replay) the API returns:

HTTP 400 - messages with tool_calls must include reasoning_content

Switch to kimi-coding with the same conversation shape and the call succeeds — because _copy_reasoning_content_for_api injects the missing field for Kimi.

Root Cause

run_agent.py::_copy_reasoning_content_for_api (around the version shipped in v0.11.0):

def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> None:
    """Copy provider-facing reasoning fields onto an API replay message."""
    if source_msg.get("role") != "assistant":
        return

    explicit_reasoning = source_msg.get("reasoning_content")
    if isinstance(explicit_reasoning, str):
        api_msg["reasoning_content"] = explicit_reasoning
        return

    normalized_reasoning = source_msg.get("reasoning")
    if isinstance(normalized_reasoning, str) and normalized_reasoning:
        api_msg["reasoning_content"] = normalized_reasoning
        return

    kimi_requires_reasoning = (
        self.provider in {"kimi-coding", "kimi-coding-cn"}
        or base_url_host_matches(self.base_url, "api.kimi.com")
        or base_url_host_matches(self.base_url, "moonshot.ai")
        or base_url_host_matches(self.base_url, "moonshot.cn")
    )
    if kimi_requires_reasoning and source_msg.get("tool_calls"):
        api_msg["reasoning_content"] = ""

The compensation block only fires for Kimi/Moonshot endpoints. DeepSeek imposes the same constraint when reasoning mode is active but is not in the allowlist, so the missing-reasoning_content assistant turn slips through and the upstream rejects it.

Suggested Fix

Generalise the compensation to cover DeepSeek when reasoning is enabled:

deepseek_requires_reasoning = (
    self.provider == "deepseek"
    or base_url_host_matches(self.base_url, "api.deepseek.com")
)

if (kimi_requires_reasoning or deepseek_requires_reasoning) and source_msg.get("tool_calls"):
    api_msg["reasoning_content"] = ""

Or, more cleanly, move the allowlist to a module-level constant and add deepseek (plus api.deepseek.com) to it.

Impact

DeepSeek is a popular, low-cost provider for users who want native reasoning + tool use. Without this fix, any DeepSeek-backed Hermes agent that uses tools breaks on the second turn — effectively unusable for anything beyond a single-turn chat.

Workaround

Switch reasoning off (use deepseek-chat without thinking), or route DeepSeek through OpenRouter / a relay that strips the reasoning_content requirement upstream. Both lose the value DeepSeek offers in the first place.

Related

The same pattern likely applies to other reasoning-capable third-party endpoints that mirror the OpenAI Chat Completions schema (Z.AI / GLM, MiniMax M2 thinking, Qwen3 with thinking on, etc.). Each would need a similar allowlist entry — or, better, a unified rule keyed on reasoning_enabled && previous_turn_has_tool_calls && previous_turn_lacks_reasoning_content.

extent analysis

TL;DR

The most likely fix is to generalize the compensation for missing reasoning_content to cover DeepSeek when reasoning is enabled.

Guidance

  • Identify the run_agent.py file and locate the _copy_reasoning_content_for_api function to apply the suggested fix.
  • Update the kimi_requires_reasoning check to include DeepSeek by adding a deepseek_requires_reasoning condition, as shown in the suggested fix.
  • Consider moving the allowlist to a module-level constant to make it easier to add or remove providers in the future.
  • Verify the fix by testing the Hermes agent with DeepSeek and tool calls to ensure the HTTP 400 error is resolved.

Example

deepseek_requires_reasoning = (
    self.provider == "deepseek"
    or base_url_host_matches(self.base_url, "api.deepseek.com")
)

if (kimi_requires_reasoning or deepseek_requires_reasoning) and source_msg.get("tool_calls"):
    api_msg["reasoning_content"] = ""

Notes

This fix assumes that the run_agent.py file and the _copy_reasoning_content_for_api function are correctly implemented and that the suggested fix is applied correctly. Additionally, this fix may need to be adapted for other reasoning-capable providers that mirror the OpenAI Chat Completions schema.

Recommendation

Apply the workaround by generalizing the compensation for missing reasoning_content to cover DeepSeek when reasoning is enabled, as this will allow DeepSeek-backed Hermes agents to function correctly with tool calls.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix DeepSeek tool-calling turns missing reasoning_content cause HTTP 400 — Kimi-only compensation should generalise [4 comments, 5 participants]