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 1/3] 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 From 375ea2483e6e79efa408baad32f00878c589034b Mon Sep 17 00:00:00 2001 From: IvanShang <77005282+qdivan@users.noreply.github.com> Date: Fri, 21 Aug 2026 07:17:05 +0800 Subject: [PATCH 2/3] test(deepseek): cover replayed tool history --- .../chat/test_deepseek_chat_transformation.py | 49 ++++++++++++------- 1 file changed, 31 insertions(+), 18 deletions(-) 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 c9aeeb409c4..9c4ceb5800b 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 @@ -1,3 +1,6 @@ +from copy import deepcopy +from unittest.mock import patch + from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig @@ -103,32 +106,42 @@ 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): +def test_fill_reasoning_content_warns_once_per_request_and_preserves_history(): messages = [ {"role": "user", "content": "Use both tools."}, - {"role": "assistant", "content": None, "tool_calls": [{"id": "first"}], "reasoning_content": ""}, + { + "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": "assistant", + "content": None, + "tool_calls": [{"id": "second"}], + "reasoning_content": "", + }, {"role": "tool", "tool_call_id": "second", "content": "second result"}, {"role": "user", "content": "Continue."}, ] + original_messages = deepcopy(messages) + config = DeepSeekChatConfig() - with caplog.at_level("WARNING"): - first_result = DeepSeekChatConfig()._fill_reasoning_content(messages) - second_result = DeepSeekChatConfig()._fill_reasoning_content(messages) + warning_path = "litellm.llms.deepseek.chat.transformation.litellm.verbose_logger.warning" + with patch(warning_path) as warning: + first_result = config._fill_reasoning_content(messages) + warning.assert_called_once() - 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"] == "" + with patch(warning_path) as warning: + second_result = config._fill_reasoning_content(messages) + warning.assert_called_once() + + assert [first_result[index]["reasoning_content"] for index in (1, 3)] == [" ", " "] + assert [second_result[index]["reasoning_content"] for index in (1, 3)] == [" ", " "] + assert first_result[0] is messages[0] + assert second_result[4] is messages[4] + assert messages == original_messages def test_thinking_mode_active_bool_thinking_returns_false_without_crashing(): From 3376a8464d82d7e6fec60bf36989a692ef380fd2 Mon Sep 17 00:00:00 2001 From: IvanShang <77005282+qdivan@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:21:42 +0800 Subject: [PATCH 3/3] test(deepseek): exercise replay through request transform --- .../chat/test_deepseek_chat_transformation.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) 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 9c4ceb5800b..ff1560ad9b2 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 @@ -106,7 +106,7 @@ 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(): +def test_transform_request_warns_once_per_replayed_history_and_preserves_history(): messages = [ {"role": "user", "content": "Use both tools."}, { @@ -130,17 +130,20 @@ def test_fill_reasoning_content_warns_once_per_request_and_preserves_history(): warning_path = "litellm.llms.deepseek.chat.transformation.litellm.verbose_logger.warning" with patch(warning_path) as warning: - first_result = config._fill_reasoning_content(messages) - warning.assert_called_once() + results = [ + config.transform_request( + model="deepseek-reasoner", + messages=messages, + optional_params={"thinking": {"type": "enabled"}}, + litellm_params={}, + headers={}, + ) + for _ in range(2) + ] - with patch(warning_path) as warning: - second_result = config._fill_reasoning_content(messages) - warning.assert_called_once() - - assert [first_result[index]["reasoning_content"] for index in (1, 3)] == [" ", " "] - assert [second_result[index]["reasoning_content"] for index in (1, 3)] == [" ", " "] - assert first_result[0] is messages[0] - assert second_result[4] is messages[4] + assert warning.call_count == 2 + for result in results: + assert [result["messages"][index]["reasoning_content"] for index in (1, 3)] == [" ", " "] assert messages == original_messages