mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-14 23:21:35 +00:00
Fix DeepSeek V4 reasoning_content in multi-turn chat
This commit is contained in:
parent
3d2b8fed32
commit
a3dbffa8e7
6 changed files with 142 additions and 1 deletions
|
|
@ -56,6 +56,7 @@ from litellm.types.utils import (
|
|||
from litellm.utils import convert_to_model_response_object
|
||||
|
||||
from ..common_utils import OpenAIError
|
||||
from ..common_utils import patch_deepseek_v4_reasoning_messages
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from litellm.litellm_core_utils.litellm_logging import Logging as _LiteLLMLoggingObj
|
||||
|
|
@ -169,6 +170,8 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
] # works across all models
|
||||
|
||||
model_specific_params = []
|
||||
if "deepseek" in model.lower():
|
||||
model_specific_params.extend(["thinking", "reasoning_effort"])
|
||||
if (
|
||||
model != "gpt-3.5-turbo-16k" and model != "gpt-4"
|
||||
): # gpt-4 does not support 'response_format'
|
||||
|
|
@ -435,6 +438,7 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
dict: The transformed request. Sent as the body of the API call.
|
||||
"""
|
||||
messages = self._transform_messages(messages=messages, model=model)
|
||||
messages = patch_deepseek_v4_reasoning_messages(model=model, messages=messages)
|
||||
messages, tools = self.remove_cache_control_flag_from_messages_and_tools(
|
||||
model=model, messages=messages, tools=optional_params.get("tools", [])
|
||||
)
|
||||
|
|
@ -460,6 +464,9 @@ class OpenAIGPTConfig(BaseLLMModelInfo, BaseConfig):
|
|||
transformed_messages = await self._transform_messages(
|
||||
messages=messages, model=model, is_async=True
|
||||
)
|
||||
transformed_messages = patch_deepseek_v4_reasoning_messages(
|
||||
model=model, messages=transformed_messages
|
||||
)
|
||||
(
|
||||
transformed_messages,
|
||||
tools,
|
||||
|
|
|
|||
|
|
@ -258,6 +258,43 @@ class BaseOpenAILLM:
|
|||
class OpenAICredentials(NamedTuple):
|
||||
api_base: str
|
||||
api_key: Optional[str]
|
||||
|
||||
|
||||
def requires_deepseek_v4_reasoning_content(model: Optional[str]) -> bool:
|
||||
"""Return True when the model requires DeepSeek V4 thinking history."""
|
||||
if not model:
|
||||
return False
|
||||
|
||||
normalized_model = model.lower()
|
||||
if normalized_model.startswith("responses/"):
|
||||
normalized_model = normalized_model.split("responses/", 1)[1]
|
||||
|
||||
return "deepseek-v4" in normalized_model
|
||||
|
||||
|
||||
def patch_deepseek_v4_reasoning_messages(
|
||||
model: Optional[str], messages: List[Any]
|
||||
) -> List[Any]:
|
||||
"""
|
||||
Ensure assistant tool-call messages include reasoning_content for DeepSeek V4.
|
||||
|
||||
DeepSeek V4 rejects multi-turn requests when prior assistant tool-call messages
|
||||
omit the reasoning_content field, even if the value is empty.
|
||||
"""
|
||||
if not requires_deepseek_v4_reasoning_content(model):
|
||||
return messages
|
||||
|
||||
for message in messages:
|
||||
if not isinstance(message, dict):
|
||||
continue
|
||||
if message.get("role") != "assistant":
|
||||
continue
|
||||
if not (message.get("tool_calls") or message.get("tool_call_id")):
|
||||
continue
|
||||
if message.get("reasoning_content") is None:
|
||||
message["reasoning_content"] = ""
|
||||
|
||||
return messages
|
||||
organization: Optional[str]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -58,6 +58,7 @@ from .chat.o_series_transformation import OpenAIOSeriesConfig
|
|||
from .common_utils import (
|
||||
BaseOpenAILLM,
|
||||
OpenAIError,
|
||||
patch_deepseek_v4_reasoning_messages,
|
||||
drop_params_from_unprocessable_entity_error,
|
||||
)
|
||||
|
||||
|
|
@ -267,6 +268,7 @@ class OpenAIConfig(BaseConfig):
|
|||
headers: dict,
|
||||
) -> dict:
|
||||
messages = self._transform_messages(messages=messages, model=model)
|
||||
messages = patch_deepseek_v4_reasoning_messages(model=model, messages=messages)
|
||||
return {"model": model, "messages": messages, **optional_params}
|
||||
|
||||
def transform_response(
|
||||
|
|
@ -433,6 +435,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
- call chat.completions.create by default
|
||||
"""
|
||||
start_time = time.time()
|
||||
data["messages"] = patch_deepseek_v4_reasoning_messages(
|
||||
model=data.get("model"), messages=data.get("messages", [])
|
||||
)
|
||||
try:
|
||||
raw_response = (
|
||||
await openai_aclient.chat.completions.with_raw_response.create(
|
||||
|
|
@ -474,6 +479,9 @@ class OpenAIChatCompletion(BaseLLM, BaseOpenAILLM):
|
|||
- call chat.completions.create by default
|
||||
"""
|
||||
raw_response = None
|
||||
data["messages"] = patch_deepseek_v4_reasoning_messages(
|
||||
model=data.get("model"), messages=data.get("messages", [])
|
||||
)
|
||||
try:
|
||||
raw_response = openai_client.chat.completions.with_raw_response.create(
|
||||
**data, timeout=timeout
|
||||
|
|
|
|||
|
|
@ -1221,7 +1221,7 @@ class Message(SafeAttributeModel, OpenAIObject):
|
|||
if hasattr(self, "annotations"):
|
||||
del self.annotations
|
||||
|
||||
if reasoning_content is None:
|
||||
if reasoning_content is None and not getattr(self, "tool_calls", None):
|
||||
# ensure default response matches OpenAI spec
|
||||
if hasattr(self, "reasoning_content"):
|
||||
del self.reasoning_content
|
||||
|
|
|
|||
|
|
@ -176,3 +176,68 @@ def test_completion_cost_deepseek():
|
|||
pass
|
||||
except Exception as e:
|
||||
pytest.fail(f"Error occurred: {e}")
|
||||
|
||||
|
||||
def test_deepseek_v4_supported_openai_params_include_thinking_controls():
|
||||
from litellm.llms.openai.chat.gpt_transformation import OpenAIGPTConfig
|
||||
|
||||
supported_params = OpenAIGPTConfig().get_supported_openai_params(
|
||||
"deepseek/deepseek-v4-pro"
|
||||
)
|
||||
|
||||
assert "thinking" in supported_params
|
||||
assert "reasoning_effort" in supported_params
|
||||
|
||||
|
||||
def test_deepseek_v4_transform_request_injects_reasoning_content_for_tool_calls():
|
||||
from litellm.llms.openai.openai import OpenAIConfig
|
||||
|
||||
request = OpenAIConfig().transform_request(
|
||||
model="deepseek/deepseek-v4-pro",
|
||||
messages=[
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert request["messages"][1]["reasoning_content"] == ""
|
||||
|
||||
|
||||
def test_deepseek_reasoner_transform_request_does_not_inject_reasoning_content():
|
||||
from litellm.llms.openai.openai import OpenAIConfig
|
||||
|
||||
request = OpenAIConfig().transform_request(
|
||||
model="deepseek/deepseek-reasoner",
|
||||
messages=[
|
||||
{"role": "user", "content": "hello"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
optional_params={},
|
||||
litellm_params={},
|
||||
headers={},
|
||||
)
|
||||
|
||||
assert "reasoning_content" not in request["messages"][1]
|
||||
|
|
|
|||
|
|
@ -310,3 +310,27 @@ def test_delta_maps_reasoning_to_reasoning_content():
|
|||
# When neither is present, reasoning_content is not set (OpenAI spec)
|
||||
delta4 = Delta(content="hello")
|
||||
assert not hasattr(delta4, "reasoning_content")
|
||||
|
||||
|
||||
def test_message_keeps_reasoning_content_slot_for_tool_calls():
|
||||
"""
|
||||
DeepSeek V4 requires reasoning_content to remain available on assistant
|
||||
tool-call messages so later request transforms can re-inject it.
|
||||
"""
|
||||
from litellm.types.utils import Message
|
||||
|
||||
message = Message(
|
||||
role="assistant",
|
||||
content=None,
|
||||
tool_calls=[
|
||||
{
|
||||
"id": "call_1",
|
||||
"type": "function",
|
||||
"function": {"name": "lookup", "arguments": "{}"},
|
||||
}
|
||||
],
|
||||
reasoning_content=None,
|
||||
)
|
||||
|
||||
assert hasattr(message, "reasoning_content")
|
||||
assert message.reasoning_content is None
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue