mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
feat: unify llm thinking level controls
This commit is contained in:
@@ -57,7 +57,9 @@ class LlmTestRequest(BaseModel):
|
|||||||
enabled: Optional[bool] = None
|
enabled: Optional[bool] = None
|
||||||
provider: Optional[str] = None
|
provider: Optional[str] = None
|
||||||
model: Optional[str] = None
|
model: Optional[str] = None
|
||||||
|
thinking_level: Optional[str] = None
|
||||||
disable_thinking: Optional[bool] = None
|
disable_thinking: Optional[bool] = None
|
||||||
|
reasoning_effort: Optional[str] = None
|
||||||
api_key: Optional[str] = None
|
api_key: Optional[str] = None
|
||||||
base_url: Optional[str] = None
|
base_url: Optional[str] = None
|
||||||
|
|
||||||
@@ -308,7 +310,13 @@ def _build_llm_test_snapshot(payload: Optional[LlmTestRequest] = None) -> dict[s
|
|||||||
"""
|
"""
|
||||||
provider = settings.LLM_PROVIDER
|
provider = settings.LLM_PROVIDER
|
||||||
model = settings.LLM_MODEL
|
model = settings.LLM_MODEL
|
||||||
|
thinking_level = _normalize_llm_test_value(
|
||||||
|
getattr(settings, "LLM_THINKING_LEVEL", None), empty_as_none=True
|
||||||
|
)
|
||||||
disable_thinking = bool(getattr(settings, "LLM_DISABLE_THINKING", False))
|
disable_thinking = bool(getattr(settings, "LLM_DISABLE_THINKING", False))
|
||||||
|
reasoning_effort = _normalize_llm_test_value(
|
||||||
|
getattr(settings, "LLM_REASONING_EFFORT", None), empty_as_none=True
|
||||||
|
)
|
||||||
api_key = settings.LLM_API_KEY
|
api_key = settings.LLM_API_KEY
|
||||||
base_url = settings.LLM_BASE_URL
|
base_url = settings.LLM_BASE_URL
|
||||||
enabled = bool(settings.AI_AGENT_ENABLE)
|
enabled = bool(settings.AI_AGENT_ENABLE)
|
||||||
@@ -320,18 +328,32 @@ def _build_llm_test_snapshot(payload: Optional[LlmTestRequest] = None) -> dict[s
|
|||||||
provider = _normalize_llm_test_value(payload.provider) or ""
|
provider = _normalize_llm_test_value(payload.provider) or ""
|
||||||
if payload.model is not None:
|
if payload.model is not None:
|
||||||
model = _normalize_llm_test_value(payload.model) or ""
|
model = _normalize_llm_test_value(payload.model) or ""
|
||||||
|
if payload.thinking_level is not None:
|
||||||
|
thinking_level = _normalize_llm_test_value(
|
||||||
|
payload.thinking_level, empty_as_none=True
|
||||||
|
)
|
||||||
if payload.disable_thinking is not None:
|
if payload.disable_thinking is not None:
|
||||||
disable_thinking = bool(payload.disable_thinking)
|
disable_thinking = bool(payload.disable_thinking)
|
||||||
|
if payload.reasoning_effort is not None:
|
||||||
|
reasoning_effort = _normalize_llm_test_value(
|
||||||
|
payload.reasoning_effort, empty_as_none=True
|
||||||
|
)
|
||||||
if payload.api_key is not None:
|
if payload.api_key is not None:
|
||||||
api_key = _normalize_llm_test_value(payload.api_key, empty_as_none=True)
|
api_key = _normalize_llm_test_value(payload.api_key, empty_as_none=True)
|
||||||
if payload.base_url is not None:
|
if payload.base_url is not None:
|
||||||
base_url = _normalize_llm_test_value(payload.base_url, empty_as_none=True)
|
base_url = _normalize_llm_test_value(payload.base_url, empty_as_none=True)
|
||||||
|
|
||||||
|
if thinking_level is not None:
|
||||||
|
disable_thinking = None
|
||||||
|
reasoning_effort = None
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"enabled": enabled,
|
"enabled": enabled,
|
||||||
"provider": provider,
|
"provider": provider,
|
||||||
"model": model,
|
"model": model,
|
||||||
|
"thinking_level": thinking_level,
|
||||||
"disable_thinking": disable_thinking,
|
"disable_thinking": disable_thinking,
|
||||||
|
"reasoning_effort": reasoning_effort,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
}
|
}
|
||||||
@@ -760,7 +782,9 @@ async def llm_test(
|
|||||||
result = await LLMHelper.test_current_settings(
|
result = await LLMHelper.test_current_settings(
|
||||||
provider=snapshot["provider"],
|
provider=snapshot["provider"],
|
||||||
model=snapshot["model"],
|
model=snapshot["model"],
|
||||||
|
thinking_level=snapshot["thinking_level"],
|
||||||
disable_thinking=snapshot["disable_thinking"],
|
disable_thinking=snapshot["disable_thinking"],
|
||||||
|
reasoning_effort=snapshot["reasoning_effort"],
|
||||||
api_key=snapshot["api_key"],
|
api_key=snapshot["api_key"],
|
||||||
base_url=snapshot["base_url"],
|
base_url=snapshot["base_url"],
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-1
@@ -505,8 +505,12 @@ class ConfigModel(BaseModel):
|
|||||||
LLM_PROVIDER: str = "deepseek"
|
LLM_PROVIDER: str = "deepseek"
|
||||||
# LLM模型名称
|
# LLM模型名称
|
||||||
LLM_MODEL: str = "deepseek-chat"
|
LLM_MODEL: str = "deepseek-chat"
|
||||||
# 是否尽量关闭模型的思考/推理能力(按各 provider/model 支持情况自动适配)
|
# 统一思考模式/深度配置:off/auto/minimal/low/medium/high/max/xhigh
|
||||||
|
LLM_THINKING_LEVEL: Optional[str] = None
|
||||||
|
# 兼容旧配置:是否尽量关闭模型的思考/推理能力(新配置优先)
|
||||||
LLM_DISABLE_THINKING: bool = True
|
LLM_DISABLE_THINKING: bool = True
|
||||||
|
# 兼容旧配置:思考强度(新配置优先)
|
||||||
|
LLM_REASONING_EFFORT: Optional[str] = None
|
||||||
# LLM是否支持图片输入,开启后消息图片会按多模态输入发送给模型
|
# LLM是否支持图片输入,开启后消息图片会按多模态输入发送给模型
|
||||||
LLM_SUPPORT_IMAGE_INPUT: bool = True
|
LLM_SUPPORT_IMAGE_INPUT: bool = True
|
||||||
# LLM API密钥
|
# LLM API密钥
|
||||||
|
|||||||
+262
-21
@@ -77,6 +77,10 @@ def _get_httpx_proxy_key() -> str:
|
|||||||
class LLMHelper:
|
class LLMHelper:
|
||||||
"""LLM模型相关辅助功能"""
|
"""LLM模型相关辅助功能"""
|
||||||
|
|
||||||
|
_SUPPORTED_THINKING_LEVELS = frozenset(
|
||||||
|
{"off", "auto", "minimal", "low", "medium", "high", "max", "xhigh"}
|
||||||
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _should_disable_thinking(disable_thinking: bool | None = None) -> bool:
|
def _should_disable_thinking(disable_thinking: bool | None = None) -> bool:
|
||||||
"""
|
"""
|
||||||
@@ -94,48 +98,276 @@ class LLMHelper:
|
|||||||
return (model_name or "").strip().lower()
|
return (model_name or "").strip().lower()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _build_disabled_thinking_kwargs(
|
def _normalize_thinking_level_value(cls, value: str | None) -> str | None:
|
||||||
|
"""
|
||||||
|
统一清理思考级别/强度值,并兼容常见别名。
|
||||||
|
"""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
normalized = str(value).strip().lower()
|
||||||
|
if not normalized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
alias_map = {
|
||||||
|
"none": "off",
|
||||||
|
"disabled": "off",
|
||||||
|
"disable": "off",
|
||||||
|
"enabled": "auto",
|
||||||
|
"enable": "auto",
|
||||||
|
"default": "auto",
|
||||||
|
"dynamic": "auto",
|
||||||
|
}
|
||||||
|
return alias_map.get(normalized, normalized)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_thinking_level(
|
||||||
|
cls, thinking_level: str | None = None
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
统一清理 thinking_level 配置。
|
||||||
|
"""
|
||||||
|
value = (
|
||||||
|
thinking_level
|
||||||
|
if thinking_level is not None
|
||||||
|
else getattr(settings, "LLM_THINKING_LEVEL", None)
|
||||||
|
)
|
||||||
|
normalized = cls._normalize_thinking_level_value(value)
|
||||||
|
if not normalized:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if normalized not in cls._SUPPORTED_THINKING_LEVELS:
|
||||||
|
logger.warning(f"忽略不支持的 thinking_level 配置: {normalized}")
|
||||||
|
return None
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_reasoning_effort(
|
||||||
|
cls, reasoning_effort: str | None = None
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
统一清理 legacy reasoning_effort 配置。
|
||||||
|
"""
|
||||||
|
value = (
|
||||||
|
reasoning_effort
|
||||||
|
if reasoning_effort is not None
|
||||||
|
else getattr(settings, "LLM_REASONING_EFFORT", None)
|
||||||
|
)
|
||||||
|
return cls._normalize_thinking_level(value)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _resolve_thinking_level(
|
||||||
|
cls,
|
||||||
|
thinking_level: str | None = None,
|
||||||
|
disable_thinking: bool | None = None,
|
||||||
|
reasoning_effort: str | None = None,
|
||||||
|
) -> str:
|
||||||
|
"""
|
||||||
|
统一解析本次调用的思考配置。
|
||||||
|
|
||||||
|
优先级:
|
||||||
|
1. 新字段 `thinking_level`
|
||||||
|
2. 本次调用传入的 legacy 字段
|
||||||
|
3. 已保存的新字段 `LLM_THINKING_LEVEL`
|
||||||
|
4. 已保存的 legacy 字段
|
||||||
|
"""
|
||||||
|
explicit_level = cls._normalize_thinking_level(thinking_level)
|
||||||
|
if explicit_level:
|
||||||
|
return explicit_level
|
||||||
|
|
||||||
|
explicit_effort = (
|
||||||
|
cls._normalize_reasoning_effort(reasoning_effort)
|
||||||
|
if reasoning_effort is not None
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
if disable_thinking is not None or reasoning_effort is not None:
|
||||||
|
if disable_thinking is not None and bool(disable_thinking):
|
||||||
|
return "off"
|
||||||
|
return explicit_effort or "auto"
|
||||||
|
|
||||||
|
configured_level = cls._normalize_thinking_level(
|
||||||
|
getattr(settings, "LLM_THINKING_LEVEL", None)
|
||||||
|
)
|
||||||
|
if configured_level:
|
||||||
|
return configured_level
|
||||||
|
|
||||||
|
legacy_disable = getattr(settings, "LLM_DISABLE_THINKING", None)
|
||||||
|
legacy_effort = cls._normalize_reasoning_effort(
|
||||||
|
getattr(settings, "LLM_REASONING_EFFORT", None)
|
||||||
|
)
|
||||||
|
if legacy_disable is not None:
|
||||||
|
return "off" if bool(legacy_disable) else (legacy_effort or "auto")
|
||||||
|
|
||||||
|
return legacy_effort or "off"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_deepseek_reasoning_effort(
|
||||||
|
cls, thinking_level: str | None = None
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
DeepSeek 文档当前建议使用 high/max;兼容常见 effort 别名。
|
||||||
|
"""
|
||||||
|
normalized = cls._normalize_thinking_level(thinking_level)
|
||||||
|
if not normalized or normalized in {"off", "auto"}:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if normalized in {"minimal", "low", "medium", "high"}:
|
||||||
|
return "high"
|
||||||
|
if normalized in {"max", "xhigh"}:
|
||||||
|
return "max"
|
||||||
|
|
||||||
|
logger.warning(f"忽略不支持的 DeepSeek reasoning_effort 配置: {normalized}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _normalize_openai_reasoning_effort(
|
||||||
|
cls, thinking_level: str | None = None
|
||||||
|
) -> str | None:
|
||||||
|
"""
|
||||||
|
OpenAI reasoning_effort 支持更细粒度的 effort,统一做最近似映射。
|
||||||
|
"""
|
||||||
|
normalized = cls._normalize_thinking_level(thinking_level)
|
||||||
|
if not normalized or normalized == "auto":
|
||||||
|
return None
|
||||||
|
if normalized == "off":
|
||||||
|
return "none"
|
||||||
|
if normalized == "max":
|
||||||
|
return "xhigh"
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_google_thinking_kwargs(
|
||||||
|
cls, model_name: str, thinking_level: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Gemini 3 使用 thinking_level;Gemini 2.5 使用 thinking_budget。
|
||||||
|
"""
|
||||||
|
if not model_name or thinking_level == "auto":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if "gemini-2.5" in model_name:
|
||||||
|
if thinking_level == "off":
|
||||||
|
if "pro" in model_name:
|
||||||
|
# Gemini 2.5 Pro 官方不支持完全关闭思考,回退到最小预算。
|
||||||
|
return {
|
||||||
|
"thinking_budget": 128,
|
||||||
|
"include_thoughts": False,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
"thinking_budget": 0,
|
||||||
|
"include_thoughts": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
budget_map = {
|
||||||
|
"minimal": 512,
|
||||||
|
"low": 1024,
|
||||||
|
"medium": 4096,
|
||||||
|
"high": 8192,
|
||||||
|
"max": 24576,
|
||||||
|
"xhigh": 24576,
|
||||||
|
}
|
||||||
|
budget = budget_map.get(thinking_level)
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
"thinking_budget": budget,
|
||||||
|
"include_thoughts": False,
|
||||||
|
}
|
||||||
|
if budget is not None
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
if "gemini-3" in model_name:
|
||||||
|
level_map = {
|
||||||
|
"off": "minimal",
|
||||||
|
"minimal": "minimal",
|
||||||
|
"low": "low",
|
||||||
|
"medium": "medium",
|
||||||
|
"high": "high",
|
||||||
|
"max": "high",
|
||||||
|
"xhigh": "high",
|
||||||
|
}
|
||||||
|
google_level = level_map.get(thinking_level)
|
||||||
|
return (
|
||||||
|
{
|
||||||
|
"thinking_level": google_level,
|
||||||
|
"include_thoughts": False,
|
||||||
|
}
|
||||||
|
if google_level
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_kimi_thinking_kwargs(
|
||||||
|
cls, model_name: str, thinking_level: str
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Kimi 当前公开文档仅支持思考开关,不支持显式深度调节。
|
||||||
|
"""
|
||||||
|
if model_name.startswith("kimi-k2-thinking"):
|
||||||
|
return {}
|
||||||
|
if thinking_level == "off":
|
||||||
|
return {"extra_body": {"thinking": {"type": "disabled"}}}
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _build_thinking_kwargs(
|
||||||
cls,
|
cls,
|
||||||
provider: str,
|
provider: str,
|
||||||
model: str | None,
|
model: str | None,
|
||||||
|
thinking_level: str | None = None,
|
||||||
disable_thinking: bool | None = None,
|
disable_thinking: bool | None = None,
|
||||||
|
reasoning_effort: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
按 provider/model 生成“禁用思考”相关参数。
|
按 provider/model 生成思考模式相关参数。
|
||||||
|
|
||||||
优先使用 LangChain/OpenAI SDK 已支持的原生字段;仅在 provider
|
优先使用 LangChain/OpenAI SDK 已支持的原生字段;仅在 provider
|
||||||
明确要求自定义请求体时,才回退到 extra_body。
|
明确要求自定义请求体时,才回退到 extra_body。
|
||||||
"""
|
"""
|
||||||
if not cls._should_disable_thinking(disable_thinking):
|
|
||||||
return {}
|
|
||||||
|
|
||||||
provider_name = (provider or "").strip().lower()
|
provider_name = (provider or "").strip().lower()
|
||||||
model_name = cls._normalize_model_name(model)
|
model_name = cls._normalize_model_name(model)
|
||||||
|
resolved_thinking_level = cls._resolve_thinking_level(
|
||||||
|
thinking_level=thinking_level,
|
||||||
|
disable_thinking=disable_thinking,
|
||||||
|
reasoning_effort=reasoning_effort,
|
||||||
|
)
|
||||||
|
|
||||||
|
if provider_name == "deepseek":
|
||||||
|
if resolved_thinking_level == "off":
|
||||||
|
return {"extra_body": {"thinking": {"type": "disabled"}}}
|
||||||
|
if resolved_thinking_level == "auto":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
kwargs: dict[str, Any] = {"extra_body": {"thinking": {"type": "enabled"}}}
|
||||||
|
deepseek_effort = cls._normalize_deepseek_reasoning_effort(
|
||||||
|
resolved_thinking_level
|
||||||
|
)
|
||||||
|
if deepseek_effort:
|
||||||
|
kwargs["reasoning_effort"] = deepseek_effort
|
||||||
|
return kwargs
|
||||||
|
|
||||||
|
if model_name.startswith(("kimi-k2.5", "kimi-k2.6", "kimi-k2-thinking")):
|
||||||
|
return cls._build_kimi_thinking_kwargs(model_name, resolved_thinking_level)
|
||||||
|
|
||||||
if not model_name:
|
if not model_name:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
# Moonshot Kimi K2.5/K2.6 需要在请求体显式声明 thinking.disabled。
|
|
||||||
if model_name.startswith(("kimi-k2.5", "kimi-k2.6")):
|
|
||||||
return {"extra_body": {"thinking": {"type": "disabled"}}}
|
|
||||||
|
|
||||||
# OpenAI 原生推理模型优先走 LangChain 内置 reasoning_effort。
|
# OpenAI 原生推理模型优先走 LangChain 内置 reasoning_effort。
|
||||||
if provider_name == "openai" and model_name.startswith(
|
if provider_name == "openai" and model_name.startswith(
|
||||||
("gpt-5", "o1", "o3", "o4")
|
("gpt-5", "o1", "o3", "o4")
|
||||||
):
|
):
|
||||||
return {"reasoning_effort": "none"}
|
openai_effort = cls._normalize_openai_reasoning_effort(
|
||||||
|
resolved_thinking_level
|
||||||
|
)
|
||||||
|
return {"reasoning_effort": openai_effort} if openai_effort else {}
|
||||||
|
|
||||||
# Gemini 使用 google-genai / langchain-google-genai 内置思考控制参数。
|
# Gemini 使用 google-genai / langchain-google-genai 内置思考控制参数。
|
||||||
if provider_name == "google":
|
if provider_name == "google":
|
||||||
if "gemini-2.5" in model_name:
|
return cls._build_google_thinking_kwargs(
|
||||||
return {
|
model_name, resolved_thinking_level
|
||||||
"thinking_budget": 0,
|
)
|
||||||
"include_thoughts": False,
|
|
||||||
}
|
|
||||||
if "gemini-3" in model_name:
|
|
||||||
return {
|
|
||||||
"thinking_level": "minimal",
|
|
||||||
"include_thoughts": False,
|
|
||||||
}
|
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -151,7 +383,9 @@ class LLMHelper:
|
|||||||
streaming: bool = False,
|
streaming: bool = False,
|
||||||
provider: str | None = None,
|
provider: str | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
thinking_level: str | None = None,
|
||||||
disable_thinking: bool | None = None,
|
disable_thinking: bool | None = None,
|
||||||
|
reasoning_effort: str | None = None,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
):
|
):
|
||||||
@@ -166,10 +400,12 @@ class LLMHelper:
|
|||||||
model_name = model if model is not None else settings.LLM_MODEL
|
model_name = model if model is not None else settings.LLM_MODEL
|
||||||
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
|
api_key_value = api_key if api_key is not None else settings.LLM_API_KEY
|
||||||
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
|
base_url_value = base_url if base_url is not None else settings.LLM_BASE_URL
|
||||||
thinking_kwargs = LLMHelper._build_disabled_thinking_kwargs(
|
thinking_kwargs = LLMHelper._build_thinking_kwargs(
|
||||||
provider=provider_name,
|
provider=provider_name,
|
||||||
model=model_name,
|
model=model_name,
|
||||||
|
thinking_level=thinking_level,
|
||||||
disable_thinking=disable_thinking,
|
disable_thinking=disable_thinking,
|
||||||
|
reasoning_effort=reasoning_effort,
|
||||||
)
|
)
|
||||||
|
|
||||||
if not api_key_value:
|
if not api_key_value:
|
||||||
@@ -204,6 +440,7 @@ class LLMHelper:
|
|||||||
model = ChatDeepSeek(
|
model = ChatDeepSeek(
|
||||||
model=model_name,
|
model=model_name,
|
||||||
api_key=api_key_value,
|
api_key=api_key_value,
|
||||||
|
api_base=base_url_value,
|
||||||
max_retries=3,
|
max_retries=3,
|
||||||
temperature=settings.LLM_TEMPERATURE,
|
temperature=settings.LLM_TEMPERATURE,
|
||||||
streaming=streaming,
|
streaming=streaming,
|
||||||
@@ -282,7 +519,9 @@ class LLMHelper:
|
|||||||
timeout: int = 20,
|
timeout: int = 20,
|
||||||
provider: str | None = None,
|
provider: str | None = None,
|
||||||
model: str | None = None,
|
model: str | None = None,
|
||||||
|
thinking_level: str | None = None,
|
||||||
disable_thinking: bool | None = None,
|
disable_thinking: bool | None = None,
|
||||||
|
reasoning_effort: str | None = None,
|
||||||
api_key: str | None = None,
|
api_key: str | None = None,
|
||||||
base_url: str | None = None,
|
base_url: str | None = None,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
@@ -298,7 +537,9 @@ class LLMHelper:
|
|||||||
streaming=False,
|
streaming=False,
|
||||||
provider=provider_name,
|
provider=provider_name,
|
||||||
model=model_name,
|
model=model_name,
|
||||||
|
thinking_level=thinking_level,
|
||||||
disable_thinking=disable_thinking,
|
disable_thinking=disable_thinking,
|
||||||
|
reasoning_effort=reasoning_effort,
|
||||||
api_key=api_key_value,
|
api_key=api_key_value,
|
||||||
base_url=base_url_value,
|
base_url=base_url_value,
|
||||||
)
|
)
|
||||||
|
|||||||
+7
-7
@@ -76,14 +76,14 @@ pympler~=1.1
|
|||||||
smbprotocol~=1.15.0
|
smbprotocol~=1.15.0
|
||||||
setproctitle~=1.3.6
|
setproctitle~=1.3.6
|
||||||
httpx[socks]~=0.28.1
|
httpx[socks]~=0.28.1
|
||||||
langchain~=1.2.13
|
langchain~=1.2.15
|
||||||
langchain-core~=1.2.20
|
langchain-core~=1.3.1
|
||||||
langchain-community~=0.4.1
|
langchain-community~=0.4.1
|
||||||
langchain-openai~=1.1.11
|
langchain-openai~=1.2.0
|
||||||
langchain-google-genai~=4.2.1
|
langchain-google-genai~=4.2.2
|
||||||
langchain-deepseek~=1.0.1
|
langchain-deepseek~=1.0.1
|
||||||
langgraph~=1.1.3
|
langgraph~=1.1.9
|
||||||
openai~=2.29.0
|
openai~=2.32.0
|
||||||
google-genai~=1.68.0
|
google-genai~=1.73.1
|
||||||
ddgs~=9.10.0
|
ddgs~=9.10.0
|
||||||
websocket-client~=1.8.0
|
websocket-client~=1.8.0
|
||||||
|
|||||||
+47
-3
@@ -1063,6 +1063,40 @@ def _prompt_choice(label: str, choices: dict[str, str], default: str) -> str:
|
|||||||
print("请输入列表中的可选值。")
|
print("请输入列表中的可选值。")
|
||||||
|
|
||||||
|
|
||||||
|
def _env_llm_thinking_level_default() -> str:
|
||||||
|
value = _normalize_choice(_env_default("LLM_THINKING_LEVEL", ""))
|
||||||
|
alias_map = {
|
||||||
|
"none": "off",
|
||||||
|
"disabled": "off",
|
||||||
|
"disable": "off",
|
||||||
|
"enabled": "auto",
|
||||||
|
"enable": "auto",
|
||||||
|
"default": "auto",
|
||||||
|
"dynamic": "auto",
|
||||||
|
}
|
||||||
|
normalized = alias_map.get(value, value)
|
||||||
|
if normalized in {
|
||||||
|
"off",
|
||||||
|
"auto",
|
||||||
|
"minimal",
|
||||||
|
"low",
|
||||||
|
"medium",
|
||||||
|
"high",
|
||||||
|
"max",
|
||||||
|
"xhigh",
|
||||||
|
}:
|
||||||
|
return normalized
|
||||||
|
|
||||||
|
legacy_disable = _env_bool("LLM_DISABLE_THINKING", True)
|
||||||
|
legacy_effort = _normalize_choice(_env_default("LLM_REASONING_EFFORT", ""))
|
||||||
|
legacy_effort = alias_map.get(legacy_effort, legacy_effort)
|
||||||
|
if legacy_disable:
|
||||||
|
return "off"
|
||||||
|
if legacy_effort in {"minimal", "low", "medium", "high", "max", "xhigh"}:
|
||||||
|
return legacy_effort
|
||||||
|
return "auto"
|
||||||
|
|
||||||
|
|
||||||
def _prompt_path(label: str, *, default: Path, allow_empty: bool = False) -> str:
|
def _prompt_path(label: str, *, default: Path, allow_empty: bool = False) -> str:
|
||||||
value = _prompt_text(label, default=str(default), allow_empty=allow_empty)
|
value = _prompt_text(label, default=str(default), allow_empty=allow_empty)
|
||||||
if not value:
|
if not value:
|
||||||
@@ -1476,9 +1510,19 @@ def _collect_agent_config() -> dict[str, Any]:
|
|||||||
current_value=read_env_value("LLM_API_KEY"),
|
current_value=read_env_value("LLM_API_KEY"),
|
||||||
required=True,
|
required=True,
|
||||||
),
|
),
|
||||||
"LLM_DISABLE_THINKING": _prompt_yes_no(
|
"LLM_THINKING_LEVEL": _prompt_choice(
|
||||||
"是否尽量关闭模型思考/推理",
|
"LLM 思考模式/深度",
|
||||||
default=_env_bool("LLM_DISABLE_THINKING", False),
|
choices={
|
||||||
|
"off": "关闭思考",
|
||||||
|
"auto": "自动",
|
||||||
|
"minimal": "最小",
|
||||||
|
"low": "低",
|
||||||
|
"medium": "中",
|
||||||
|
"high": "高",
|
||||||
|
"max": "极高",
|
||||||
|
"xhigh": "超高",
|
||||||
|
},
|
||||||
|
default=_env_llm_thinking_level_default(),
|
||||||
),
|
),
|
||||||
"LLM_SUPPORT_IMAGE_INPUT": _prompt_yes_no(
|
"LLM_SUPPORT_IMAGE_INPUT": _prompt_yes_no(
|
||||||
"是否启用图片输入支持",
|
"是否启用图片输入支持",
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ _stub_module(
|
|||||||
LLM_MODEL="global-model",
|
LLM_MODEL="global-model",
|
||||||
LLM_API_KEY="global-key",
|
LLM_API_KEY="global-key",
|
||||||
LLM_BASE_URL="https://global.example.com",
|
LLM_BASE_URL="https://global.example.com",
|
||||||
|
LLM_THINKING_LEVEL=None,
|
||||||
LLM_DISABLE_THINKING=False,
|
LLM_DISABLE_THINKING=False,
|
||||||
|
LLM_REASONING_EFFORT=None,
|
||||||
LLM_TEMPERATURE=0.1,
|
LLM_TEMPERATURE=0.1,
|
||||||
LLM_MAX_CONTEXT_TOKENS=64,
|
LLM_MAX_CONTEXT_TOKENS=64,
|
||||||
PROXY_HOST=None,
|
PROXY_HOST=None,
|
||||||
@@ -83,7 +85,9 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
streaming=False,
|
streaming=False,
|
||||||
provider="deepseek",
|
provider="deepseek",
|
||||||
model="deepseek-chat",
|
model="deepseek-chat",
|
||||||
|
thinking_level=None,
|
||||||
disable_thinking=None,
|
disable_thinking=None,
|
||||||
|
reasoning_effort=None,
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
)
|
)
|
||||||
@@ -138,7 +142,65 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
{"thinking": {"type": "disabled"}},
|
{"thinking": {"type": "disabled"}},
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_get_llm_uses_openai_reasoning_effort_none(self):
|
def test_get_llm_uses_deepseek_thinking_level_controls(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class _FakeChatDeepSeek:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
self.model = kwargs["model"]
|
||||||
|
self.profile = None
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
sys.modules,
|
||||||
|
{"langchain_deepseek": SimpleNamespace(ChatDeepSeek=_FakeChatDeepSeek)},
|
||||||
|
):
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="deepseek",
|
||||||
|
model="deepseek-v4-pro",
|
||||||
|
thinking_level="xhigh",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://api.deepseek.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
calls[0].get("extra_body"),
|
||||||
|
{"thinking": {"type": "enabled"}},
|
||||||
|
)
|
||||||
|
self.assertEqual(calls[0].get("reasoning_effort"), "max")
|
||||||
|
self.assertEqual(calls[0].get("api_base"), "https://api.deepseek.com")
|
||||||
|
|
||||||
|
def test_get_llm_disables_deepseek_thinking_via_thinking_level(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class _FakeChatDeepSeek:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
self.model = kwargs["model"]
|
||||||
|
self.profile = None
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
sys.modules,
|
||||||
|
{"langchain_deepseek": SimpleNamespace(ChatDeepSeek=_FakeChatDeepSeek)},
|
||||||
|
):
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="deepseek",
|
||||||
|
model="deepseek-v4-flash",
|
||||||
|
thinking_level="off",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://proxy.example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(
|
||||||
|
calls[0].get("extra_body"),
|
||||||
|
{"thinking": {"type": "disabled"}},
|
||||||
|
)
|
||||||
|
self.assertIsNone(calls[0].get("reasoning_effort"))
|
||||||
|
self.assertEqual(calls[0].get("api_base"), "https://proxy.example.com")
|
||||||
|
|
||||||
|
def test_get_llm_uses_openai_reasoning_effort_none_for_off(self):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
class _FakeChatOpenAI:
|
class _FakeChatOpenAI:
|
||||||
@@ -154,7 +216,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
llm_module.LLMHelper.get_llm(
|
llm_module.LLMHelper.get_llm(
|
||||||
provider="openai",
|
provider="openai",
|
||||||
model="gpt-5-mini",
|
model="gpt-5-mini",
|
||||||
disable_thinking=True,
|
thinking_level="off",
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.openai.com/v1",
|
base_url="https://api.openai.com/v1",
|
||||||
)
|
)
|
||||||
@@ -162,6 +224,30 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
self.assertEqual(len(calls), 1)
|
self.assertEqual(len(calls), 1)
|
||||||
self.assertEqual(calls[0].get("reasoning_effort"), "none")
|
self.assertEqual(calls[0].get("reasoning_effort"), "none")
|
||||||
|
|
||||||
|
def test_get_llm_maps_unified_max_to_openai_xhigh(self):
|
||||||
|
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)},
|
||||||
|
):
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="openai",
|
||||||
|
model="gpt-5.4",
|
||||||
|
thinking_level="max",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url="https://api.openai.com/v1",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(calls[0].get("reasoning_effort"), "xhigh")
|
||||||
|
|
||||||
def test_get_llm_uses_gemini_builtin_thinking_controls(self):
|
def test_get_llm_uses_gemini_builtin_thinking_controls(self):
|
||||||
calls = []
|
calls = []
|
||||||
|
|
||||||
@@ -182,7 +268,7 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
llm_module.LLMHelper.get_llm(
|
llm_module.LLMHelper.get_llm(
|
||||||
provider="google",
|
provider="google",
|
||||||
model="gemini-2.5-flash",
|
model="gemini-2.5-flash",
|
||||||
disable_thinking=True,
|
thinking_level="off",
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url=None,
|
base_url=None,
|
||||||
)
|
)
|
||||||
@@ -191,6 +277,35 @@ class LlmHelperTestCallTest(unittest.TestCase):
|
|||||||
self.assertEqual(calls[0].get("thinking_budget"), 0)
|
self.assertEqual(calls[0].get("thinking_budget"), 0)
|
||||||
self.assertFalse(calls[0].get("include_thoughts"))
|
self.assertFalse(calls[0].get("include_thoughts"))
|
||||||
|
|
||||||
|
def test_get_llm_uses_gemini_3_thinking_level_controls(self):
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
class _FakeChatGoogleGenerativeAI:
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
calls.append(kwargs)
|
||||||
|
self.model = kwargs["model"]
|
||||||
|
self.profile = None
|
||||||
|
|
||||||
|
with patch.dict(
|
||||||
|
sys.modules,
|
||||||
|
{
|
||||||
|
"langchain_google_genai": SimpleNamespace(
|
||||||
|
ChatGoogleGenerativeAI=_FakeChatGoogleGenerativeAI
|
||||||
|
)
|
||||||
|
},
|
||||||
|
):
|
||||||
|
llm_module.LLMHelper.get_llm(
|
||||||
|
provider="google",
|
||||||
|
model="gemini-3.1-flash",
|
||||||
|
thinking_level="xhigh",
|
||||||
|
api_key="sk-test",
|
||||||
|
base_url=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
self.assertEqual(calls[0].get("thinking_level"), "high")
|
||||||
|
self.assertFalse(calls[0].get("include_thoughts"))
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -119,6 +119,8 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", True), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", True), patch.object(
|
||||||
system_endpoint.settings, "LLM_PROVIDER", "deepseek"
|
system_endpoint.settings, "LLM_PROVIDER", "deepseek"
|
||||||
), patch.object(system_endpoint.settings, "LLM_MODEL", "deepseek-chat"), patch.object(
|
), patch.object(system_endpoint.settings, "LLM_MODEL", "deepseek-chat"), patch.object(
|
||||||
|
system_endpoint.settings, "LLM_THINKING_LEVEL", "max"
|
||||||
|
), patch.object(
|
||||||
system_endpoint.settings, "LLM_API_KEY", "sk-test"
|
system_endpoint.settings, "LLM_API_KEY", "sk-test"
|
||||||
), patch.object(
|
), patch.object(
|
||||||
system_endpoint.settings, "LLM_BASE_URL", "https://api.deepseek.com"
|
system_endpoint.settings, "LLM_BASE_URL", "https://api.deepseek.com"
|
||||||
@@ -133,7 +135,9 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
llm_test_mock.assert_awaited_once_with(
|
llm_test_mock.assert_awaited_once_with(
|
||||||
provider="deepseek",
|
provider="deepseek",
|
||||||
model="deepseek-chat",
|
model="deepseek-chat",
|
||||||
disable_thinking=False,
|
thinking_level="max",
|
||||||
|
disable_thinking=None,
|
||||||
|
reasoning_effort=None,
|
||||||
api_key="sk-test",
|
api_key="sk-test",
|
||||||
base_url="https://api.deepseek.com",
|
base_url="https://api.deepseek.com",
|
||||||
)
|
)
|
||||||
@@ -156,7 +160,7 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
enabled=True,
|
enabled=True,
|
||||||
provider="openai",
|
provider="openai",
|
||||||
model="gpt-4.1-mini",
|
model="gpt-4.1-mini",
|
||||||
disable_thinking=True,
|
thinking_level="high",
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
)
|
)
|
||||||
@@ -178,7 +182,9 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
llm_test_mock.assert_awaited_once_with(
|
llm_test_mock.assert_awaited_once_with(
|
||||||
provider="openai",
|
provider="openai",
|
||||||
model="gpt-4.1-mini",
|
model="gpt-4.1-mini",
|
||||||
disable_thinking=True,
|
thinking_level="high",
|
||||||
|
disable_thinking=None,
|
||||||
|
reasoning_effort=None,
|
||||||
api_key="sk-live",
|
api_key="sk-live",
|
||||||
base_url="https://example.com/v1",
|
base_url="https://example.com/v1",
|
||||||
)
|
)
|
||||||
@@ -186,6 +192,44 @@ class LlmTestEndpointTest(unittest.TestCase):
|
|||||||
self.assertEqual(resp.data["provider"], "openai")
|
self.assertEqual(resp.data["provider"], "openai")
|
||||||
self.assertEqual(resp.data["model"], "gpt-4.1-mini")
|
self.assertEqual(resp.data["model"], "gpt-4.1-mini")
|
||||||
|
|
||||||
|
def test_llm_test_supports_legacy_thinking_payload(self):
|
||||||
|
llm_test_mock = AsyncMock(
|
||||||
|
return_value={
|
||||||
|
"provider": "deepseek",
|
||||||
|
"model": "deepseek-v4-pro",
|
||||||
|
"duration_ms": 123,
|
||||||
|
"reply_preview": "OK",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
payload = system_endpoint.LlmTestRequest(
|
||||||
|
enabled=True,
|
||||||
|
provider="deepseek",
|
||||||
|
model="deepseek-v4-pro",
|
||||||
|
disable_thinking=False,
|
||||||
|
reasoning_effort="xhigh",
|
||||||
|
api_key="sk-live",
|
||||||
|
base_url="https://api.deepseek.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", False), patch.object(
|
||||||
|
system_endpoint.LLMHelper,
|
||||||
|
"test_current_settings",
|
||||||
|
llm_test_mock,
|
||||||
|
create=True,
|
||||||
|
):
|
||||||
|
resp = asyncio.run(system_endpoint.llm_test(payload=payload, _="token"))
|
||||||
|
|
||||||
|
llm_test_mock.assert_awaited_once_with(
|
||||||
|
provider="deepseek",
|
||||||
|
model="deepseek-v4-pro",
|
||||||
|
thinking_level=None,
|
||||||
|
disable_thinking=False,
|
||||||
|
reasoning_effort="xhigh",
|
||||||
|
api_key="sk-live",
|
||||||
|
base_url="https://api.deepseek.com",
|
||||||
|
)
|
||||||
|
self.assertTrue(resp.success)
|
||||||
|
|
||||||
def test_llm_test_rejects_empty_reply(self):
|
def test_llm_test_rejects_empty_reply(self):
|
||||||
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", True), patch.object(
|
with patch.object(system_endpoint.settings, "AI_AGENT_ENABLE", True), patch.object(
|
||||||
system_endpoint.settings, "LLM_PROVIDER", "deepseek"
|
system_endpoint.settings, "LLM_PROVIDER", "deepseek"
|
||||||
|
|||||||
Reference in New Issue
Block a user