Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 34 additions & 12 deletions libs/partners/deepseek/langchain_deepseek/chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -261,21 +261,43 @@ def _get_request_payload(
stop: list[str] | None = None,
**kwargs: Any,
) -> dict:
# Get the original messages before conversion to extract reasoning_content
messages = self._convert_input(input_).to_messages()

# Build a map of message index to reasoning_content from additional_kwargs
# This is needed because the parent's _get_request_payload doesn't preserve
# reasoning_content in the converted message dicts
reasoning_content_map: dict[int, str | None] = {}
for i, msg in enumerate(messages):
if isinstance(msg, AIMessage):
reasoning_content = msg.additional_kwargs.get("reasoning_content")
if reasoning_content is not None:
reasoning_content_map[i] = reasoning_content

payload = super()._get_request_payload(input_, stop=stop, **kwargs)
for message in payload["messages"]:
for i, message in enumerate(payload["messages"]):
if message["role"] == "tool" and isinstance(message["content"], list):
message["content"] = json.dumps(message["content"])
elif message["role"] == "assistant" and isinstance(
message["content"], list
):
# DeepSeek API expects assistant content to be a string, not a list.
# Extract text blocks and join them, or use empty string if none exist.
text_parts = [
block.get("text", "")
for block in message["content"]
if isinstance(block, dict) and block.get("type") == "text"
]
message["content"] = "".join(text_parts) if text_parts else ""
elif message["role"] == "assistant":
# DeepSeek reasoner models require reasoning_content in assistant
# messages for multi-turn conversations. Add it from additional_kwargs
# if present. Fixes issue #34166.
if i in reasoning_content_map:
message["reasoning_content"] = reasoning_content_map[i]
# Even if no reasoning_content was captured, DeepSeek API requires
# the field to be present (can be empty string) for reasoner models
elif "deepseek-reasoner" in self.model_name and "reasoning_content" not in message:
message["reasoning_content"] = ""

if isinstance(message["content"], list):
# DeepSeek API expects assistant content to be a string, not a list.
# Extract text blocks and join them, or use empty string if none exist.
text_parts = [
block.get("text", "")
for block in message["content"]
if isinstance(block, dict) and block.get("type") == "text"
]
message["content"] = "".join(text_parts) if text_parts else ""
return payload

def _create_chat_result(
Expand Down
76 changes: 75 additions & 1 deletion libs/partners/deepseek/tests/unit_tests/test_chat_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from typing import Any, Literal
from unittest.mock import MagicMock

from langchain_core.messages import AIMessageChunk, ToolMessage
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage, ToolMessage
from langchain_tests.unit_tests import ChatModelUnitTests
from openai import BaseModel
from openai.types.chat import ChatCompletionMessage
Expand Down Expand Up @@ -244,6 +244,80 @@ def test_get_request_payload(self) -> None:
payload = chat_model._get_request_payload([tool_message])
assert payload["messages"][0]["content"] == "test string"

def test_get_request_payload_preserves_reasoning_content(self) -> None:
"""Test that reasoning_content from AIMessage additional_kwargs is preserved.

This is a regression test for issue #34166 where multi-turn conversations
with deepseek-reasoner failed because reasoning_content was not included
in assistant messages when resending the conversation history.
"""
chat_model = ChatDeepSeek(
model="deepseek-reasoner", api_key=SecretStr("api_key")
)

# Simulate a multi-turn conversation where the first response included
# reasoning_content (stored in additional_kwargs)
messages = [
HumanMessage(content="What is 2 + 2?"),
AIMessage(
content="The answer is 4.",
additional_kwargs={"reasoning_content": "Let me think... 2 + 2 = 4"},
),
HumanMessage(content="And what is 3 + 3?"),
]

payload = chat_model._get_request_payload(messages)

# Verify the assistant message includes reasoning_content
assistant_message = payload["messages"][1]
assert assistant_message["role"] == "assistant"
assert assistant_message["content"] == "The answer is 4."
assert (
assistant_message["reasoning_content"] == "Let me think... 2 + 2 = 4"
), "reasoning_content should be preserved from additional_kwargs"

def test_get_request_payload_adds_empty_reasoning_for_reasoner(self) -> None:
"""Test that empty reasoning_content is added for reasoner models.

DeepSeek API requires the reasoning_content field to be present
in assistant messages when using reasoner models, even if empty.
"""
chat_model = ChatDeepSeek(
model="deepseek-reasoner", api_key=SecretStr("api_key")
)

# AIMessage without reasoning_content
messages = [
HumanMessage(content="Hello"),
AIMessage(content="Hi there!"),
HumanMessage(content="How are you?"),
]

payload = chat_model._get_request_payload(messages)

# Verify the assistant message has reasoning_content (empty string)
assistant_message = payload["messages"][1]
assert assistant_message["role"] == "assistant"
assert "reasoning_content" in assistant_message
assert assistant_message["reasoning_content"] == ""

def test_get_request_payload_no_reasoning_for_non_reasoner(self) -> None:
"""Test that reasoning_content is not added for non-reasoner models."""
chat_model = ChatDeepSeek(model=MODEL_NAME, api_key=SecretStr("api_key"))

messages = [
HumanMessage(content="Hello"),
AIMessage(content="Hi there!"),
HumanMessage(content="How are you?"),
]

payload = chat_model._get_request_payload(messages)

# Verify the assistant message does NOT have reasoning_content
assistant_message = payload["messages"][1]
assert assistant_message["role"] == "assistant"
assert "reasoning_content" not in assistant_message


class SampleTool(PydanticBaseModel):
"""Sample tool schema for testing."""
Expand Down