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..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 @@ -1,3 +1,6 @@ +from copy import deepcopy +from unittest.mock import patch + from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig @@ -103,6 +106,47 @@ async def test_async_transform_request_strips_unsupported_tools_from_body(): assert body["tools"][0]["function"]["name"] == "shell" +def test_transform_request_warns_once_per_replayed_history_and_preserves_history(): + 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."}, + ] + original_messages = deepcopy(messages) + config = DeepSeekChatConfig() + + warning_path = "litellm.llms.deepseek.chat.transformation.litellm.verbose_logger.warning" + with patch(warning_path) as warning: + results = [ + config.transform_request( + model="deepseek-reasoner", + messages=messages, + optional_params={"thinking": {"type": "enabled"}}, + litellm_params={}, + headers={}, + ) + for _ in range(2) + ] + + assert warning.call_count == 2 + for result in results: + assert [result["messages"][index]["reasoning_content"] for index in (1, 3)] == [" ", " "] + assert messages == original_messages + + 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