refactor: remove legacy LLM_DISABLE_THINKING and LLM_REASONING_EFFORT config, unify thinking_level handling

- Eliminate support for LLM_DISABLE_THINKING and LLM_REASONING_EFFORT in config, code, and tests
- Simplify LLM thinking level logic to rely solely on LLM_THINKING_LEVEL
- Refactor LLMHelper and related endpoints to remove legacy parameter handling
- Update system API and test utilities to match new configuration structure
- Minor code cleanup and formatting improvements
This commit is contained in:
jxxghp
2026-04-25 10:42:03 +08:00
parent 4a81417fb7
commit a05ffc07d4
6 changed files with 143 additions and 389 deletions

View File

@@ -12,6 +12,7 @@ from anyio import Path as AsyncPath
from app.helper.sites import SitesHelper # noqa # noqa
from fastapi import APIRouter, Body, Depends, HTTPException, Header, Request, Response
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from app import schemas
from app.chain.mediaserver import MediaServerChain
@@ -29,14 +30,14 @@ from app.db.user_oper import (
get_current_active_superuser_async,
get_current_active_user_async,
)
from app.helper.llm import LLMHelper, LLMTestError, LLMTestTimeout
from app.helper.image import ImageHelper
from app.helper.llm import LLMHelper, LLMTestTimeout
from app.helper.mediaserver import MediaServerHelper
from app.helper.message import MessageHelper
from app.helper.progress import ProgressHelper
from app.helper.rule import RuleHelper
from app.helper.subscribe import SubscribeHelper
from app.helper.system import SystemHelper
from app.helper.image import ImageHelper
from app.log import logger
from app.scheduler import Scheduler
from app.schemas import ConfigChangeEventData
@@ -45,7 +46,6 @@ from app.utils.crypto import HashUtils
from app.utils.http import RequestUtils, AsyncRequestUtils
from app.utils.security import SecurityUtils
from app.utils.url import UrlUtils
from pydantic import BaseModel
from version import APP_VERSION
router = APIRouter()
@@ -58,8 +58,6 @@ class LlmTestRequest(BaseModel):
provider: Optional[str] = None
model: Optional[str] = None
thinking_level: Optional[str] = None
disable_thinking: Optional[bool] = None
reasoning_effort: Optional[str] = None
api_key: Optional[str] = None
base_url: Optional[str] = None
@@ -271,94 +269,6 @@ def _build_nettest_rules() -> list[dict[str, Any]]:
return rules
def _build_llm_test_data(
duration_ms: Optional[int] = None,
provider: Optional[str] = None,
model: Optional[str] = None,
) -> dict[str, Any]:
"""
构造 LLM 测试接口的基础返回数据。
"""
data = {
"provider": provider if provider is not None else settings.LLM_PROVIDER,
"model": model if model is not None else settings.LLM_MODEL,
}
if duration_ms is not None:
data["duration_ms"] = duration_ms
return data
def _normalize_llm_test_value(
value: Optional[str], *, empty_as_none: bool = False
) -> Optional[str]:
"""
清理来自前端的 LLM 测试字段。
"""
if value is None:
return None
stripped = value.strip()
if empty_as_none and not stripped:
return None
return stripped
def _build_llm_test_snapshot(payload: Optional[LlmTestRequest] = None) -> dict[str, Any]:
"""
冻结当前 LLM 测试所需配置。
优先使用前端传入的临时参数;未传入时回退到已保存配置,兼容旧调用。
"""
provider = settings.LLM_PROVIDER
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))
reasoning_effort = _normalize_llm_test_value(
getattr(settings, "LLM_REASONING_EFFORT", None), empty_as_none=True
)
api_key = settings.LLM_API_KEY
base_url = settings.LLM_BASE_URL
enabled = bool(settings.AI_AGENT_ENABLE)
if payload:
if payload.enabled is not None:
enabled = bool(payload.enabled)
if payload.provider is not None:
provider = _normalize_llm_test_value(payload.provider) or ""
if payload.model is not None:
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:
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:
api_key = _normalize_llm_test_value(payload.api_key, empty_as_none=True)
if payload.base_url is not None:
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 {
"enabled": enabled,
"provider": provider,
"model": model,
"thinking_level": thinking_level,
"disable_thinking": disable_thinking,
"reasoning_effort": reasoning_effort,
"api_key": api_key,
"base_url": base_url,
}
def _sanitize_llm_test_error(message: str, api_key: Optional[str] = None) -> str:
"""
清理错误信息中的敏感字段,避免回显密钥。
@@ -450,12 +360,12 @@ async def _close_nettest_response(response: Any) -> None:
async def fetch_image(
url: str,
proxy: Optional[bool] = None,
use_cache: bool = False,
if_none_match: Optional[str] = None,
cookies: Optional[str | dict] = None,
allowed_domains: Optional[set[str]] = None,
url: str,
proxy: Optional[bool] = None,
use_cache: bool = False,
if_none_match: Optional[str] = None,
cookies: Optional[str | dict] = None,
allowed_domains: Optional[set[str]] = None,
) -> Optional[Response]:
"""
处理图片缓存逻辑支持HTTP缓存和磁盘缓存
@@ -477,6 +387,7 @@ async def fetch_image(
use_cache=use_cache,
cookies=cookies,
)
if content:
# 检查 If-None-Match
etag = HashUtils.md5(content)
@@ -489,16 +400,17 @@ async def fetch_image(
media_type=UrlUtils.get_mime_type(url, "image/jpeg"),
headers=headers,
)
return None
@router.get("/img/{proxy}", summary="图片代理")
async def proxy_img(
imgurl: str,
proxy: bool = False,
cache: bool = False,
use_cookies: bool = False,
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token),
imgurl: str,
proxy: bool = False,
cache: bool = False,
use_cookies: bool = False,
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token),
) -> Response:
"""
图片代理,可选是否使用代理服务器,支持 HTTP 缓存
@@ -527,9 +439,9 @@ async def proxy_img(
@router.get("/cache/image", summary="图片缓存")
async def cache_img(
url: str,
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token),
url: str,
if_none_match: Annotated[str | None, Header()] = None,
_: schemas.TokenPayload = Depends(verify_resource_token),
) -> Response:
"""
本地缓存图片文件,支持 HTTP 缓存,如果启用全局图片缓存,则使用磁盘缓存
@@ -623,7 +535,7 @@ async def get_env_setting(_: User = Depends(get_current_active_user_async)):
@router.post("/env", summary="更新系统配置", response_model=schemas.Response)
async def set_env_setting(
env: dict, _: User = Depends(get_current_active_superuser_async)
env: dict, _: User = Depends(get_current_active_superuser_async)
):
"""
更新系统环境变量(仅管理员)
@@ -658,9 +570,9 @@ async def set_env_setting(
@router.get("/progress/{process_type}", summary="实时进度")
async def get_progress(
request: Request,
process_type: str,
_: schemas.TokenPayload = Depends(verify_resource_token),
request: Request,
process_type: str,
_: schemas.TokenPayload = Depends(verify_resource_token),
):
"""
实时获取处理进度返回格式为SSE
@@ -695,9 +607,9 @@ async def get_setting(key: str, _: User = Depends(get_current_active_user_async)
@router.post("/setting/{key}", summary="更新系统设置", response_model=schemas.Response)
async def set_setting(
key: str,
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
_: User = Depends(get_current_active_superuser_async),
key: str,
value: Annotated[Union[list, dict, bool, int, str] | None, Body()] = None,
_: User = Depends(get_current_active_superuser_async),
):
"""
更新系统设置(仅管理员)
@@ -731,10 +643,10 @@ async def set_setting(
@router.get("/llm-models", summary="获取LLM模型列表", response_model=schemas.Response)
async def get_llm_models(
provider: str,
api_key: str,
base_url: Optional[str] = None,
_: User = Depends(get_current_active_user_async),
provider: str,
api_key: str,
base_url: Optional[str] = None,
_: User = Depends(get_current_active_user_async),
):
"""
获取LLM模型列表
@@ -750,28 +662,33 @@ async def get_llm_models(
@router.post("/llm-test", summary="测试LLM调用", response_model=schemas.Response)
async def llm_test(
payload: Annotated[Optional[LlmTestRequest], Body()] = None,
_: User = Depends(get_current_active_superuser_async),
payload: Annotated[Optional[LlmTestRequest], Body()] = None,
_: User = Depends(get_current_active_superuser_async),
):
"""
使用传入配置或当前已保存配置执行一次最小 LLM 调用。
"""
snapshot = _build_llm_test_snapshot(payload)
data = _build_llm_test_data(
provider=snapshot["provider"],
model=snapshot["model"],
)
if not snapshot["enabled"]:
if not payload:
return schemas.Response(success=False, message="请配置智能助手LLM相关参数后再进行测试")
if not payload.provider or not payload.model:
return schemas.Response(success=False, message="请配置LLM提供商和模型")
data = {
"provider": payload.provider,
"model": payload.model,
}
if not payload.enabled:
return schemas.Response(success=False, message="请先启用智能助手", data=data)
if not snapshot["api_key"]:
if not payload.api_key or not payload.api_key.strip():
return schemas.Response(
success=False,
message="请先配置 LLM API Key",
data=data,
)
if not (snapshot["model"] or "").strip():
if not payload.model or not payload.model.strip():
return schemas.Response(
success=False,
message="请先配置 LLM 模型",
@@ -780,52 +697,36 @@ async def llm_test(
try:
result = await LLMHelper.test_current_settings(
provider=snapshot["provider"],
model=snapshot["model"],
thinking_level=snapshot["thinking_level"],
disable_thinking=snapshot["disable_thinking"],
reasoning_effort=snapshot["reasoning_effort"],
api_key=snapshot["api_key"],
base_url=snapshot["base_url"],
provider=payload.provider,
model=payload.model,
thinking_level=payload.thinking_level,
api_key=payload.api_key,
base_url=payload.base_url,
)
if not result.get("reply_preview"):
return schemas.Response(
success=False,
message="模型响应为空",
data=_build_llm_test_data(
result.get("duration_ms"),
provider=snapshot["provider"],
model=snapshot["model"],
),
message="模型响应为空"
)
return schemas.Response(success=True, data=result)
except (LLMTestTimeout, TimeoutError) as err:
logger.warning(err)
return schemas.Response(
success=False,
message="LLM 调用超时",
data=_build_llm_test_data(
getattr(err, "duration_ms", None),
provider=snapshot["provider"],
model=snapshot["model"],
),
message="LLM 调用超时"
)
except Exception as err:
return schemas.Response(
success=False,
message=_sanitize_llm_test_error(str(err), snapshot["api_key"]),
data=_build_llm_test_data(
getattr(err, "duration_ms", None),
provider=snapshot["provider"],
model=snapshot["model"],
),
message=_sanitize_llm_test_error(str(err), payload.api_key)
)
@router.get("/message", summary="实时消息")
async def get_message(
request: Request,
role: Optional[str] = "system",
_: schemas.TokenPayload = Depends(verify_resource_token),
request: Request,
role: Optional[str] = "system",
_: schemas.TokenPayload = Depends(verify_resource_token),
):
"""
实时获取系统消息返回格式为SSE
@@ -848,10 +749,10 @@ async def get_message(
@router.get("/logging", summary="实时日志")
async def get_logging(
request: Request,
length: Optional[int] = 50,
logfile: Optional[str] = "moviepilot.log",
_: schemas.TokenPayload = Depends(verify_resource_token),
request: Request,
length: Optional[int] = 50,
logfile: Optional[str] = "moviepilot.log",
_: schemas.TokenPayload = Depends(verify_resource_token),
):
"""
实时获取系统日志
@@ -862,7 +763,7 @@ async def get_logging(
log_path = base_path / logfile
if not await SecurityUtils.async_is_safe_path(
base_path=base_path, user_path=log_path, allowed_suffixes={".log"}
base_path=base_path, user_path=log_path, allowed_suffixes={".log"}
):
raise HTTPException(status_code=404, detail="Not Found")
@@ -879,7 +780,7 @@ async def get_logging(
# 读取历史日志
async with aiofiles.open(
log_path, mode="r", encoding="utf-8", errors="ignore"
log_path, mode="r", encoding="utf-8", errors="ignore"
) as f:
# 优化大文件读取策略
if file_size > 100 * 1024:
@@ -891,7 +792,7 @@ async def get_logging(
# 找到第一个完整的行
first_newline = content.find("\n")
if first_newline != -1:
content = content[first_newline + 1 :]
content = content[first_newline + 1:]
else:
# 小文件直接读取全部内容
content = await f.read()
@@ -899,7 +800,7 @@ async def get_logging(
# 按行分割并添加到队列,只保留非空行
lines = [line.strip() for line in content.splitlines() if line.strip()]
# 只取最后N行
for line in lines[-max(length, 50) :]:
for line in lines[-max(length, 50):]:
lines_queue.append(line)
# 输出历史日志
@@ -908,7 +809,7 @@ async def get_logging(
# 实时监听新日志
async with aiofiles.open(
log_path, mode="r", encoding="utf-8", errors="ignore"
log_path, mode="r", encoding="utf-8", errors="ignore"
) as f:
# 移动文件指针到文件末尾,继续监听新增内容
await f.seek(0, 2)
@@ -947,7 +848,7 @@ async def get_logging(
try:
# 使用 aiofiles 异步读取文件
async with aiofiles.open(
log_path, mode="r", encoding="utf-8", errors="ignore"
log_path, mode="r", encoding="utf-8", errors="ignore"
) as file:
text = await file.read()
# 倒序输出
@@ -979,10 +880,10 @@ async def latest_version(_: schemas.TokenPayload = Depends(verify_token)):
@router.get("/ruletest", summary="过滤规则测试", response_model=schemas.Response)
def ruletest(
title: str,
rulegroup_name: str,
subtitle: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token),
title: str,
rulegroup_name: str,
subtitle: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token),
):
"""
过滤规则测试,规则类型 1-订阅2-洗版3-搜索
@@ -1037,11 +938,10 @@ async def nettest_targets(_: schemas.TokenPayload = Depends(verify_token)):
@router.get("/nettest", summary="测试网络连通性")
async def nettest(
target_id: Optional[str] = None,
url: Optional[str] = None,
proxy: Optional[bool] = None,
include: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token),
target_id: Optional[str] = None,
url: Optional[str] = None,
include: Optional[str] = None,
_: schemas.TokenPayload = Depends(verify_token),
):
"""
测试内置目标的网络连通性。

View File

@@ -505,12 +505,8 @@ class ConfigModel(BaseModel):
LLM_PROVIDER: str = "deepseek"
# LLM模型名称
LLM_MODEL: str = "deepseek-chat"
# 统一思考模式/深度配置off/auto/minimal/low/medium/high/max/xhigh
LLM_THINKING_LEVEL: Optional[str] = None
# 兼容旧配置:是否尽量关闭模型的思考/推理能力(新配置优先)
LLM_DISABLE_THINKING: bool = True
# 兼容旧配置:思考强度(新配置优先)
LLM_REASONING_EFFORT: Optional[str] = None
# 思考模式/深度配置off/auto/minimal/low/medium/high/max/xhigh
LLM_THINKING_LEVEL: Optional[str] = 'off'
# LLM是否支持图片输入开启后消息图片会按多模态输入发送给模型
LLM_SUPPORT_IMAGE_INPUT: bool = True
# LLM API密钥

View File

@@ -6,6 +6,8 @@ import time
from functools import wraps
from typing import Any, List
from langchain_core.messages import convert_to_messages
from app.core.config import settings
from app.log import logger
@@ -71,7 +73,8 @@ def _get_httpx_proxy_key() -> str:
if "proxy" in params:
return "proxy"
return "proxies"
except Exception:
except Exception as e:
logger.warning(f"检测 httpx 代理参数失败,默认使用 'proxies'{e}")
return "proxies"
@@ -111,20 +114,6 @@ def _is_deepseek_thinking_enabled(model_name: str | None, extra_body: Any) -> bo
return False
def _extract_input_messages(input_: Any) -> list[Any]:
"""
将 chat model 输入还原为原始 BaseMessage 序列。
"""
try:
from langchain_core.messages import convert_to_messages
return list(convert_to_messages(input_))
except Exception:
if isinstance(input_, list):
return list(input_)
return []
def _patch_deepseek_reasoning_content_support():
"""
修补 langchain-deepseek 在 tool-call 场景下遗漏 reasoning_content 回传的问题。
@@ -153,7 +142,7 @@ def _patch_deepseek_reasoning_content_support():
payload = original_get_request_payload(self, input_, stop=stop, **kwargs)
try:
original_messages = _extract_input_messages(input_)
original_messages = convert_to_messages(input_)
payload_messages = payload.get("messages") or []
model_name = getattr(self, "model_name", None) or getattr(
self, "model", None
@@ -180,8 +169,8 @@ def _patch_deepseek_reasoning_content_support():
reasoning_content = ""
if index < len(original_messages):
additional_kwargs = (
getattr(original_messages[index], "additional_kwargs", None)
or {}
getattr(original_messages[index], "additional_kwargs", None)
or {}
)
if isinstance(additional_kwargs, dict):
captured_reasoning = additional_kwargs.get("reasoning_content")
@@ -189,9 +178,9 @@ def _patch_deepseek_reasoning_content_support():
reasoning_content = captured_reasoning
message["reasoning_content"] = reasoning_content
except Exception as err:
except Exception as e:
logger.warning(
f"修补 langchain-deepseek reasoning_content 请求载荷时失败,将继续使用原始载荷: {err}"
f"修补 langchain-deepseek reasoning_content 请求载荷时失败,将继续使用原始载荷: {e}"
)
return payload
@@ -208,15 +197,6 @@ class LLMHelper:
{"off", "auto", "minimal", "low", "medium", "high", "max", "xhigh"}
)
@staticmethod
def _should_disable_thinking(disable_thinking: bool | None = None) -> bool:
"""
判断本次调用是否应尝试关闭模型思考能力。
"""
if disable_thinking is not None:
return bool(disable_thinking)
return bool(getattr(settings, "LLM_DISABLE_THINKING", False))
@staticmethod
def _normalize_model_name(model_name: str | None) -> str:
"""
@@ -224,147 +204,42 @@ class LLMHelper:
"""
return (model_name or "").strip().lower()
@classmethod
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
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"}:
if not thinking_level or thinking_level in {"off", "auto"}:
return None
if normalized in {"minimal", "low", "medium", "high"}:
if thinking_level in {"minimal", "low", "medium", "high"}:
return "high"
if normalized in {"max", "xhigh"}:
if thinking_level in {"max", "xhigh"}:
return "max"
logger.warning(f"忽略不支持的 DeepSeek reasoning_effort 配置: {normalized}")
logger.warning(f"忽略不支持的 DeepSeek reasoning_effort 配置: {thinking_level}")
return None
@classmethod
def _normalize_openai_reasoning_effort(
cls, thinking_level: str | None = None
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":
if not thinking_level or thinking_level == "auto":
return None
if normalized == "off":
if thinking_level == "off":
return "none"
if normalized == "max":
if thinking_level == "max":
return "xhigh"
return normalized
return thinking_level
@classmethod
def _build_google_thinking_kwargs(
cls, model_name: str, thinking_level: str
cls, model_name: str, thinking_level: str
) -> dict[str, Any]:
"""
Gemini 3 使用 thinking_levelGemini 2.5 使用 thinking_budget。
@@ -427,7 +302,7 @@ class LLMHelper:
@classmethod
def _build_kimi_thinking_kwargs(
cls, model_name: str, thinking_level: str
cls, model_name: str, thinking_level: str
) -> dict[str, Any]:
"""
Kimi 当前公开文档仅支持思考开关,不支持显式深度调节。
@@ -440,12 +315,10 @@ class LLMHelper:
@classmethod
def _build_thinking_kwargs(
cls,
provider: str,
model: str | None,
thinking_level: str | None = None,
disable_thinking: bool | None = None,
reasoning_effort: str | None = None,
cls,
provider: str,
model: str | None,
thinking_level: str | None = None
) -> dict[str, Any]:
"""
按 provider/model 生成思考模式相关参数。
@@ -455,45 +328,40 @@ class LLMHelper:
"""
provider_name = (provider or "").strip().lower()
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":
if thinking_level == "off":
return {"extra_body": {"thinking": {"type": "disabled"}}}
if resolved_thinking_level == "auto":
if thinking_level == "auto":
return {}
kwargs: dict[str, Any] = {"extra_body": {"thinking": {"type": "enabled"}}}
deepseek_effort = cls._normalize_deepseek_reasoning_effort(
resolved_thinking_level
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)
return cls._build_kimi_thinking_kwargs(model_name, thinking_level)
if not model_name:
return {}
# OpenAI 原生推理模型优先走 LangChain 内置 reasoning_effort。
if provider_name == "openai" and model_name.startswith(
("gpt-5", "o1", "o3", "o4")
("gpt-5", "o1", "o3", "o4")
):
openai_effort = cls._normalize_openai_reasoning_effort(
resolved_thinking_level
thinking_level
)
return {"reasoning_effort": openai_effort} if openai_effort else {}
# Gemini 使用 google-genai / langchain-google-genai 内置思考控制参数。
if provider_name == "google":
return cls._build_google_thinking_kwargs(
model_name, resolved_thinking_level
model_name, thinking_level
)
return {}
@@ -507,18 +375,26 @@ class LLMHelper:
@staticmethod
def get_llm(
streaming: bool = False,
provider: str | None = None,
model: str | None = None,
thinking_level: str | None = None,
disable_thinking: bool | None = None,
reasoning_effort: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
streaming: bool = False,
provider: str | None = None,
model: str | None = None,
thinking_level: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
):
"""
获取LLM实例
:param streaming: 是否启用流式输出
:param provider: LLM提供商默认为配置项LLM_PROVIDER
:param model: 模型名称默认为配置项LLM_MODEL
:param thinking_level: 思考模式级别,默认为 None即自动判断
是否启用思考模式)。支持的级别包括 "off"(关闭)、"auto"(自动)、"minimal""low""medium""high""max"/"xhigh"(最大)。
不同模型对思考模式的支持和表现不同,具体映射关系请
参考代码实现。对于不支持思考模式的模型,该参数将被忽略。
:param api_key: API Key默认为
配置项LLM_API_KEY。对于某些提供商
如 DeepSeek可能需要同时提供 base_url。
:param base_url: API Base URL默认为配置项LLM_BASE_URL。
:return: LLM实例
"""
provider_name = str(
@@ -530,9 +406,7 @@ class LLMHelper:
thinking_kwargs = LLMHelper._build_thinking_kwargs(
provider=provider_name,
model=model_name,
thinking_level=thinking_level,
disable_thinking=disable_thinking,
reasoning_effort=reasoning_effort,
thinking_level=thinking_level
)
if not api_key_value:
@@ -596,7 +470,7 @@ class LLMHelper:
else:
model.profile = {
"max_input_tokens": settings.LLM_MAX_CONTEXT_TOKENS
* 1000, # 转换为token单位
* 1000, # 转换为token单位
}
return model
@@ -620,10 +494,10 @@ class LLMHelper:
if isinstance(block, dict) or hasattr(block, "get"):
block_type = block.get("type")
if block.get("thought") or block_type in (
"thinking",
"reasoning_content",
"reasoning",
"thought",
"thinking",
"reasoning_content",
"reasoning",
"thought",
):
continue
if block_type == "text":
@@ -643,15 +517,13 @@ class LLMHelper:
@staticmethod
async def test_current_settings(
prompt: str = "请只回复 OK",
timeout: int = 20,
provider: str | None = None,
model: str | None = None,
thinking_level: str | None = None,
disable_thinking: bool | None = None,
reasoning_effort: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
prompt: str = "请只回复 OK",
timeout: int = 20,
provider: str | None = None,
model: str | None = None,
thinking_level: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
) -> dict:
"""
使用当前已保存配置执行一次最小 LLM 调用。
@@ -666,8 +538,6 @@ class LLMHelper:
provider=provider_name,
model=model_name,
thinking_level=thinking_level,
disable_thinking=disable_thinking,
reasoning_effort=reasoning_effort,
api_key=api_key_value,
base_url=base_url_value,
)
@@ -695,7 +565,7 @@ class LLMHelper:
return data
def get_models(
self, provider: str, api_key: str, base_url: str = None
self, provider: str, api_key: str, base_url: str = None
) -> List[str]:
"""获取模型列表"""
logger.info(f"获取 {provider} 模型列表...")
@@ -733,7 +603,7 @@ class LLMHelper:
@staticmethod
def _get_openai_compatible_models(
provider: str, api_key: str, base_url: str = None
provider: str, api_key: str, base_url: str = None
) -> List[str]:
"""获取OpenAI兼容模型列表"""
try:

View File

@@ -1086,14 +1086,6 @@ def _env_llm_thinking_level_default() -> str:
"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"
@@ -1550,7 +1542,7 @@ def _load_auth_site_definitions_inner() -> dict[str, Any]:
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
from app.helper.sites import SitesHelper
from app.helper.sites import SitesHelper # noqa
auth_sites = SitesHelper().get_authsites() or {}
definitions: dict[str, Any] = {}
@@ -1887,7 +1879,7 @@ def _apply_local_system_config_inner(config_payload: dict[str, Any]) -> None:
):
system_config.set(SystemConfigKey.UserSiteAuthParams, site_auth_item)
try:
from app.helper.sites import SitesHelper
from app.helper.sites import SitesHelper # noqa
status, msg = SitesHelper().check_user(
site_auth_item.get("site"), site_auth_item.get("params")

View File

@@ -72,8 +72,6 @@ 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_DISABLE_THINKING = False
sys.modules["app.core.config"].settings.LLM_REASONING_EFFORT = 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

View File

@@ -39,8 +39,6 @@ _stub_module(
LLM_API_KEY="global-key",
LLM_BASE_URL="https://global.example.com",
LLM_THINKING_LEVEL=None,
LLM_DISABLE_THINKING=False,
LLM_REASONING_EFFORT=None,
LLM_TEMPERATURE=0.1,
LLM_MAX_CONTEXT_TOKENS=64,
PROXY_HOST=None,