import importlib.util import sys import unittest from pathlib import Path from types import ModuleType from langchain_core.messages import AIMessage, HumanMessage, ToolMessage def _stub_module(name: str, **attrs): module = sys.modules.get(name) if module is None: module = ModuleType(name) sys.modules[name] = module for key, value in attrs.items(): setattr(module, key, value) return module class _DummyLogger: def __getattr__(self, _name): return lambda *args, **kwargs: None def _build_tool_call(name: str = "search", arguments: str = "{}"): return [ { "id": "call_1", "type": "tool_call", "name": name, "args": {}, } ] class _FakeChatDeepSeek: def __init__(self, model_name: str, model_kwargs: dict | None = None): self.model_name = model_name self.model_kwargs = model_kwargs or {} def _get_request_payload(self, input_, *, stop=None, **kwargs): messages = [] for message in input_: payload_message = { "role": message.type, "content": message.content, } if message.type == "human": payload_message["role"] = "user" elif message.type == "ai": payload_message["role"] = "assistant" tool_calls = getattr(message, "tool_calls", None) if tool_calls: payload_message["tool_calls"] = tool_calls elif message.type == "tool": payload_message["role"] = "tool" payload_message["tool_call_id"] = message.tool_call_id messages.append(payload_message) return {"messages": messages} _ORIGINAL_GET_REQUEST_PAYLOAD = _FakeChatDeepSeek._get_request_payload sys.modules.pop("app.helper.llm", None) _stub_module( "app.core.config", settings=ModuleType("settings"), ) sys.modules["app.core.config"].settings.LLM_PROVIDER = "deepseek" sys.modules["app.core.config"].settings.LLM_MODEL = "deepseek-v4-pro" sys.modules["app.core.config"].settings.LLM_API_KEY = "sk-test" sys.modules["app.core.config"].settings.LLM_BASE_URL = "https://api.deepseek.com" sys.modules["app.core.config"].settings.LLM_THINKING_LEVEL = None sys.modules["app.core.config"].settings.LLM_TEMPERATURE = 0.1 sys.modules["app.core.config"].settings.LLM_MAX_CONTEXT_TOKENS = 64 sys.modules["app.core.config"].settings.PROXY_HOST = None _stub_module("app.log", logger=_DummyLogger()) _stub_module("langchain_deepseek", ChatDeepSeek=_FakeChatDeepSeek) module_path = Path(__file__).resolve().parents[1] / "app" / "helper" / "llm.py" spec = importlib.util.spec_from_file_location("test_llm_module_for_deepseek_compat", module_path) llm_module = importlib.util.module_from_spec(spec) assert spec and spec.loader spec.loader.exec_module(llm_module) class DeepSeekCompatPatchTest(unittest.TestCase): def setUp(self): _FakeChatDeepSeek._get_request_payload = _ORIGINAL_GET_REQUEST_PAYLOAD if hasattr(_FakeChatDeepSeek, "_moviepilot_reasoning_content_patched"): delattr(_FakeChatDeepSeek, "_moviepilot_reasoning_content_patched") llm_module._patch_deepseek_reasoning_content_support() def test_injects_reasoning_content_for_assistant_tool_calls(self): llm = _FakeChatDeepSeek("deepseek-v4-pro") messages = [ HumanMessage(content="天气如何?"), AIMessage( content="", tool_calls=_build_tool_call(), additional_kwargs={"reasoning_content": "先调用天气工具"}, ), ToolMessage(content="晴天", tool_call_id="call_1"), ] payload = llm._get_request_payload(messages) self.assertEqual( payload["messages"][1]["reasoning_content"], "先调用天气工具", ) def test_falls_back_to_empty_reasoning_content_when_missing(self): llm = _FakeChatDeepSeek("deepseek-v4-flash") messages = [ HumanMessage(content="天气如何?"), AIMessage(content="", tool_calls=_build_tool_call()), ToolMessage(content="晴天", tool_call_id="call_1"), ] payload = llm._get_request_payload(messages) self.assertIn("reasoning_content", payload["messages"][1]) self.assertEqual(payload["messages"][1]["reasoning_content"], "") def test_skips_injection_when_thinking_is_disabled(self): llm = _FakeChatDeepSeek( "deepseek-v4-pro", model_kwargs={"extra_body": {"thinking": {"type": "disabled"}}}, ) messages = [ HumanMessage(content="天气如何?"), AIMessage( content="", tool_calls=_build_tool_call(), additional_kwargs={"reasoning_content": "先调用天气工具"}, ), ToolMessage(content="晴天", tool_call_id="call_1"), ] payload = llm._get_request_payload(messages) self.assertNotIn("reasoning_content", payload["messages"][1])