mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 01:41:43 +00:00
On provider switches mid-session (e.g. MiniMax -> DeepSeek), the source assistant turn carries a 'reasoning' field written by the prior provider but no 'reasoning_content' key. _copy_reasoning_content_for_api would promote that foreign 'reasoning' to 'reasoning_content' on the outbound DeepSeek request, leaking a cross-provider chain of thought and in practice causing HTTP 400. DeepSeek's own _build_assistant_message always pins reasoning_content='' at creation time for tool-call turns, so the shape (reasoning set, reasoning_content absent, tool_calls present) is unreachable from same-provider DeepSeek history — it can only come from a prior provider. Pad with '' in that case instead of promoting. Healthy same-provider 'reasoning' promotion (no tool_calls, or on providers that do not require the empty-string pin) is unchanged.
This commit is contained in:
parent
65f648ee84
commit
ee1a07f9e9
2 changed files with 73 additions and 25 deletions
|
|
@ -109,17 +109,59 @@ class TestCopyReasoningContentForApi:
|
|||
assert api_msg["reasoning_content"] == "<think>real chain of thought</think>"
|
||||
|
||||
def test_deepseek_reasoning_field_promoted(self) -> None:
|
||||
"""When only 'reasoning' is set, it gets promoted to reasoning_content."""
|
||||
"""When only 'reasoning' is set (no tool_calls), it gets promoted to reasoning_content.
|
||||
|
||||
On DeepSeek/Kimi, tool-call turns with 'reasoning' but no
|
||||
'reasoning_content' are treated as cross-provider poisoned history
|
||||
(#15748) and padded with "" instead of promoted. Same-provider
|
||||
DeepSeek tool-call turns always have reasoning_content pinned at
|
||||
creation time by _build_assistant_message, so the (reasoning-set,
|
||||
reasoning_content-absent, tool_calls-present) shape is unreachable
|
||||
from same-provider history.
|
||||
"""
|
||||
agent = _make_agent(provider="deepseek", model="deepseek-v4-flash")
|
||||
source = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning": "thought trace",
|
||||
"tool_calls": [{"id": "c1", "function": {"name": "terminal"}}],
|
||||
}
|
||||
api_msg: dict = {}
|
||||
agent._copy_reasoning_content_for_api(source, api_msg)
|
||||
assert api_msg["reasoning_content"] == "thought trace"
|
||||
|
||||
def test_deepseek_poisoned_cross_provider_history_padded(self) -> None:
|
||||
"""Cross-provider tool-call turn (#15748): MiniMax reasoning leaks
|
||||
to DeepSeek/Kimi request.
|
||||
|
||||
If the source turn has tool_calls AND a 'reasoning' field but NO
|
||||
'reasoning_content' key, it's from a prior provider (the DeepSeek
|
||||
build path always pins reasoning_content="" at creation). Inject
|
||||
"" instead of forwarding the prior provider's chain of thought.
|
||||
"""
|
||||
agent = _make_agent(provider="deepseek", model="deepseek-v4-flash")
|
||||
source = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning": "MiniMax chain of thought from a prior turn",
|
||||
"tool_calls": [{"id": "c1", "function": {"name": "terminal"}}],
|
||||
}
|
||||
api_msg: dict = {}
|
||||
agent._copy_reasoning_content_for_api(source, api_msg)
|
||||
assert api_msg["reasoning_content"] == ""
|
||||
|
||||
def test_kimi_poisoned_cross_provider_history_padded(self) -> None:
|
||||
"""Kimi path of #15748 — same rule as DeepSeek."""
|
||||
agent = _make_agent(provider="kimi-coding", model="kimi-k2.5")
|
||||
source = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"reasoning": "DeepSeek chain of thought from a prior turn",
|
||||
"tool_calls": [{"id": "c1", "function": {"name": "terminal"}}],
|
||||
}
|
||||
api_msg: dict = {}
|
||||
agent._copy_reasoning_content_for_api(source, api_msg)
|
||||
assert api_msg["reasoning_content"] == ""
|
||||
|
||||
def test_kimi_path_still_works(self) -> None:
|
||||
"""Existing Kimi detection still pads reasoning_content."""
|
||||
agent = _make_agent(provider="kimi-coding", model="kimi-k2.5")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue