From 08b94986fc6b9912d33bb97dd5e99b96a11d4d1c Mon Sep 17 00:00:00 2001 From: OSS Agent Date: Thu, 20 Aug 2026 18:47:32 +0200 Subject: [PATCH 1/2] test(deepseek): reproduce per-message reasoning_content warning flood (#37629) --- .../chat/test_deepseek_chat_transformation.py | 90 +++++++++++++++++++ 1 file changed, 90 insertions(+) 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..ebab3b12739 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,6 +1,96 @@ +import logging + +import litellm from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig +class _ListHandler(logging.Handler): + """Capture emitted LogRecords so we can count warnings deterministically.""" + + def __init__(self) -> None: + super().__init__(level=logging.WARNING) + self.records: list[logging.LogRecord] = [] + + def emit(self, record: logging.LogRecord) -> None: + self.records.append(record) + + +def _capture_reasoning_warnings(messages): + """ + Run _fill_reasoning_content while capturing the WARNING records emitted by + litellm.verbose_logger. Returns (result, warning_records). + """ + handler = _ListHandler() + logger = litellm.verbose_logger + previous_level = logger.level + logger.addHandler(handler) + logger.setLevel(logging.WARNING) + try: + result = DeepSeekChatConfig()._fill_reasoning_content(messages) + finally: + logger.removeHandler(handler) + logger.setLevel(previous_level) + reasoning_records = [ + record + for record in handler.records + if "reasoning_content" in record.getMessage() + ] + return result, reasoning_records + + +def test_fill_reasoning_content_warns_once_per_request_with_count(): + """ + Reproduces issue #37629: _fill_reasoning_content used to emit one identical + WARNING per historical assistant message that lacked reasoning_content. In a + multi-turn conversation this floods the logs (6 messages -> 6 warnings on a + single request) and buries genuine errors. + + Expected behaviour: at most ONE aggregated warning per request, and it must + report how many assistant messages were back-filled with the placeholder. + """ + messages = [{"role": "system", "content": "You are helpful."}] + for i in range(6): + messages.append({"role": "user", "content": f"q{i}"}) + messages.append({"role": "assistant", "content": f"a{i}"}) + + result, warning_records = _capture_reasoning_warnings(messages) + + # All six assistant messages still get the single-space placeholder. + assistant_placeholders = [ + msg + for msg in result + if msg.get("role") == "assistant" and msg.get("reasoning_content") == " " + ] + assert len(assistant_placeholders) == 6 + + # Exactly one aggregated warning, not one-per-message. + assert len(warning_records) == 1, ( + f"expected a single aggregated warning, got {len(warning_records)}: " + f"{[r.getMessage() for r in warning_records]}" + ) + # And that warning must surface the count of affected messages. + assert "6" in warning_records[0].getMessage() + + +def test_fill_reasoning_content_no_warning_when_nothing_missing(): + """When every assistant message already carries reasoning_content, the + aggregated warning must not fire at all.""" + messages = [ + {"role": "user", "content": "hi"}, + {"role": "assistant", "content": "hello", "reasoning_content": "thinking"}, + {"role": "user", "content": "again"}, + { + "role": "assistant", + "content": "sure", + "provider_specific_fields": {"reasoning_content": "stored"}, + }, + ] + + _result, warning_records = _capture_reasoning_warnings(messages) + + assert warning_records == [] + + def _function_tool(name: str) -> dict: return { "type": "function", From 4c3f0306f29dba6ba3c792830c08d09e143371c6 Mon Sep 17 00:00:00 2001 From: OSS Agent Date: Thu, 20 Aug 2026 18:55:05 +0200 Subject: [PATCH 2/2] fix(deepseek): aggregate reasoning_content placeholder warning to one per request (#37629) --- litellm/llms/deepseek/chat/transformation.py | 32 +++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/litellm/llms/deepseek/chat/transformation.py b/litellm/llms/deepseek/chat/transformation.py index 566c960333a..7b1bc954ab3 100644 --- a/litellm/llms/deepseek/chat/transformation.py +++ b/litellm/llms/deepseek/chat/transformation.py @@ -72,7 +72,8 @@ class DeepSeekChatConfig(OpenAIGPTConfig): (LiteLLM stores provider-specific response fields there). 2. Otherwise inject a single space — the minimum value the API accepts. """ - result: Final[list[AllMessageValues]] = [] + result: list[AllMessageValues] = [] + placeholder_injections = 0 for msg in messages: if msg.get("role") == "assistant" and not msg.get("reasoning_content"): patched = dict(cast(dict, msg)) @@ -84,20 +85,29 @@ 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." - ) + placeholder_injections += 1 patched["reasoning_content"] = " " result.append(cast(AllMessageValues, patched)) else: result.append(msg) + + # Emit a single aggregated warning per request instead of one per + # affected message. The old per-message warning produced an identical + # line for every historical assistant turn (issue #37629): a 6-message + # conversation logged 6 copies on every request, drowning out real + # errors. Aggregating keeps the diagnostic while reporting the count. + if placeholder_injections: + litellm.verbose_logger.warning( + "DeepSeek thinking mode: %d assistant message(s) were missing " + "`reasoning_content` and none was saved in " + "`provider_specific_fields`. A single-space placeholder was " + "injected for each to satisfy API validation, but the model " + "will receive a blank reasoning chain for those turns, which " + "may silently degrade multi-turn response quality. Preserve " + "`reasoning_content` from the original assistant response when " + "building multi-turn conversation history.", + placeholder_injections, + ) return result @overload