mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 09:39:31 +00:00
Merge a31204ef09 into 006948232c
This commit is contained in:
commit
db00e954b0
2 changed files with 97 additions and 5 deletions
|
|
@ -11,7 +11,7 @@ from __future__ import annotations
|
|||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from langchain_core.messages import AIMessage, HumanMessage
|
||||
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
||||
|
||||
|
||||
def _make_model(**kwargs):
|
||||
|
|
@ -184,3 +184,72 @@ def test_positional_fallback_when_count_differs():
|
|||
|
||||
assistant_msg = next(m for m in payload["messages"] if m["role"] == "assistant")
|
||||
assert assistant_msg["reasoning_content"] == "My reasoning"
|
||||
|
||||
|
||||
def test_reasoning_content_injected_when_assistant_has_tool_calls():
|
||||
"""reasoning_content is preserved on assistant messages that also carry tool_calls.
|
||||
|
||||
Regression for issues #3043 and #3050: APIs like Kimi K2.5 and DeepSeek Chat (thinking
|
||||
mode) reject the request with 'reasoning_content missing in assistant tool call message'
|
||||
when reasoning_content is dropped from a tool-call turn.
|
||||
"""
|
||||
model = _make_model()
|
||||
|
||||
human = HumanMessage(content="Search for X")
|
||||
ai_with_tools = AIMessage(
|
||||
content="",
|
||||
tool_calls=[{"id": "call_1", "name": "search", "args": {"q": "X"}, "type": "tool_call"}],
|
||||
additional_kwargs={"reasoning_content": "I need to search for X"},
|
||||
)
|
||||
|
||||
tool_calls_payload = [{"id": "call_1", "type": "function", "function": {"name": "search", "arguments": '{"q": "X"}'}}]
|
||||
base_payload = {
|
||||
"messages": [
|
||||
_make_payload_message("user", "Search for X"),
|
||||
_make_payload_message("assistant", "", tool_calls=tool_calls_payload),
|
||||
]
|
||||
}
|
||||
|
||||
with patch.object(type(model).__bases__[0], "_get_request_payload", return_value=base_payload):
|
||||
with patch.object(model, "_convert_input") as mock_convert:
|
||||
mock_convert.return_value = MagicMock(to_messages=lambda: [human, ai_with_tools])
|
||||
payload = model._get_request_payload([human, ai_with_tools])
|
||||
|
||||
assistant_msg = next(m for m in payload["messages"] if m["role"] == "assistant")
|
||||
assert assistant_msg["reasoning_content"] == "I need to search for X"
|
||||
assert assistant_msg["tool_calls"] == tool_calls_payload
|
||||
|
||||
|
||||
def test_reasoning_content_preserved_across_tool_call_turn():
|
||||
"""reasoning_content survives a full tool-call round-trip (assistant → tool → next turn).
|
||||
|
||||
The payload for the follow-up model call still includes the assistant tool-call message
|
||||
from turn 1; that message must carry reasoning_content or the API rejects the request.
|
||||
"""
|
||||
model = _make_model()
|
||||
|
||||
human = HumanMessage(content="Search for X")
|
||||
ai_with_tools = AIMessage(
|
||||
content="",
|
||||
tool_calls=[{"id": "call_1", "name": "search", "args": {"q": "X"}, "type": "tool_call"}],
|
||||
additional_kwargs={"reasoning_content": "I need to search for X"},
|
||||
)
|
||||
tool_result = ToolMessage(content="Found results for X", tool_call_id="call_1")
|
||||
|
||||
tool_calls_payload = [{"id": "call_1", "type": "function", "function": {"name": "search", "arguments": '{"q": "X"}'}}]
|
||||
base_payload = {
|
||||
"messages": [
|
||||
_make_payload_message("user", "Search for X"),
|
||||
_make_payload_message("assistant", "", tool_calls=tool_calls_payload),
|
||||
{"role": "tool", "content": "Found results for X", "tool_call_id": "call_1"},
|
||||
]
|
||||
}
|
||||
|
||||
with patch.object(type(model).__bases__[0], "_get_request_payload", return_value=base_payload):
|
||||
with patch.object(model, "_convert_input") as mock_convert:
|
||||
mock_convert.return_value = MagicMock(to_messages=lambda: [human, ai_with_tools, tool_result])
|
||||
payload = model._get_request_payload([human, ai_with_tools, tool_result])
|
||||
|
||||
assistant_msg = next(m for m in payload["messages"] if m["role"] == "assistant")
|
||||
assert assistant_msg["reasoning_content"] == "I need to search for X"
|
||||
assert assistant_msg["tool_calls"] == tool_calls_payload
|
||||
|
|
|
|||
|
|
@ -171,9 +171,9 @@ models:
|
|||
# thinking:
|
||||
# type: disabled
|
||||
|
||||
# Example: DeepSeek model (with thinking support)
|
||||
# - name: deepseek-v3
|
||||
# display_name: DeepSeek V3 (Thinking)
|
||||
# Example: DeepSeek Reasoner (deepseek-reasoner / R1) with thinking support
|
||||
# - name: deepseek-reasoner
|
||||
# display_name: DeepSeek Reasoner
|
||||
# use: deerflow.models.patched_deepseek:PatchedChatDeepSeek
|
||||
# model: deepseek-reasoner
|
||||
# api_key: $DEEPSEEK_API_KEY
|
||||
|
|
@ -181,7 +181,30 @@ models:
|
|||
# max_retries: 2
|
||||
# max_tokens: 8192
|
||||
# supports_thinking: true
|
||||
# supports_vision: false # DeepSeek V3 does not support vision
|
||||
# supports_vision: false # DeepSeek Reasoner does not support vision
|
||||
# when_thinking_enabled:
|
||||
# extra_body:
|
||||
# thinking:
|
||||
# type: enabled
|
||||
# when_thinking_disabled:
|
||||
# extra_body:
|
||||
# thinking:
|
||||
# type: disabled
|
||||
|
||||
# Example: DeepSeek Chat (deepseek-chat) with thinking enabled
|
||||
# Use PatchedChatDeepSeek — NOT langchain_deepseek:ChatDeepSeek — to avoid
|
||||
# "reasoning_content must be passed back to the API" errors in multi-turn
|
||||
# tool-call conversations (affects DeepSeek Chat, DeepSeek V4 Flash, etc.).
|
||||
# - name: deepseek-chat
|
||||
# display_name: DeepSeek Chat
|
||||
# use: deerflow.models.patched_deepseek:PatchedChatDeepSeek
|
||||
# model: deepseek-chat
|
||||
# api_key: $DEEPSEEK_API_KEY
|
||||
# timeout: 600.0
|
||||
# max_retries: 2
|
||||
# max_tokens: 8192
|
||||
# supports_thinking: true
|
||||
# supports_vision: false
|
||||
# when_thinking_enabled:
|
||||
# extra_body:
|
||||
# thinking:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue