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)
|
raw_reasoning_content = getattr(assistant_message, "reasoning_content", None)
|
||||||
if raw_reasoning_content is not None:
|
if raw_reasoning_content is not None:
|
||||||
msg["reasoning_content"] = _sanitize_surrogates(raw_reasoning_content)
|
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:
|
if hasattr(assistant_message, 'reasoning_details') and assistant_message.reasoning_details:
|
||||||
# Pass reasoning_details back unmodified so providers (OpenRouter,
|
# Pass reasoning_details back unmodified so providers (OpenRouter,
|
||||||
|
|
@ -7700,6 +7706,22 @@ class AIAgent:
|
||||||
|
|
||||||
return msg
|
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:
|
def _copy_reasoning_content_for_api(self, source_msg: dict, api_msg: dict) -> None:
|
||||||
"""Copy provider-facing reasoning fields onto an API replay message."""
|
"""Copy provider-facing reasoning fields onto an API replay message."""
|
||||||
if source_msg.get("role") != "assistant":
|
if source_msg.get("role") != "assistant":
|
||||||
|
|
@ -7715,13 +7737,17 @@ class AIAgent:
|
||||||
api_msg["reasoning_content"] = normalized_reasoning
|
api_msg["reasoning_content"] = normalized_reasoning
|
||||||
return
|
return
|
||||||
|
|
||||||
kimi_requires_reasoning = (
|
provider = (self.provider or "").lower()
|
||||||
self.provider in {"kimi-coding", "kimi-coding-cn"}
|
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, "api.kimi.com")
|
||||||
or base_url_host_matches(self.base_url, "moonshot.ai")
|
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, "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"] = ""
|
api_msg["reasoning_content"] = ""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -9095,6 +9121,7 @@ class AIAgent:
|
||||||
api_messages = []
|
api_messages = []
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
api_msg = msg.copy()
|
api_msg = msg.copy()
|
||||||
|
self._copy_reasoning_content_for_api(msg, api_msg)
|
||||||
for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"):
|
for internal_field in ("reasoning", "finish_reason", "_thinking_prefill"):
|
||||||
api_msg.pop(internal_field, None)
|
api_msg.pop(internal_field, None)
|
||||||
if _needs_sanitize:
|
if _needs_sanitize:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue