This commit is contained in:
Paco Cartones 2026-08-27 17:10:58 +00:00 committed by GitHub
commit ca53de22ce
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 127 additions and 11 deletions

View file

@ -73,7 +73,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))
@ -85,20 +86,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

View file

@ -1,7 +1,113 @@
import logging
from copy import deepcopy
import litellm
from litellm.llms.deepseek.chat.transformation import DeepSeekChatConfig
class _ListHandler(logging.Handler):
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):
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)
records = [record for record in handler.records if "reasoning_content" in record.getMessage()]
return result, records
def test_fill_reasoning_content_warns_once_per_request_with_count():
messages = [{"role": "system", "content": "You are helpful."}]
for index in range(6):
messages.append({"role": "user", "content": f"q{index}"})
messages.append({"role": "assistant", "content": f"a{index}"})
result, warning_records = _capture_reasoning_warnings(messages)
placeholders = [
message for message in result if message.get("role") == "assistant" and message.get("reasoning_content") == " "
]
assert len(placeholders) == 6
assert len(warning_records) == 1
assert "6 assistant message(s)" in warning_records[0].getMessage()
def test_fill_reasoning_content_does_not_warn_when_nothing_is_missing():
messages = [
{"role": "user", "content": "hi"},
{"role": "assistant", "content": "hello", "reasoning_content": "thinking"},
{
"role": "assistant",
"content": "sure",
"provider_specific_fields": {"reasoning_content": "stored"},
},
]
_result, warning_records = _capture_reasoning_warnings(messages)
assert warning_records == []
def test_transform_request_replays_warn_once_and_preserve_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)
handler = _ListHandler()
logger = litellm.verbose_logger
previous_level = logger.level
logger.addHandler(handler)
logger.setLevel(logging.WARNING)
try:
results = [
DeepSeekChatConfig().transform_request(
model="deepseek-reasoner",
messages=messages,
optional_params={"thinking": {"type": "enabled"}},
litellm_params={},
headers={},
)
for _ in range(2)
]
finally:
logger.removeHandler(handler)
logger.setLevel(previous_level)
warnings = [record for record in handler.records if "reasoning_content" in record.getMessage()]
assert len(warnings) == 2
assert all("2 assistant message(s)" in record.getMessage() for record in warnings)
assert all([result["messages"][index]["reasoning_content"] for index in (1, 3)] == [" ", " "] for result in results)
assert messages == original_messages
def _function_tool(name: str) -> dict:
return {
"type": "function",