mirror of
https://github.com/hwchase17/langchain.git
synced 2025-06-22 06:39:52 +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"] = (
|
||||
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(
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user