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

View File

@@ -505,12 +505,8 @@ class ConfigModel(BaseModel):
LLM_PROVIDER: str = "deepseek" LLM_PROVIDER: str = "deepseek"
# LLM模型名称 # LLM模型名称
LLM_MODEL: str = "deepseek-chat" LLM_MODEL: str = "deepseek-chat"
# 统一思考模式/深度配置off/auto/minimal/low/medium/high/max/xhigh # 思考模式/深度配置off/auto/minimal/low/medium/high/max/xhigh
LLM_THINKING_LEVEL: Optional[str] = None LLM_THINKING_LEVEL: Optional[str] = 'off'
# 兼容旧配置:是否尽量关闭模型的思考/推理能力(新配置优先)
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密钥

View File

@@ -6,6 +6,8 @@ import time
from functools import wraps from functools import wraps
from typing import Any, List from typing import Any, List
from langchain_core.messages import convert_to_messages
from app.core.config import settings from app.core.config import settings
from app.log import logger from app.log import logger
@@ -71,7 +73,8 @@ def _get_httpx_proxy_key() -> str:
if "proxy" in params: if "proxy" in params:
return "proxy" return "proxy"
return "proxies" return "proxies"
except Exception: except Exception as e:
logger.warning(f"检测 httpx 代理参数失败,默认使用 'proxies'{e}")
return "proxies" return "proxies"
@@ -111,20 +114,6 @@ def _is_deepseek_thinking_enabled(model_name: str | None, extra_body: Any) -> bo
return False 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(): def _patch_deepseek_reasoning_content_support():
""" """
修补 langchain-deepseek 在 tool-call 场景下遗漏 reasoning_content 回传的问题。 修补 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) payload = original_get_request_payload(self, input_, stop=stop, **kwargs)
try: try:
original_messages = _extract_input_messages(input_) original_messages = convert_to_messages(input_)
payload_messages = payload.get("messages") or [] payload_messages = payload.get("messages") or []
model_name = getattr(self, "model_name", None) or getattr( model_name = getattr(self, "model_name", None) or getattr(
self, "model", None self, "model", None
@@ -180,8 +169,8 @@ def _patch_deepseek_reasoning_content_support():
reasoning_content = "" reasoning_content = ""
if index < len(original_messages): if index < len(original_messages):
additional_kwargs = ( additional_kwargs = (
getattr(original_messages[index], "additional_kwargs", None) getattr(original_messages[index], "additional_kwargs", None)
or {} or {}
) )
if isinstance(additional_kwargs, dict): if isinstance(additional_kwargs, dict):
captured_reasoning = additional_kwargs.get("reasoning_content") captured_reasoning = additional_kwargs.get("reasoning_content")
@@ -189,9 +178,9 @@ def _patch_deepseek_reasoning_content_support():
reasoning_content = captured_reasoning reasoning_content = captured_reasoning
message["reasoning_content"] = reasoning_content message["reasoning_content"] = reasoning_content
except Exception as err: except Exception as e:
logger.warning( logger.warning(
f"修补 langchain-deepseek reasoning_content 请求载荷时失败,将继续使用原始载荷: {err}" f"修补 langchain-deepseek reasoning_content 请求载荷时失败,将继续使用原始载荷: {e}"
) )
return payload return payload
@@ -208,15 +197,6 @@ class LLMHelper:
{"off", "auto", "minimal", "low", "medium", "high", "max", "xhigh"} {"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 @staticmethod
def _normalize_model_name(model_name: str | None) -> str: def _normalize_model_name(model_name: str | None) -> str:
""" """
@@ -224,147 +204,42 @@ class LLMHelper:
""" """
return (model_name or "").strip().lower() 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 @classmethod
def _normalize_deepseek_reasoning_effort( def _normalize_deepseek_reasoning_effort(
cls, thinking_level: str | None = None cls, thinking_level: str | None = None
) -> str | None: ) -> str | None:
""" """
DeepSeek 文档当前建议使用 high/max兼容常见 effort 别名。 DeepSeek 文档当前建议使用 high/max兼容常见 effort 别名。
""" """
normalized = cls._normalize_thinking_level(thinking_level) if not thinking_level or thinking_level in {"off", "auto"}:
if not normalized or normalized in {"off", "auto"}:
return None return None
if normalized in {"minimal", "low", "medium", "high"}: if thinking_level in {"minimal", "low", "medium", "high"}:
return "high" return "high"
if normalized in {"max", "xhigh"}: if thinking_level in {"max", "xhigh"}:
return "max" return "max"
logger.warning(f"忽略不支持的 DeepSeek reasoning_effort 配置: {normalized}") logger.warning(f"忽略不支持的 DeepSeek reasoning_effort 配置: {thinking_level}")
return None return None
@classmethod @classmethod
def _normalize_openai_reasoning_effort( def _normalize_openai_reasoning_effort(
cls, thinking_level: str | None = None cls, thinking_level: str | None = None
) -> str | None: ) -> str | None:
""" """
OpenAI reasoning_effort 支持更细粒度的 effort统一做最近似映射。 OpenAI reasoning_effort 支持更细粒度的 effort统一做最近似映射。
""" """
normalized = cls._normalize_thinking_level(thinking_level) if not thinking_level or thinking_level == "auto":
if not normalized or normalized == "auto":
return None return None
if normalized == "off": if thinking_level == "off":
return "none" return "none"
if normalized == "max": if thinking_level == "max":
return "xhigh" return "xhigh"
return normalized return thinking_level
@classmethod @classmethod
def _build_google_thinking_kwargs( def _build_google_thinking_kwargs(
cls, model_name: str, thinking_level: str cls, model_name: str, thinking_level: str
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Gemini 3 使用 thinking_levelGemini 2.5 使用 thinking_budget。 Gemini 3 使用 thinking_levelGemini 2.5 使用 thinking_budget。
@@ -427,7 +302,7 @@ class LLMHelper:
@classmethod @classmethod
def _build_kimi_thinking_kwargs( def _build_kimi_thinking_kwargs(
cls, model_name: str, thinking_level: str cls, model_name: str, thinking_level: str
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
Kimi 当前公开文档仅支持思考开关,不支持显式深度调节。 Kimi 当前公开文档仅支持思考开关,不支持显式深度调节。
@@ -440,12 +315,10 @@ class LLMHelper:
@classmethod @classmethod
def _build_thinking_kwargs( def _build_thinking_kwargs(
cls, cls,
provider: str, provider: str,
model: str | None, model: str | None,
thinking_level: str | None = None, thinking_level: str | None = None
disable_thinking: bool | None = None,
reasoning_effort: str | None = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
""" """
按 provider/model 生成思考模式相关参数。 按 provider/model 生成思考模式相关参数。
@@ -455,45 +328,40 @@ class LLMHelper:
""" """
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 provider_name == "deepseek":
if resolved_thinking_level == "off": if thinking_level == "off":
return {"extra_body": {"thinking": {"type": "disabled"}}} return {"extra_body": {"thinking": {"type": "disabled"}}}
if resolved_thinking_level == "auto": if thinking_level == "auto":
return {} return {}
kwargs: dict[str, Any] = {"extra_body": {"thinking": {"type": "enabled"}}} kwargs: dict[str, Any] = {"extra_body": {"thinking": {"type": "enabled"}}}
deepseek_effort = cls._normalize_deepseek_reasoning_effort( deepseek_effort = cls._normalize_deepseek_reasoning_effort(
resolved_thinking_level thinking_level
) )
if deepseek_effort: if deepseek_effort:
kwargs["reasoning_effort"] = deepseek_effort kwargs["reasoning_effort"] = deepseek_effort
return kwargs return kwargs
if model_name.startswith(("kimi-k2.5", "kimi-k2.6", "kimi-k2-thinking")): 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: if not model_name:
return {} return {}
# 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")
): ):
openai_effort = cls._normalize_openai_reasoning_effort( openai_effort = cls._normalize_openai_reasoning_effort(
resolved_thinking_level thinking_level
) )
return {"reasoning_effort": openai_effort} if openai_effort else {} 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":
return cls._build_google_thinking_kwargs( return cls._build_google_thinking_kwargs(
model_name, resolved_thinking_level model_name, thinking_level
) )
return {} return {}
@@ -507,18 +375,26 @@ class LLMHelper:
@staticmethod @staticmethod
def get_llm( def get_llm(
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, thinking_level: str | None = None,
disable_thinking: bool | None = None, api_key: str | None = None,
reasoning_effort: str | None = None, base_url: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
): ):
""" """
获取LLM实例 获取LLM实例
:param streaming: 是否启用流式输出 :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实例 :return: LLM实例
""" """
provider_name = str( provider_name = str(
@@ -530,9 +406,7 @@ class LLMHelper:
thinking_kwargs = LLMHelper._build_thinking_kwargs( thinking_kwargs = LLMHelper._build_thinking_kwargs(
provider=provider_name, provider=provider_name,
model=model_name, model=model_name,
thinking_level=thinking_level, thinking_level=thinking_level
disable_thinking=disable_thinking,
reasoning_effort=reasoning_effort,
) )
if not api_key_value: if not api_key_value:
@@ -596,7 +470,7 @@ class LLMHelper:
else: else:
model.profile = { model.profile = {
"max_input_tokens": settings.LLM_MAX_CONTEXT_TOKENS "max_input_tokens": settings.LLM_MAX_CONTEXT_TOKENS
* 1000, # 转换为token单位 * 1000, # 转换为token单位
} }
return model return model
@@ -620,10 +494,10 @@ class LLMHelper:
if isinstance(block, dict) or hasattr(block, "get"): if isinstance(block, dict) or hasattr(block, "get"):
block_type = block.get("type") block_type = block.get("type")
if block.get("thought") or block_type in ( if block.get("thought") or block_type in (
"thinking", "thinking",
"reasoning_content", "reasoning_content",
"reasoning", "reasoning",
"thought", "thought",
): ):
continue continue
if block_type == "text": if block_type == "text":
@@ -643,15 +517,13 @@ class LLMHelper:
@staticmethod @staticmethod
async def test_current_settings( async def test_current_settings(
prompt: str = "请只回复 OK", prompt: str = "请只回复 OK",
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, thinking_level: str | None = None,
disable_thinking: bool | None = None, api_key: str | None = None,
reasoning_effort: str | None = None, base_url: str | None = None,
api_key: str | None = None,
base_url: str | None = None,
) -> dict: ) -> dict:
""" """
使用当前已保存配置执行一次最小 LLM 调用。 使用当前已保存配置执行一次最小 LLM 调用。
@@ -666,8 +538,6 @@ class LLMHelper:
provider=provider_name, provider=provider_name,
model=model_name, model=model_name,
thinking_level=thinking_level, thinking_level=thinking_level,
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,
) )
@@ -695,7 +565,7 @@ class LLMHelper:
return data return data
def get_models( 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]: ) -> List[str]:
"""获取模型列表""" """获取模型列表"""
logger.info(f"获取 {provider} 模型列表...") logger.info(f"获取 {provider} 模型列表...")
@@ -733,7 +603,7 @@ class LLMHelper:
@staticmethod @staticmethod
def _get_openai_compatible_models( 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]: ) -> List[str]:
"""获取OpenAI兼容模型列表""" """获取OpenAI兼容模型列表"""
try: try:

View File

@@ -1086,14 +1086,6 @@ def _env_llm_thinking_level_default() -> str:
"xhigh", "xhigh",
}: }:
return normalized 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" return "auto"
@@ -1550,7 +1542,7 @@ def _load_auth_site_definitions_inner() -> dict[str, Any]:
if str(ROOT) not in sys.path: if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT)) 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 {} auth_sites = SitesHelper().get_authsites() or {}
definitions: dict[str, Any] = {} 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) system_config.set(SystemConfigKey.UserSiteAuthParams, site_auth_item)
try: try:
from app.helper.sites import SitesHelper from app.helper.sites import SitesHelper # noqa
status, msg = SitesHelper().check_user( status, msg = SitesHelper().check_user(
site_auth_item.get("site"), site_auth_item.get("params") 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_API_KEY = "sk-test"
sys.modules["app.core.config"].settings.LLM_BASE_URL = "https://api.deepseek.com" 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_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_TEMPERATURE = 0.1
sys.modules["app.core.config"].settings.LLM_MAX_CONTEXT_TOKENS = 64 sys.modules["app.core.config"].settings.LLM_MAX_CONTEXT_TOKENS = 64
sys.modules["app.core.config"].settings.PROXY_HOST = None sys.modules["app.core.config"].settings.PROXY_HOST = None

View File

@@ -39,8 +39,6 @@ _stub_module(
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_THINKING_LEVEL=None,
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,