From 6abaa6c083d939ab14d94d2a49ce486529377a6c Mon Sep 17 00:00:00 2001 From: IvanShang <77005282+qdivan@users.noreply.github.com> Date: Fri, 21 Aug 2026 01:05:25 +0800 Subject: [PATCH] fix(deepseek): aggregate missing reasoning warnings --- litellm/llms/deepseek/chat/transformation.py | 30 ++++++++++++------- .../chat/test_deepseek_chat_transformation.py | 28 +++++++++++++++++ 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 566c960333a..deac36670c5 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -72,6 +72,15 @@ class DeepSeekChatConfig(OpenAIGPTConfig): (LiteLLM stores provider-specific response fields there). 2. Otherwise inject a single space — the minimum value the API accepts. """ + missing_reasoning_content: Final = any( + msg.get("role") == "assistant" + and not msg.get("reasoning_content") + and not ( + isinstance(provider_fields := msg.get("provider_specific_fields"), dict) + and provider_fields.get("reasoning_content") + ) + for msg in messages + ) result: Final[list[AllMessageValues]] = [] for msg in messages: if msg.get("role") == "assistant" and not msg.get("reasoning_content"): @@ -84,20 +93,21 @@ class DeepSeekChatConfig(OpenAIGPTConfig): cleaned.pop("reasoning_content", None) patched["provider_specific_fields"] = cleaned else: - litellm.verbose_logger.warning( - "DeepSeek thinking mode: assistant message is missing " - "`reasoning_content` and none was saved in " - "`provider_specific_fields`. A single-space placeholder " - "is being injected to satisfy API validation, but the " - "model will receive a blank reasoning chain for this turn, " - "which may silently degrade multi-turn response quality. " - "Preserve `reasoning_content` from the original assistant " - "response when building multi-turn conversation history." - ) patched["reasoning_content"] = " " result.append(cast(AllMessageValues, patched)) else: result.append(msg) + if missing_reasoning_content: + litellm.verbose_logger.warning( + "DeepSeek thinking mode: assistant message is missing " + "`reasoning_content` and none was saved in " + "`provider_specific_fields`. A single-space placeholder " + "is being injected to satisfy API validation, but the " + "model will receive a blank reasoning chain for this turn, " + "which may silently degrade multi-turn response quality. " + "Preserve `reasoning_content` from the original assistant " + "response when building multi-turn conversation history." + ) return result @overload diff --git a/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py b/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py index fa6f23dc7ff..c9aeeb409c4 100644 --- a/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py +++ b/tests/test_litellm/llms/deepseek/chat/test_deepseek_chat_transformation.py @@ -103,6 +103,34 @@ async def test_async_transform_request_strips_unsupported_tools_from_body(): assert body["tools"][0]["function"]["name"] == "shell" +def test_fill_reasoning_content_warns_once_per_request_and_preserves_history(caplog): + messages = [ + {"role": "user", "content": "Use both tools."}, + {"role": "assistant", "content": None, "tool_calls": [{"id": "first"}], "reasoning_content": ""}, + {"role": "tool", "tool_call_id": "first", "content": "first result"}, + {"role": "assistant", "content": None, "tool_calls": [{"id": "second"}], "reasoning_content": ""}, + {"role": "tool", "tool_call_id": "second", "content": "second result"}, + {"role": "user", "content": "Continue."}, + ] + + with caplog.at_level("WARNING"): + first_result = DeepSeekChatConfig()._fill_reasoning_content(messages) + second_result = DeepSeekChatConfig()._fill_reasoning_content(messages) + + warnings = [ + record + for record in caplog.records + if "DeepSeek thinking mode: assistant message is missing `reasoning_content`" in record.message + ] + assert len(warnings) == 2 + assert first_result[1]["reasoning_content"] == " " + assert first_result[3]["reasoning_content"] == " " + assert second_result[1]["reasoning_content"] == " " + assert second_result[3]["reasoning_content"] == " " + assert messages[1]["reasoning_content"] == "" + assert messages[3]["reasoning_content"] == "" + + def test_thinking_mode_active_bool_thinking_returns_false_without_crashing(): config = DeepSeekChatConfig() assert config._thinking_mode_active(model="deepseek-reasoner", optional_params={"thinking": True}) is False