mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-23 15:19:33 +00:00
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:
parent
4852ab8d0a
commit
b5f49df86a
@ -228,6 +228,15 @@ class ChatDeepSeek(BaseChatOpenAI):
|
|||||||
rtn.generations[0].message.additional_kwargs["reasoning_content"] = (
|
rtn.generations[0].message.additional_kwargs["reasoning_content"] = (
|
||||||
response.choices[0].message.reasoning_content # type: ignore
|
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
|
return rtn
|
||||||
|
|
||||||
@ -244,11 +253,17 @@ class ChatDeepSeek(BaseChatOpenAI):
|
|||||||
)
|
)
|
||||||
if (choices := chunk.get("choices")) and generation_chunk:
|
if (choices := chunk.get("choices")) and generation_chunk:
|
||||||
top = choices[0]
|
top = choices[0]
|
||||||
if reasoning_content := top.get("delta", {}).get("reasoning_content"):
|
|
||||||
if isinstance(generation_chunk.message, AIMessageChunk):
|
if isinstance(generation_chunk.message, AIMessageChunk):
|
||||||
|
if reasoning_content := top.get("delta", {}).get("reasoning_content"):
|
||||||
generation_chunk.message.additional_kwargs["reasoning_content"] = (
|
generation_chunk.message.additional_kwargs["reasoning_content"] = (
|
||||||
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
|
return generation_chunk
|
||||||
|
|
||||||
def _stream(
|
def _stream(
|
||||||
|
@ -40,7 +40,7 @@ class TestChatDeepSeek(ChatModelIntegrationTests):
|
|||||||
def test_reasoning_content() -> None:
|
def test_reasoning_content() -> None:
|
||||||
"""Test reasoning content."""
|
"""Test reasoning content."""
|
||||||
chat_model = ChatDeepSeek(model="deepseek-reasoner")
|
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.content
|
||||||
assert response.additional_kwargs["reasoning_content"]
|
assert response.additional_kwargs["reasoning_content"]
|
||||||
raise ValueError()
|
raise ValueError()
|
||||||
@ -50,7 +50,7 @@ def test_reasoning_content() -> None:
|
|||||||
def test_reasoning_content_streaming() -> None:
|
def test_reasoning_content_streaming() -> None:
|
||||||
chat_model = ChatDeepSeek(model="deepseek-reasoner")
|
chat_model = ChatDeepSeek(model="deepseek-reasoner")
|
||||||
full: Optional[BaseMessageChunk] = None
|
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
|
full = chunk if full is None else full + chunk
|
||||||
assert isinstance(full, AIMessageChunk)
|
assert isinstance(full, AIMessageChunk)
|
||||||
assert full.additional_kwargs["reasoning_content"]
|
assert full.additional_kwargs["reasoning_content"]
|
||||||
|
@ -1,12 +1,60 @@
|
|||||||
"""Test chat model integration."""
|
"""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 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
|
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):
|
class TestChatDeepSeekUnit(ChatModelUnitTests):
|
||||||
@property
|
@property
|
||||||
def chat_model_class(self) -> Type[ChatDeepSeek]:
|
def chat_model_class(self) -> Type[ChatDeepSeek]:
|
||||||
@ -35,3 +83,122 @@ class TestChatDeepSeekUnit(ChatModelUnitTests):
|
|||||||
"model": "deepseek-chat",
|
"model": "deepseek-chat",
|
||||||
"api_key": "api_key",
|
"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
|
||||||
|
Loading…
Reference in New Issue
Block a user