mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: add DeepSeek reasoning_content echo for tool-call messages
DeepSeek V4 thinking mode requires reasoning_content on every assistant message that includes tool_calls. When this field is missing from persisted history, replaying the session causes HTTP 400: 'The reasoning_content in the thinking mode must be passed back to the API.' Two-part fix (refs #15250): 1. _copy_reasoning_content_for_api: Merge the Kimi-only and DeepSeek detection into a single needs_tool_reasoning_echo check. This handles already-poisoned persisted sessions by injecting an empty reasoning_content on replay. 2. _build_assistant_message: Store reasoning_content='' on new DeepSeek tool-call messages at creation time, preventing future session poisoning at the source. Additional fix: 3. _handle_max_iterations: Add missing call to _copy_reasoning_content_for_api in the max-iterations flush path (previously only main loop and flush_memories had it). Detection covers: - provider == 'deepseek' - model name containing 'deepseek' (case-insensitive) - base URL matching api.deepseek.com (for custom provider)
This commit is contained in:
parent
4fade39c90
commit
93a2d6b307
1 changed files with 30 additions and 3 deletions
33
run_agent.py
33
run_agent.py
|
|
@ -7625,6 +7625,12 @@ class AIAgent:
|
|||
raw_reasoning_content = getattr(assistant_message, "reasoning_content", None)
|
||||
if raw_reasoning_content is not None:
|
||||
msg["reasoning_content"] = _sanitize_surrogates(raw_reasoning_content)
|
||||
elif msg.get("tool_calls") and self._needs_deepseek_tool_reasoning():
|
||||
# DeepSeek thinking mode requires reasoning_content on every
|
||||
# assistant tool-call message. Without it, replaying the
|
||||
# persisted message causes HTTP 400. Include empty string
|
||||
# as a defensive compatibility fallback (refs #15250).
|
||||
msg["reasoning_content"] = ""
|
||||
|
||||
if hasattr(assistant_message, 'reasoning_details') and assistant_message.reasoning_details:
|
||||
# Pass reasoning_details back unmodified so providers (OpenRouter,
|
||||
|
|
@ -7700,6 +7706,22 @@ class AIAgent:
|
|||
|
||||
return msg
|
||||
|
||||
def _needs_deepseek_tool_reasoning(self) -> bool:
|
||||
"""Return True when the current provider is DeepSeek thinking mode.
|
||||
|
||||
Used to decide whether to store reasoning_content on tool-call
|
||||
assistant messages. DeepSeek V4 thinking mode requires this field
|
||||
on every assistant tool-call turn; omitting it causes HTTP 400
|
||||
when the message is replayed in a subsequent API request (#15250).
|
||||
"""
|
||||
provider = (self.provider or "").lower()
|
||||
model = (self.model or "").lower()
|
||||
return (
|
||||
provider == "deepseek"
|
||||
or "deepseek" in model
|
||||
or base_url_host_matches(self.base_url, "api.deepseek.com")
|
||||
)
|
||||
|
||||
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":
|
||||
|
|
@ -7715,13 +7737,17 @@ class AIAgent:
|
|||
api_msg["reasoning_content"] = normalized_reasoning
|
||||
return
|
||||
|
||||
kimi_requires_reasoning = (
|
||||
self.provider in {"kimi-coding", "kimi-coding-cn"}
|
||||
provider = (self.provider or "").lower()
|
||||
model = (self.model or "").lower()
|
||||
needs_tool_reasoning_echo = (
|
||||
provider in {"kimi-coding", "kimi-coding-cn", "deepseek"}
|
||||
or "deepseek" in model
|
||||
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")
|
||||
or base_url_host_matches(self.base_url, "api.deepseek.com")
|
||||
)
|
||||
if kimi_requires_reasoning and source_msg.get("tool_calls"):
|
||||
if needs_tool_reasoning_echo and source_msg.get("tool_calls"):
|
||||
api_msg["reasoning_content"] = ""
|
||||
|
||||
@staticmethod
|
||||
|
|
@ -9095,6 +9121,7 @@ class AIAgent:
|
|||
api_messages = []
|
||||
for msg in messages:
|
||||
api_msg = msg.copy()
|
||||
self._copy_reasoning_content_for_api(msg, api_msg)
|
||||
for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"):
|
||||
api_msg.pop(internal_field, None)
|
||||
if _needs_sanitize:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue