fix(agent): extend thinking-mode reasoning_content pad to Kimi/Moonshot

Builds on #16855 (@lsdsjy) which fixed DeepSeek v4 reasoning_content
replay via model_extra fallback + capturing tool_calls at method entry.
Kimi / Moonshot thinking mode enforces the same echo-back contract and
hits the same 400 when a tool-call turn is persisted without
reasoning_content.

- _build_assistant_message: pad branch now uses _needs_thinking_reasoning_pad()
  (DeepSeek OR Kimi) instead of _needs_deepseek_tool_reasoning() alone.
- Extract _needs_thinking_reasoning_pad() and reuse it in
  _copy_reasoning_content_for_api so both sites share one predicate.
- tests/run_agent/test_deepseek_reasoning_content_echo.py: add
  TestBuildAssistantMessagePadsStrictProviders parametrized over DeepSeek
  (attr=None, attr-absent), Kimi (attr=None), Moonshot (via base_url),
  and an OpenRouter negative control that must NOT pad. Proven to fail
  2/5 cases on Kimi/Moonshot without this change.
- scripts/release.py: add AUTHOR_MAP entries for lsdsjy and season179.

Refs #17400.

Co-authored-by: season179 <season.saw@gmail.com>
This commit is contained in:
Teknium
2026-04-30 10:49:07 -07:00
parent b9b9ee3e6c
commit 76edc40ab0
3 changed files with 133 additions and 9 deletions

View File

@@ -8568,11 +8568,14 @@ class AIAgent:
raw_reasoning_content = model_extra["reasoning_content"]
if raw_reasoning_content is not None:
msg["reasoning_content"] = _sanitize_surrogates(raw_reasoning_content)
elif assistant_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
# only when no structured reasoning text was captured.
elif assistant_tool_calls and self._needs_thinking_reasoning_pad():
# DeepSeek v4 thinking mode and Kimi / Moonshot thinking mode
# both require reasoning_content on every assistant tool-call
# message. Without it, replaying the persisted message causes
# HTTP 400 ("The reasoning_content in the thinking mode must
# be passed back to the API"). Include streamed reasoning
# text when captured; otherwise pad with empty string.
# Refs #15250, #17400.
msg["reasoning_content"] = reasoning_text or ""
# Additive fallback (refs #16844, #16884). Streaming-only providers
@@ -8681,6 +8684,18 @@ class AIAgent:
return msg
def _needs_thinking_reasoning_pad(self) -> bool:
"""Return True when the active provider enforces reasoning_content echo-back.
DeepSeek v4 thinking and Kimi / Moonshot thinking both reject replays
of assistant tool-call messages that omit ``reasoning_content`` (refs
#15250, #17400).
"""
return (
self._needs_deepseek_tool_reasoning()
or self._needs_kimi_tool_reasoning()
)
def _needs_kimi_tool_reasoning(self) -> bool:
"""Return True when the current provider is Kimi / Moonshot thinking mode.
@@ -8723,10 +8738,7 @@ class AIAgent:
api_msg["reasoning_content"] = existing
return
needs_thinking_pad = (
self._needs_kimi_tool_reasoning()
or self._needs_deepseek_tool_reasoning()
)
needs_thinking_pad = self._needs_thinking_reasoning_pad()
# 2. Cross-provider poisoned history (#15748): on DeepSeek/Kimi,
# if the source turn has tool_calls AND a 'reasoning' field but no