mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-10 09:25:57 +00:00
feat(provider) Add patched MiMo reasoning content support (#3298)
* Add patched MiMo reasoning content support * Clarify MiMo patched model coverage * Remove unused MiMo payload index * Address MiMo review nits
This commit is contained in:
@@ -995,6 +995,41 @@ def test_openai_responses_api_settings_are_passed_to_chatopenai(monkeypatch):
|
||||
assert captured.get("output_version") == "responses/v1"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Provider class path resolution
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model_id", ["mimo-v2.5-pro", "mimo-v2.5", "mimo-v2-flash"])
|
||||
def test_create_chat_model_resolves_patched_mimo_provider(model_id):
|
||||
from deerflow.models.patched_mimo import PatchedChatMiMo
|
||||
|
||||
model = ModelConfig(
|
||||
name=f"{model_id}-thinking",
|
||||
display_name=f"{model_id} Thinking",
|
||||
description=None,
|
||||
use="deerflow.models.patched_mimo:PatchedChatMiMo",
|
||||
model=model_id,
|
||||
api_key="test-key",
|
||||
base_url="https://api.xiaomimimo.com/v1",
|
||||
supports_thinking=True,
|
||||
when_thinking_enabled={"extra_body": {"thinking": {"type": "enabled"}}},
|
||||
supports_vision=False,
|
||||
)
|
||||
cfg = _make_app_config([model])
|
||||
|
||||
chat_model = factory_module.create_chat_model(
|
||||
name=f"{model_id}-thinking",
|
||||
thinking_enabled=True,
|
||||
app_config=cfg,
|
||||
attach_tracing=False,
|
||||
)
|
||||
|
||||
assert isinstance(chat_model, PatchedChatMiMo)
|
||||
assert chat_model.model_name == model_id
|
||||
assert chat_model.extra_body["thinking"]["type"] == "enabled"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Duplicate keyword argument collision (issue #1977)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Tests for deerflow.models.patched_mimo.PatchedChatMiMo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from langchain_core.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
|
||||
|
||||
def _make_model(**kwargs):
|
||||
from deerflow.models.patched_mimo import PatchedChatMiMo
|
||||
|
||||
return PatchedChatMiMo(
|
||||
model="mimo-v2.5-pro",
|
||||
api_key="test-key",
|
||||
base_url="https://api.xiaomimimo.com/v1",
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
def test_is_lc_serializable_returns_true():
|
||||
from deerflow.models.patched_mimo import PatchedChatMiMo
|
||||
|
||||
assert PatchedChatMiMo.is_lc_serializable() is True
|
||||
|
||||
|
||||
def test_lc_secrets_contains_mimo_api_key_mapping():
|
||||
model = _make_model()
|
||||
|
||||
assert model.lc_secrets["api_key"] == "MIMO_API_KEY"
|
||||
assert model.lc_secrets["openai_api_key"] == "MIMO_API_KEY"
|
||||
|
||||
|
||||
def test_reasoning_content_injected_into_assistant_tool_call_message():
|
||||
model = _make_model()
|
||||
|
||||
human = HumanMessage(content="Check Beijing weather.")
|
||||
ai = AIMessage(
|
||||
content="",
|
||||
additional_kwargs={"reasoning_content": "I need to call the weather tool."},
|
||||
)
|
||||
payload_message = {
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_weather",
|
||||
"type": "function",
|
||||
"function": {"name": "get_weather", "arguments": '{"location":"Beijing"}'},
|
||||
}
|
||||
],
|
||||
}
|
||||
base_payload = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "Check Beijing weather."},
|
||||
payload_message,
|
||||
]
|
||||
}
|
||||
|
||||
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])
|
||||
payload = model._get_request_payload([human, ai])
|
||||
|
||||
assert payload["messages"][1]["reasoning_content"] == "I need to call the weather tool."
|
||||
|
||||
|
||||
def test_reasoning_content_is_noop_when_missing():
|
||||
model = _make_model()
|
||||
|
||||
human = HumanMessage(content="hello")
|
||||
ai = AIMessage(content="hi", additional_kwargs={})
|
||||
base_payload = {
|
||||
"messages": [
|
||||
{"role": "user", "content": "hello"},
|
||||
{"role": "assistant", "content": "hi"},
|
||||
]
|
||||
}
|
||||
|
||||
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])
|
||||
payload = model._get_request_payload([human, ai])
|
||||
|
||||
assert "reasoning_content" not in payload["messages"][1]
|
||||
|
||||
|
||||
def test_create_chat_result_maps_message_reasoning_content():
|
||||
model = _make_model()
|
||||
response = {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "The weather is sunny.",
|
||||
"reasoning_content": "The tool returned sunny weather, so answer directly.",
|
||||
"tool_calls": None,
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"model": "mimo-v2.5-pro",
|
||||
}
|
||||
|
||||
result = model._create_chat_result(response)
|
||||
message = result.generations[0].message
|
||||
|
||||
assert message.content == "The weather is sunny."
|
||||
assert message.additional_kwargs["reasoning_content"] == "The tool returned sunny weather, so answer directly."
|
||||
|
||||
|
||||
def test_create_chat_result_reads_reasoning_content_from_message_attribute():
|
||||
model = _make_model()
|
||||
|
||||
class FakeMessage:
|
||||
reasoning_content = "Reasoning stored on the SDK message object."
|
||||
|
||||
class FakeChoice:
|
||||
message = FakeMessage()
|
||||
|
||||
class FakeResponse:
|
||||
choices = [FakeChoice()]
|
||||
|
||||
def model_dump(self, **kwargs):
|
||||
return {
|
||||
"choices": [
|
||||
{
|
||||
"message": {
|
||||
"role": "assistant",
|
||||
"content": "Answer.",
|
||||
},
|
||||
"finish_reason": "stop",
|
||||
}
|
||||
],
|
||||
"model": "mimo-v2.5-pro",
|
||||
}
|
||||
|
||||
result = model._create_chat_result(FakeResponse())
|
||||
|
||||
assert result.generations[0].message.additional_kwargs["reasoning_content"] == "Reasoning stored on the SDK message object."
|
||||
|
||||
|
||||
def test_convert_chunk_to_generation_chunk_preserves_reasoning_deltas():
|
||||
model = _make_model()
|
||||
|
||||
first = model._convert_chunk_to_generation_chunk(
|
||||
{"choices": [{"delta": {"role": "assistant", "reasoning_content": "I need "}}]},
|
||||
AIMessageChunk,
|
||||
{},
|
||||
)
|
||||
second = model._convert_chunk_to_generation_chunk(
|
||||
{"choices": [{"delta": {"reasoning_content": "a tool."}}]},
|
||||
AIMessageChunk,
|
||||
{},
|
||||
)
|
||||
answer = model._convert_chunk_to_generation_chunk(
|
||||
{"choices": [{"delta": {"content": "Done."}, "finish_reason": "stop"}], "model": "mimo-v2.5-pro"},
|
||||
AIMessageChunk,
|
||||
{},
|
||||
)
|
||||
|
||||
assert first is not None
|
||||
assert second is not None
|
||||
assert answer is not None
|
||||
|
||||
combined = first.message + second.message + answer.message
|
||||
|
||||
assert combined.additional_kwargs["reasoning_content"] == "I need a tool."
|
||||
assert combined.content == "Done."
|
||||
Reference in New Issue
Block a user