From 444c4547fca1480aa8b3fef671d5c2e564be2310 Mon Sep 17 00:00:00 2001 From: luyao618 <364939526@qq.com> Date: Wed, 29 Apr 2026 10:25:52 +0800 Subject: [PATCH] fix(agent): send thinking control to DeepSeek direct API to prevent 400 on tool-call replay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DeepSeek's direct API (api.deepseek.com) defaults to thinking=enabled when no explicit thinking parameter is sent. This causes the model to generate reasoning_content in responses, which must be replayed on subsequent API calls. When the user sets reasoning_effort: none, the config was silently ignored because _supports_reasoning_extra_body() returns False for direct DeepSeek — so extra_body.reasoning was never sent. The model defaulted to thinking=enabled, generated reasoning_content, and the next API call failed with 400 'reasoning_content must be passed back'. Add DeepSeek direct API thinking control to ChatCompletionsTransport, following the existing Kimi pattern: emit extra_body.thinking with type 'enabled' or 'disabled' based on reasoning_config. Thread is_deepseek_direct from run_agent.py (detected via provider name or base_url matching api.deepseek.com). OpenRouter DeepSeek path is unchanged — it uses extra_body.reasoning which is a separate parameter handled by the existing supports_reasoning gate. Fixes #17212 --- agent/transports/chat_completions.py | 14 ++++++ run_agent.py | 5 +++ .../agent/transports/test_chat_completions.py | 43 +++++++++++++++++++ 3 files changed, 62 insertions(+) diff --git a/agent/transports/chat_completions.py b/agent/transports/chat_completions.py index 9a115e454731..9a8a0a91fda3 100644 --- a/agent/transports/chat_completions.py +++ b/agent/transports/chat_completions.py @@ -182,6 +182,7 @@ def build_kwargs( is_github_models: bool is_nvidia_nim: bool is_kimi: bool + is_deepseek_direct: bool is_lmstudio: bool is_custom_provider: bool ollama_num_ctx: int | None @@ -353,6 +354,19 @@ def build_kwargs( "type": "enabled" if _kimi_thinking_enabled else "disabled", } + # DeepSeek direct API: extra_body.thinking + is_deepseek_direct = params.get("is_deepseek_direct", False) + if is_deepseek_direct: + _ds_thinking_enabled = True + if reasoning_config and isinstance(reasoning_config, dict): + if reasoning_config.get("enabled") is False: + _ds_thinking_enabled = False + elif (reasoning_config.get("effort") or "").strip().lower() == "none": + _ds_thinking_enabled = False + extra_body["thinking"] = { + "type": "enabled" if _ds_thinking_enabled else "disabled", + } + # Reasoning. LM Studio is handled above via top-level reasoning_effort, # so skip emitting extra_body.reasoning for it. if params.get("supports_reasoning", False) and not params.get("is_lmstudio", False): diff --git a/run_agent.py b/run_agent.py index 8e1549925bd7..84a009231c01 100644 --- a/run_agent.py +++ b/run_agent.py @@ -8410,6 +8410,10 @@ def _build_api_kwargs(self, api_messages: list) -> dict: or base_url_host_matches(self.base_url, "moonshot.ai") or base_url_host_matches(self.base_url, "moonshot.cn") ) + _is_deepseek_direct = ( + (self.provider or "").strip().lower() == "deepseek" + or base_url_host_matches(self.base_url, "api.deepseek.com") + ) _is_tokenhub = base_url_host_matches(self._base_url_lower, "tokenhub.tencentmaas.com") _is_lmstudio = (self.provider or "").strip().lower() == "lmstudio" @@ -8484,6 +8488,7 @@ def _build_api_kwargs(self, api_messages: list) -> dict: is_github_models=_is_gh, is_nvidia_nim=_is_nvidia, is_kimi=_is_kimi, + is_deepseek_direct=_is_deepseek_direct, is_tokenhub=_is_tokenhub, is_lmstudio=_is_lmstudio, is_custom_provider=self.provider == "custom", diff --git a/tests/agent/transports/test_chat_completions.py b/tests/agent/transports/test_chat_completions.py index b8fdced8aa6c..bb0343979a50 100644 --- a/tests/agent/transports/test_chat_completions.py +++ b/tests/agent/transports/test_chat_completions.py @@ -454,6 +454,49 @@ def test_non_moonshot_tools_are_not_mutated(self, transport): assert "type" not in kw["tools"][0]["function"]["parameters"]["properties"]["q"] +class TestChatCompletionsDeepSeekDirect: + """DeepSeek direct API (api.deepseek.com) thinking control via extra_body.""" + + def test_deepseek_thinking_enabled_by_default(self, transport): + kw = transport.build_kwargs( + model="deepseek-chat", messages=[{"role": "user", "content": "Hi"}], + is_deepseek_direct=True, + ) + assert kw["extra_body"]["thinking"] == {"type": "enabled"} + + def test_deepseek_thinking_disabled_when_enabled_false(self, transport): + kw = transport.build_kwargs( + model="deepseek-chat", messages=[{"role": "user", "content": "Hi"}], + is_deepseek_direct=True, + reasoning_config={"enabled": False}, + ) + assert kw["extra_body"]["thinking"] == {"type": "disabled"} + + def test_deepseek_thinking_disabled_when_effort_none(self, transport): + kw = transport.build_kwargs( + model="deepseek-chat", messages=[{"role": "user", "content": "Hi"}], + is_deepseek_direct=True, + reasoning_config={"effort": "none"}, + ) + assert kw["extra_body"]["thinking"] == {"type": "disabled"} + + def test_openrouter_deepseek_no_thinking_param(self, transport): + kw = transport.build_kwargs( + model="deepseek/deepseek-chat", + messages=[{"role": "user", "content": "Hi"}], + is_openrouter=True, + ) + assert "thinking" not in kw.get("extra_body", {}) + + def test_kimi_thinking_unchanged_with_deepseek_present(self, transport): + kw = transport.build_kwargs( + model="kimi-k2", messages=[{"role": "user", "content": "Hi"}], + is_kimi=True, + max_tokens_param_fn=lambda n: {"max_tokens": n}, + ) + assert kw["extra_body"]["thinking"] == {"type": "enabled"} + + class TestChatCompletionsLmStudioReasoning: """LM Studio publishes per-model reasoning ``allowed_options``. When the user requests an effort the model can't honor (e.g. ``high`` on a