mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-10-30 11:54:50 +08:00
175 lines
6.6 KiB
Python
175 lines
6.6 KiB
Python
"""
|
||
LLM 温度的未指定、显式留空与数值透传合同。
|
||
"""
|
||
|
||
import asyncio
|
||
from types import SimpleNamespace
|
||
from unittest.mock import AsyncMock, Mock
|
||
|
||
import httpx
|
||
import pytest
|
||
from langchain_google_genai import chat_models as _google_models
|
||
|
||
from app.agent.llm import helper
|
||
from app.agent.llm.helper import LLMHelper
|
||
from app.agent.llm.provider import LLMProviderManager
|
||
|
||
# 在收集测试时保存原生判断;签名兼容测试会在执行阶段改写 SDK 全局函数。
|
||
_NATIVE_GEMINI_GENERATION = _google_models._is_gemini_3_or_later
|
||
|
||
|
||
@pytest.fixture
|
||
def offline_runtime(monkeypatch):
|
||
"""
|
||
隔离提供商解析和 HTTP 客户端,所有温度验证禁止真实网络请求。
|
||
"""
|
||
runtime = SimpleNamespace(resolve_runtime=AsyncMock(return_value={
|
||
"provider_id": "openai",
|
||
"runtime": "openai_compatible",
|
||
"model_id": "gpt-6-luna",
|
||
"api_key": "offline-key",
|
||
"base_url": "https://offline.example/v1",
|
||
"model_metadata": None,
|
||
}))
|
||
monkeypatch.setattr(helper, "_patch_openai_responses_instructions_support", lambda: None)
|
||
monkeypatch.setattr(helper, "_patch_tool_schema_request_support", lambda *_args, **_kwargs: None)
|
||
clients = []
|
||
|
||
def reject_request(_request):
|
||
"""
|
||
拒绝任何意外的提供商网络调用。
|
||
"""
|
||
raise AssertionError("温度序列化测试不得发起外部请求")
|
||
|
||
def make_client(_proxy, *, async_client=False):
|
||
"""
|
||
创建只经过离线传输器的同步或异步客户端。
|
||
"""
|
||
client_type = httpx.AsyncClient if async_client else httpx.Client
|
||
client = client_type(transport=httpx.MockTransport(reject_request))
|
||
clients.append(client)
|
||
return client
|
||
|
||
monkeypatch.setattr(helper, "_build_httpx_client", make_client)
|
||
yield runtime
|
||
for client in clients:
|
||
if isinstance(client, httpx.AsyncClient):
|
||
asyncio.run(client.aclose())
|
||
else:
|
||
client.close()
|
||
|
||
|
||
@pytest.mark.parametrize("protocol", ["chat_completions", "responses"])
|
||
@pytest.mark.parametrize(
|
||
("saved", "override", "expected"),
|
||
[
|
||
(None, {}, None),
|
||
(0.3, {}, 0.3),
|
||
(0.3, {"temperature": None}, None),
|
||
(0.3, {"temperature": 0}, 0),
|
||
(None, {"temperature": 1}, 1),
|
||
],
|
||
)
|
||
def test_llm_payload_omits_empty_temperature(
|
||
monkeypatch, offline_runtime, protocol, saved, override, expected,
|
||
) -> None:
|
||
"""
|
||
两个 OpenAI 协议均省略空温度,并保留显式零及未覆盖的已保存值。
|
||
"""
|
||
original_setting = helper.get_runtime_setting
|
||
monkeypatch.setattr(
|
||
helper, "get_runtime_setting",
|
||
lambda key, default=None: saved if key == "LLM_TEMPERATURE" else original_setting(key, default),
|
||
)
|
||
model = asyncio.run(LLMHelper.get_llm(
|
||
provider="openai", model="gpt-6-luna", api_key="offline-key",
|
||
base_url="https://offline.example/v1", use_proxy=False,
|
||
api_protocol=protocol, web_search_mode="disabled", thinking_level="off",
|
||
provider_runtime=offline_runtime, **override,
|
||
))
|
||
|
||
payload = model._get_request_payload("只回复 OK")
|
||
|
||
if expected is None:
|
||
assert "temperature" not in payload
|
||
else:
|
||
assert payload["temperature"] == expected
|
||
|
||
|
||
@pytest.fixture
|
||
def _native_google_defaults(monkeypatch):
|
||
"""
|
||
原生默认值测试恢复 SDK 世代判断,隔离其它签名兼容测试留下的全局补丁。
|
||
"""
|
||
monkeypatch.setattr(_google_models, "_is_gemini_3_or_later", _NATIVE_GEMINI_GENERATION)
|
||
|
||
|
||
@pytest.mark.parametrize("model_name", ["gemini-2.5-flash", "gemini-3.6-flash-preview"])
|
||
@pytest.mark.parametrize("temperature", [None, 0, 1])
|
||
def test_google_empty_temperature_uses_sdk_default(
|
||
monkeypatch, offline_runtime, model_name, temperature, _native_google_defaults,
|
||
) -> None:
|
||
"""
|
||
原生 Google SDK 不接受 None,留空须保留其模型默认值且显式零仍有效。
|
||
"""
|
||
from langchain_google_genai import chat_models
|
||
|
||
offline_runtime.resolve_runtime.return_value.update(
|
||
provider_id="google", runtime="google", model_id=model_name, base_url=None,
|
||
)
|
||
monkeypatch.setattr(helper, "_patch_gemini_thought_signature", lambda: None)
|
||
monkeypatch.setattr(chat_models, "Client", Mock(return_value=Mock(aio=None)))
|
||
|
||
model = asyncio.run(LLMHelper.get_llm(
|
||
provider="google", model=model_name, api_key="offline-key", use_proxy=False,
|
||
web_search_mode="disabled", thinking_level="off", temperature=temperature,
|
||
provider_runtime=offline_runtime,
|
||
))
|
||
|
||
expected = temperature if temperature is not None else (0.7 if "2.5" in model_name else 1.0)
|
||
assert model.temperature == expected
|
||
assert model._prepare_params(None).temperature == expected
|
||
|
||
|
||
@pytest.mark.parametrize("override", [{}, {"temperature": None}, {"temperature": 0}])
|
||
def test_llm_test_preserves_temperature_presence(monkeypatch, override) -> None:
|
||
"""
|
||
连接测试区分未传温度与显式留空,避免清空草稿沿用旧配置。
|
||
"""
|
||
get_model = AsyncMock(return_value=SimpleNamespace(
|
||
ainvoke=AsyncMock(return_value=SimpleNamespace(content="OK")),
|
||
))
|
||
monkeypatch.setattr(LLMHelper, "get_llm", get_model)
|
||
|
||
result = asyncio.run(LLMHelper.test_current_settings(
|
||
provider="openai", model="gpt-6-luna", **override,
|
||
))
|
||
|
||
assert result["reply_preview"] == "OK"
|
||
forwarded = get_model.await_args.kwargs
|
||
assert ("temperature" in forwarded) == ("temperature" in override)
|
||
if "temperature" in override:
|
||
assert forwarded["temperature"] == override["temperature"]
|
||
|
||
|
||
@pytest.mark.parametrize("override", [{}, {"temperature": None}, {"temperature": 0}])
|
||
def test_provider_test_preserves_temperature_presence(monkeypatch, override) -> None:
|
||
"""
|
||
管理 API 原样保留温度是否出现及其空值,不借用已保存温度。
|
||
"""
|
||
manager = LLMProviderManager()
|
||
monkeypatch.setattr(manager, "get_saved_auth", lambda _provider: None)
|
||
monkeypatch.setattr(manager, "get_provider", lambda _provider: SimpleNamespace(oauth_methods=()))
|
||
test_settings = AsyncMock(return_value={"reply_preview": "OK"})
|
||
monkeypatch.setattr(LLMHelper, "test_current_settings", test_settings)
|
||
|
||
result = asyncio.run(manager.provider_manage(
|
||
"openai", "test", enabled=True, model="gpt-6-luna", api_key="offline-key", **override,
|
||
))
|
||
|
||
assert result["success"] is True
|
||
forwarded = test_settings.await_args.kwargs
|
||
assert ("temperature" in forwarded) == ("temperature" in override)
|
||
if "temperature" in override:
|
||
assert forwarded["temperature"] == override["temperature"]
|