feat: 增加 MoviePilot 选项 (#6212)

This commit is contained in:
jxxghp
2026-07-30 13:33:08 +08:00
committed by GitHub
parent cf80b551f9
commit 33a97eb2c8
10 changed files with 273 additions and 5 deletions

View File

@@ -78,6 +78,7 @@ async def test_agent_bundle_signature_changes_with_temperature(monkeypatch) -> N
"user_agent": None,
"use_proxy": False,
"thinking_level": "off",
"api_protocol": "auto",
}
with patch.object(

View File

@@ -45,3 +45,42 @@ def test_resolve_llm_runtime_config_prefers_plugin_thinking_level(monkeypatch) -
runtime_config = asyncio.run(agent._resolve_llm_runtime_config())
assert runtime_config["thinking_level"] == "high"
def test_resolve_llm_runtime_config_uses_system_api_protocol(monkeypatch) -> None:
"""插件未提供 API 协议时应使用系统配置。"""
monkeypatch.setattr(settings, "LLM_API_PROTOCOL", "responses")
agent = MoviePilotAgent(session_id="api-protocol-default", user_id="user-1")
async def return_empty_config(event_type, event_data):
"""模拟插件未返回有效运行时配置。"""
assert event_type == ChainEventType.AgentLLMProvider
assert event_data.api_protocol == "responses"
return SimpleNamespace(event_data=AgentLLMProviderEventData())
with patch(
"app.agent.eventmanager.async_send_event",
new=AsyncMock(side_effect=return_empty_config),
):
runtime_config = asyncio.run(agent._resolve_llm_runtime_config())
assert runtime_config["api_protocol"] == "responses"
def test_resolve_llm_runtime_config_prefers_plugin_api_protocol(monkeypatch) -> None:
"""插件显式覆盖 API 协议时应优先使用插件值。"""
monkeypatch.setattr(settings, "LLM_API_PROTOCOL", "responses")
agent = MoviePilotAgent(session_id="api-protocol-plugin", user_id="user-1")
async def override_api_protocol(_event_type, event_data):
"""模拟插件覆盖 API 协议。"""
event_data.api_protocol = "chat_completions"
return SimpleNamespace(event_data=event_data)
with patch(
"app.agent.eventmanager.async_send_event",
new=AsyncMock(side_effect=override_api_protocol),
):
runtime_config = asyncio.run(agent._resolve_llm_runtime_config())
assert runtime_config["api_protocol"] == "chat_completions"

View File

@@ -87,6 +87,7 @@ def test_initialize_llm_uses_chain_event_selection(monkeypatch) -> None:
user_agent="AgentTokens-UA/1.0",
use_proxy=True,
thinking_level="xhigh",
api_protocol="auto",
)
assert agent._llm_provider_selection["selected_provider_id"] == "provider-1"

View File

