mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-25 00:51:20 +00:00
fix: preserve deepseek-chat default thinking behavior
This commit is contained in:
parent
1d38b0f888
commit
44dd879536
3 changed files with 87 additions and 24 deletions
|
|
@ -242,10 +242,18 @@ class ChatCompletionsTransport(ProviderTransport):
|
|||
# DeepSeek: thinking mode toggle and effort mapping
|
||||
is_deepseek = params.get("is_deepseek", False)
|
||||
if is_deepseek:
|
||||
_ds_thinking_enabled = True
|
||||
# Legacy ``deepseek-chat`` is the non-thinking alias; the V4
|
||||
# family and ``deepseek-reasoner`` default to thinking mode.
|
||||
_ds_default_thinking = model_lower != "deepseek-chat"
|
||||
_ds_thinking_enabled = _ds_default_thinking
|
||||
_ds_has_explicit_toggle = False
|
||||
if reasoning_config and isinstance(reasoning_config, dict):
|
||||
if reasoning_config.get("enabled") is False:
|
||||
_ds_thinking_enabled = False
|
||||
_ds_has_explicit_toggle = True
|
||||
elif reasoning_config.get("enabled") is True or reasoning_config.get("effort"):
|
||||
_ds_thinking_enabled = True
|
||||
_ds_has_explicit_toggle = True
|
||||
if _ds_thinking_enabled:
|
||||
# DeepSeek only supports "high" and "max" effort values.
|
||||
# Map low/medium/high → "high", xhigh/max → "max".
|
||||
|
|
@ -260,7 +268,7 @@ class ChatCompletionsTransport(ProviderTransport):
|
|||
# frequency_penalty when thinking is enabled.
|
||||
for _k in ("temperature", "top_p", "presence_penalty", "frequency_penalty"):
|
||||
api_kwargs.pop(_k, None)
|
||||
else:
|
||||
elif _ds_default_thinking or _ds_has_explicit_toggle:
|
||||
extra_body["thinking"] = {"type": "disabled"}
|
||||
|
||||
# Reasoning
|
||||
|
|
|
|||
34
run_agent.py
34
run_agent.py
|
|
@ -7731,20 +7731,28 @@ class AIAgent:
|
|||
return
|
||||
|
||||
# DeepSeek thinking mode requires reasoning_content on ALL assistant
|
||||
# messages — not just tool_calls turns. Empty string is valid.
|
||||
# Scope to native DeepSeek API and OpenRouter-routed DeepSeek.
|
||||
deepseek_requires_reasoning = (
|
||||
base_url_host_matches(self.base_url, "api.deepseek.com")
|
||||
or (
|
||||
self._is_openrouter_url()
|
||||
and (self.model or "").lower().startswith("deepseek/")
|
||||
)
|
||||
)
|
||||
if deepseek_requires_reasoning:
|
||||
rc = self.reasoning_config
|
||||
if isinstance(rc, dict) and rc.get("enabled") is False:
|
||||
# messages — not just tool_calls turns. Empty string is valid.
|
||||
#
|
||||
# Native DeepSeek keeps ``deepseek-chat`` as the legacy non-thinking
|
||||
# alias, while V4 models and ``deepseek-reasoner`` default to
|
||||
# thinking. Preserve that distinction so enabling native DeepSeek
|
||||
# support does not silently change ``deepseek-chat`` semantics.
|
||||
_model_lower = (self.model or "").lower()
|
||||
_deepseek_native = base_url_host_matches(self.base_url, "api.deepseek.com")
|
||||
_deepseek_openrouter = self._is_openrouter_url() and _model_lower.startswith("deepseek/")
|
||||
if _deepseek_native or _deepseek_openrouter:
|
||||
rc = self.reasoning_config if isinstance(self.reasoning_config, dict) else {}
|
||||
if rc.get("enabled") is False:
|
||||
return
|
||||
api_msg["reasoning_content"] = ""
|
||||
_deepseek_requires_reasoning = _deepseek_openrouter
|
||||
if _deepseek_native:
|
||||
_deepseek_requires_reasoning = (
|
||||
_model_lower != "deepseek-chat"
|
||||
or rc.get("enabled") is True
|
||||
or bool(rc.get("effort"))
|
||||
)
|
||||
if _deepseek_requires_reasoning:
|
||||
api_msg["reasoning_content"] = ""
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_tool_calls_for_strict_api(api_msg: dict) -> dict:
|
||||
|
|
|
|||
|
|
@ -59,16 +59,22 @@ class TestDeepSeekV4ContextWindows(unittest.TestCase):
|
|||
class TestDeepSeekThinkingMode(unittest.TestCase):
|
||||
"""Verify build_kwargs handles DeepSeek thinking mode correctly."""
|
||||
|
||||
def _build(self, reasoning_config=None, is_deepseek=True, temperature=0.7):
|
||||
def _build(
|
||||
self,
|
||||
reasoning_config=None,
|
||||
is_deepseek=True,
|
||||
model="deepseek-v4-pro",
|
||||
fixed_temperature=0.7,
|
||||
):
|
||||
transport = ChatCompletionsTransport.__new__(ChatCompletionsTransport)
|
||||
kwargs = transport.build_kwargs(
|
||||
model="deepseek-v4-pro",
|
||||
model=model,
|
||||
messages=[{"role": "user", "content": "Hello"}],
|
||||
tools=None,
|
||||
is_deepseek=is_deepseek,
|
||||
reasoning_config=reasoning_config,
|
||||
model_lower="deepseek-v4-pro",
|
||||
temperature=temperature,
|
||||
model_lower=model.lower(),
|
||||
fixed_temperature=fixed_temperature,
|
||||
)
|
||||
return kwargs
|
||||
|
||||
|
|
@ -106,7 +112,7 @@ class TestDeepSeekThinkingMode(unittest.TestCase):
|
|||
|
||||
def test_temperature_stripped_when_thinking_enabled(self):
|
||||
"""DeepSeek rejects temperature when thinking is enabled."""
|
||||
kwargs = self._build(temperature=0.7)
|
||||
kwargs = self._build(fixed_temperature=0.7)
|
||||
self.assertNotIn("temperature", kwargs)
|
||||
|
||||
def test_non_deepseek_not_affected(self):
|
||||
|
|
@ -119,11 +125,29 @@ class TestDeepSeekThinkingMode(unittest.TestCase):
|
|||
"""When thinking is disabled, temperature should be preserved."""
|
||||
kwargs = self._build(
|
||||
reasoning_config={"enabled": False},
|
||||
temperature=0.7,
|
||||
fixed_temperature=0.7,
|
||||
)
|
||||
# Temperature should not be stripped when thinking is disabled
|
||||
# (The transport may or may not set temperature — the key point
|
||||
# is that the DeepSeek block does not strip it)
|
||||
self.assertEqual(kwargs.get("temperature"), 0.7)
|
||||
|
||||
def test_deepseek_chat_does_not_force_thinking(self):
|
||||
"""Legacy deepseek-chat should stay on its non-thinking default."""
|
||||
kwargs = self._build(model="deepseek-chat")
|
||||
extra = kwargs.get("extra_body", {})
|
||||
self.assertNotIn("thinking", extra)
|
||||
self.assertNotIn("reasoning_effort", kwargs)
|
||||
self.assertEqual(kwargs.get("temperature"), 0.7)
|
||||
|
||||
def test_deepseek_chat_can_opt_in_to_thinking(self):
|
||||
"""Explicit reasoning config should enable thinking for deepseek-chat."""
|
||||
kwargs = self._build(
|
||||
model="deepseek-chat",
|
||||
reasoning_config={"enabled": True, "effort": "xhigh"},
|
||||
fixed_temperature=0.7,
|
||||
)
|
||||
extra = kwargs.get("extra_body", {})
|
||||
self.assertEqual(extra.get("thinking", {}).get("type"), "enabled")
|
||||
self.assertEqual(kwargs.get("reasoning_effort"), "max")
|
||||
self.assertNotIn("temperature", kwargs)
|
||||
|
||||
|
||||
class TestDeepSeekReasoningContentReplay(unittest.TestCase):
|
||||
|
|
@ -197,6 +221,29 @@ class TestDeepSeekReasoningContentReplay(unittest.TestCase):
|
|||
)
|
||||
self.assertNotIn("reasoning_content", api_msg)
|
||||
|
||||
def test_native_deepseek_chat_does_not_inject_by_default(self):
|
||||
"""Legacy non-thinking deepseek-chat should not replay reasoning_content."""
|
||||
agent = self._make_agent(model="deepseek-chat")
|
||||
api_msg = {}
|
||||
agent._copy_reasoning_content_for_api(
|
||||
{"role": "assistant", "content": "Hi"},
|
||||
api_msg,
|
||||
)
|
||||
self.assertNotIn("reasoning_content", api_msg)
|
||||
|
||||
def test_native_deepseek_chat_injects_when_enabled(self):
|
||||
"""deepseek-chat should replay reasoning_content once thinking is enabled."""
|
||||
agent = self._make_agent(
|
||||
model="deepseek-chat",
|
||||
reasoning_config={"enabled": True, "effort": "high"},
|
||||
)
|
||||
api_msg = {}
|
||||
agent._copy_reasoning_content_for_api(
|
||||
{"role": "assistant", "content": "Hi"},
|
||||
api_msg,
|
||||
)
|
||||
self.assertEqual(api_msg.get("reasoning_content"), "")
|
||||
|
||||
def test_non_assistant_skipped(self):
|
||||
"""Non-assistant messages should be skipped entirely."""
|
||||
agent = self._make_agent()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue