partner: ChatDeepSeek on openrouter not returning reasoning (#30240)

Deepseek model does not return reasoning when hosted on openrouter
(Issue [30067](https://github.com/langchain-ai/langchain/issues/30067))

the following code did not return reasoning:

```python
llm = ChatDeepSeek( model = 'deepseek/deepseek-r1:nitro', api_base="https://openrouter.ai/api/v1", api_key=os.getenv("OPENROUTER_API_KEY")) 
messages = [
    {"role": "system", "content": "You are an assistant."},
    {"role": "user", "content": "9.11 and 9.8, which is greater? Explain the reasoning behind this decision."}
]
response = llm.invoke(messages, extra_body={"include_reasoning": True})
print(response.content)
print(f"REASONING: {response.additional_kwargs.get('reasoning_content', '')}")
print(response)
```

The fix is to extract reasoning from
response.choices[0].message["model_extra"] and from
choices[0].delta["reasoning"]. and place in response additional_kwargs.
Change is really just the addition of a couple one-sentence if
statements.

---------

Co-authored-by: andrasfe <andrasf94@gmail.com>
Co-authored-by: Chester Curme <chester.curme@gmail.com>
This commit is contained in:
Andras L Ferenczi 2025-03-21 09:35:37 -07:00 committed by GitHub
parent 4852ab8d0a
commit b5f49df86a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 187 additions and 5 deletions

View File

@ -228,6 +228,15 @@ class ChatDeepSeek(BaseChatOpenAI):
rtn.generations[0].message.additional_kwargs["reasoning_content"] = (
response.choices[0].message.reasoning_content # type: ignore
)
# Handle use via OpenRouter
elif hasattr(response.choices[0].message, "model_extra"): # type: ignore
model_extra = response.choices[0].message.model_extra # type: ignore
if isinstance(model_extra, dict) and (
reasoning := model_extra.get("reasoning")
):
rtn.generations[0].message.additional_kwargs["reasoning_content"] = (
reasoning
)
return rtn
@ -244,11 +253,17 @@ class ChatDeepSeek(BaseChatOpenAI):
)
if (choices := chunk.get("choices")) and generation_chunk:
top = choices[0]
if reasoning_content := top.get("delta", {}).get("reasoning_content"):
if isinstance(generation_chunk.message, AIMessageChunk):
if reasoning_content := top.get("delta", {}).get("reasoning_content"):
generation_chunk.message.additional_kwargs["reasoning_content"] = (
reasoning_content
)
# Handle use via OpenRouter
elif reasoning := top.get("delta", {}).get("reasoning"):
generation_chunk.message.additional_kwargs["reasoning_content"] = (
reasoning
)
return generation_chunk
def _stream(

View File

@ -40,7 +40,7 @@ class TestChatDeepSeek(ChatModelIntegrationTests):
def test_reasoning_content() -> None:
"""Test reasoning content."""
chat_model = ChatDeepSeek(model="deepseek-reasoner")
response = chat_model.invoke("What is the square root of 256256?")
response = chat_model.invoke("What is 3^3?")
assert response.content
assert response.additional_kwargs["reasoning_content"]
raise ValueError()
@ -50,7 +50,7 @@ def test_reasoning_content() -> None:
def test_reasoning_content_streaming() -> None:
chat_model = ChatDeepSeek(model="deepseek-reasoner")
full: Optional[BaseMessageChunk] = None
for chunk in chat_model.stream("What is the square root of 256256?"):
for chunk in chat_model.stream("What is 3^3?"):
full = chunk if full is None else full + chunk
assert isinstance(full, AIMessageChunk)
assert full.additional_kwargs["reasoning_content"]

View File

@ -1,12 +1,60 @@
"""Test chat model integration."""
from typing import Type
from typing import Any, Dict, Literal, Type, Union
from unittest.mock import MagicMock
from langchain_core.messages import AIMessageChunk
from langchain_tests.unit_tests import ChatModelUnitTests
from openai import BaseModel
from openai.types.chat import ChatCompletionMessage
from pydantic import SecretStr
from langchain_deepseek.chat_models import ChatDeepSeek
class MockOpenAIResponse(BaseModel):
choices: list
error: None = None
def model_dump(
self,
*,
mode: Union[Literal["json", "python"], str] = "python",
include: Any = None,
exclude: Any = None,
by_alias: bool = False,
exclude_unset: bool = False,
exclude_defaults: bool = False,
exclude_none: bool = False,
round_trip: bool = False,
warnings: Union[Literal["none", "warn", "error"], bool] = True,
context: Union[Dict[str, Any], None] = None,
serialize_as_any: bool = False,
) -> Dict[str, Any]:
choices_list = []
for choice in self.choices:
if isinstance(choice.message, ChatCompletionMessage):
message_dict = choice.message.model_dump()
# Ensure model_extra fields are at top level
if "model_extra" in message_dict:
message_dict.update(message_dict["model_extra"])
else:
message_dict = {
"role": "assistant",
"content": choice.message.content,
}
# Add reasoning_content if present
if hasattr(choice.message, "reasoning_content"):
message_dict["reasoning_content"] = choice.message.reasoning_content
# Add model_extra fields at the top level if present
if hasattr(choice.message, "model_extra"):
message_dict.update(choice.message.model_extra)
message_dict["model_extra"] = choice.message.model_extra
choices_list.append({"message": message_dict})
return {"choices": choices_list, "error": self.error}
class TestChatDeepSeekUnit(ChatModelUnitTests):
@property
def chat_model_class(self) -> Type[ChatDeepSeek]:
@ -35,3 +83,122 @@ class TestChatDeepSeekUnit(ChatModelUnitTests):
"model": "deepseek-chat",
"api_key": "api_key",
}
def get_chat_model(self) -> ChatDeepSeek:
"""Get a chat model instance for testing."""
return ChatDeepSeek(**self.chat_model_params)
class TestChatDeepSeekCustomUnit:
"""Custom tests specific to DeepSeek chat model."""
def test_create_chat_result_with_reasoning_content(self) -> None:
"""Test that reasoning_content is properly extracted from response."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
mock_message = MagicMock()
mock_message.content = "Main content"
mock_message.reasoning_content = "This is the reasoning content"
mock_message.role = "assistant"
mock_response = MockOpenAIResponse(
choices=[MagicMock(message=mock_message)], error=None
)
result = chat_model._create_chat_result(mock_response)
assert (
result.generations[0].message.additional_kwargs.get("reasoning_content")
== "This is the reasoning content"
)
def test_create_chat_result_with_model_extra_reasoning(self) -> None:
"""Test that reasoning is properly extracted from model_extra."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
mock_message = MagicMock(spec=ChatCompletionMessage)
mock_message.content = "Main content"
mock_message.role = "assistant"
mock_message.model_extra = {"reasoning": "This is the reasoning"}
mock_message.model_dump.return_value = {
"role": "assistant",
"content": "Main content",
"model_extra": {"reasoning": "This is the reasoning"},
}
mock_choice = MagicMock()
mock_choice.message = mock_message
mock_response = MockOpenAIResponse(choices=[mock_choice], error=None)
result = chat_model._create_chat_result(mock_response)
assert (
result.generations[0].message.additional_kwargs.get("reasoning_content")
== "This is the reasoning"
)
def test_convert_chunk_with_reasoning_content(self) -> None:
"""Test that reasoning_content is properly extracted from streaming chunk."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
chunk: Dict[str, Any] = {
"choices": [
{
"delta": {
"content": "Main content",
"reasoning_content": "Streaming reasoning content",
}
}
]
}
chunk_result = chat_model._convert_chunk_to_generation_chunk(
chunk, AIMessageChunk, None
)
if chunk_result is None:
raise AssertionError("Expected chunk_result not to be None")
assert (
chunk_result.message.additional_kwargs.get("reasoning_content")
== "Streaming reasoning content"
)
def test_convert_chunk_with_reasoning(self) -> None:
"""Test that reasoning is properly extracted from streaming chunk."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
chunk: Dict[str, Any] = {
"choices": [
{
"delta": {
"content": "Main content",
"reasoning": "Streaming reasoning",
}
}
]
}
chunk_result = chat_model._convert_chunk_to_generation_chunk(
chunk, AIMessageChunk, None
)
if chunk_result is None:
raise AssertionError("Expected chunk_result not to be None")
assert (
chunk_result.message.additional_kwargs.get("reasoning_content")
== "Streaming reasoning"
)
def test_convert_chunk_without_reasoning(self) -> None:
"""Test that chunk without reasoning fields works correctly."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
chunk: Dict[str, Any] = {"choices": [{"delta": {"content": "Main content"}}]}
chunk_result = chat_model._convert_chunk_to_generation_chunk(
chunk, AIMessageChunk, None
)
if chunk_result is None:
raise AssertionError("Expected chunk_result not to be None")
assert chunk_result.message.additional_kwargs.get("reasoning_content") is None
def test_convert_chunk_with_empty_delta(self) -> None:
"""Test that chunk with empty delta works correctly."""
chat_model = ChatDeepSeek(model="deepseek-chat", api_key=SecretStr("api_key"))
chunk: Dict[str, Any] = {"choices": [{"delta": {}}]}
chunk_result = chat_model._convert_chunk_to_generation_chunk(
chunk, AIMessageChunk, None
)
if chunk_result is None:
raise AssertionError("Expected chunk_result not to be None")
assert chunk_result.message.additional_kwargs.get("reasoning_content") is None