@@ -130,6 +130,7 @@ _config_stub.settings = SimpleNamespace(
LLM_BASE_URL_PRESET=None,
LLM_USER_AGENT=None,
LLM_THINKING_LEVEL=None,
LLM_API_PROTOCOL="auto",
LLM_TEMPERATURE=0.1,
LLM_MAX_CONTEXT_TOKENS=64,
LLM_USE_PROXY=True,
@@ -243,6 +244,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
base_url_preset="deepseek-default",
user_agent=None,
use_proxy=None,
api_protocol=None,
)
self.assertEqual(result["provider"], "deepseek")
self.assertEqual(result["model"], "deepseek-chat")
@@ -870,3 +872,178 @@ class LlmHelperTestCallTest(unittest.TestCase):
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0].get("thinking_level"), "high")
self.assertFalse(calls[0].get("include_thoughts"))
def test_get_llm_responses_protocol_forces_responses_api(self):
"""显式 responses 协议应让通用 OpenAI 兼容入口走 Responses API。"""
calls = []
class _FakeChatOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.model = kwargs["model"]
self.profile = None
with patch.dict(
sys.modules,
{"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI)},
):
asyncio.run(
llm_module.LLMHelper.get_llm(
provider="openai",
model="gpt-5.6-terra",
api_key="sk-test",
base_url="https://example.com/v1",
api_protocol="responses",
)
)
self.assertEqual(len(calls), 1)
self.assertTrue(calls[0].get("use_responses_api"))
def test_get_llm_chat_completions_protocol_overrides_chatgpt_auto(self):
"""显式 chat_completions 应覆盖 ChatGPT 官方推理模型的自动 Responses 切换。"""
calls = []
class _FakeChatOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.model = kwargs["model"]
self.profile = None
with patch.dict(
sys.modules,
{"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI)},
):
asyncio.run(
llm_module.LLMHelper.get_llm(
provider="chatgpt",
model="gpt-5.4",
api_key="sk-test",
base_url="https://api.openai.com/v1",
api_protocol="chat_completions",
)
)
self.assertEqual(len(calls), 1)
self.assertFalse(calls[0].get("use_responses_api"))
def test_get_llm_auto_protocol_keeps_chat_completions_for_compatible(self):
"""auto 协议下通用 OpenAI 兼容入口应保持默认 Chat CompletionsNone"""
calls = []
class _FakeChatOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.model = kwargs["model"]
self.profile = None
with patch.dict(
sys.modules,
{"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI)},
):
asyncio.run(
llm_module.LLMHelper.get_llm(
provider="openai",
model="gpt-4o",
api_key="sk-test",
base_url="https://example.com/v1",
api_protocol="auto",
)
)
self.assertEqual(len(calls), 1)
self.assertIsNone(calls[0].get("use_responses_api"))
def test_get_llm_runtime_override_beats_chat_completions_protocol(self):
"""运行时强制 ResponsesOAuth/Codex应优先于用户 chat_completions 设置。"""
calls = []
class _FakeProviderManager:
async def resolve_runtime(self, **kwargs):
return {
"provider_id": kwargs["provider_id"],
"runtime": "openai_compatible",
"model_id": kwargs["model"],
"api_key": kwargs["api_key"],
"base_url": kwargs["base_url"],
"default_headers": None,
"use_responses_api": True,
"model_record": None,
"model_metadata": None,
}
class _FakeChatOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.model = kwargs["model"]
self.profile = None
provider_module = ModuleType("app.agent.llm.provider")
provider_module.LLMProviderManager = _FakeProviderManager
with patch.dict(
sys.modules,
{
"app.agent.llm.provider": provider_module,
"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI),
},
):
asyncio.run(
llm_module.LLMHelper.get_llm(
provider="chatgpt",
model="gpt-5.4",
api_key="sk-test",
base_url="https://api.openai.com/v1",
api_protocol="chat_completions",
)
)
self.assertEqual(len(calls), 1)
self.assertTrue(calls[0].get("use_responses_api"))
def test_get_llm_reads_api_protocol_from_settings_when_omitted(self):
"""未显式传入协议时应读取 LLM_API_PROTOCOL 配置。"""
calls = []
class _FakeChatOpenAI:
def __init__(self, **kwargs):
calls.append(kwargs)
self.model = kwargs["model"]
self.profile = None
with patch.object(
llm_module.settings, "LLM_API_PROTOCOL", "responses"
), patch.dict(
sys.modules,
{"langchain_openai": SimpleNamespace(ChatOpenAI=_FakeChatOpenAI)},
):
asyncio.run(
llm_module.LLMHelper.get_llm(
provider="openai",
model="gpt-5.6-terra",
api_key="sk-test",
base_url="https://example.com/v1",
)
)
self.assertEqual(len(calls), 1)
self.assertTrue(calls[0].get("use_responses_api"))
def test_normalize_api_protocol_accepts_known_and_falls_back(self):
"""_normalize_api_protocol 应大小写不敏感识别已知值,未知值回退 auto。"""
self.assertEqual(
llm_module.LLMHelper._normalize_api_protocol("Responses"), "responses"
)
self.assertEqual(
llm_module.LLMHelper._normalize_api_protocol("CHAT_COMPLETIONS"),
"chat_completions",
)
self.assertEqual(
llm_module.LLMHelper._normalize_api_protocol("auto"), "auto"
)
self.assertEqual(
llm_module.LLMHelper._normalize_api_protocol(None), "auto"
)
self.assertEqual(
llm_module.LLMHelper._normalize_api_protocol("weird"), "auto"
)

View File

@@ -118,6 +118,8 @@ class LlmTestEndpointTest(unittest.TestCase):
system_endpoint.settings, "LLM_USER_AGENT", "MoviePilot-Test/1.0"
), patch.object(
system_endpoint.settings, "LLM_USE_PROXY", True
), patch.object(
system_endpoint.settings, "LLM_API_PROTOCOL", "responses"
), patch.object(
system_endpoint.LLMHelper,
"test_current_settings",
@@ -135,6 +137,7 @@ class LlmTestEndpointTest(unittest.TestCase):
base_url_preset="deepseek-default",
user_agent="MoviePilot-Test/1.0",
use_proxy=True,
api_protocol="responses",
)
self.assertTrue(resp.success)
self.assertEqual(resp.data["provider"], "deepseek")
@@ -186,6 +189,7 @@ class LlmTestEndpointTest(unittest.TestCase):
base_url_preset="openai-default",
user_agent="MoviePilot-Custom/1.0",
use_proxy=False,
api_protocol=None,
)
self.assertTrue(resp.success)
self.assertEqual(resp.data["provider"], "openai")
@@ -228,6 +232,7 @@ class LlmTestEndpointTest(unittest.TestCase):
base_url_preset="deepseek-default",
user_agent=None,
use_proxy=None,
api_protocol=None,
)
self.assertTrue(resp.success)