From a05ffc07d41301ae1e7cb9981ff2fe103b010ec4 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 25 Apr 2026 10:42:03 +0800 Subject: [PATCH] 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 --- app/api/endpoints/system.py | 252 +++++++---------------- app/core/config.py | 8 +- app/helper/llm.py | 256 ++++++------------------ scripts/local_setup.py | 12 +- tests/test_langchain_deepseek_compat.py | 2 - tests/test_llm_helper_testcall.py | 2 - 6 files changed, 143 insertions(+), 389 deletions(-) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 570ddf1c..22d7bccf 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -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), ): """ 测试内置目标的网络连通性。 diff --git a/app/core/config.py b/app/core/config.py index 76576ade..84861bd1 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -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密钥 diff --git a/app/helper/llm.py b/app/helper/llm.py index b78369a8..f4587a43 100644 --- a/app/helper/llm.py +++ b/app/helper/llm.py @@ -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_level;Gemini 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: diff --git a/scripts/local_setup.py b/scripts/local_setup.py index ddad23a1..4e98138a 100644 --- a/scripts/local_setup.py +++ b/scripts/local_setup.py @@ -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") diff --git a/tests/test_langchain_deepseek_compat.py b/tests/test_langchain_deepseek_compat.py index b22d4d8a..2635f132 100644 --- a/tests/test_langchain_deepseek_compat.py +++ b/tests/test_langchain_deepseek_compat.py @@ -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 diff --git a/tests/test_llm_helper_testcall.py b/tests/test_llm_helper_testcall.py index 7b6000ca..6da49b5a 100644 --- a/tests/test_llm_helper_testcall.py +++ b/tests/test_llm_helper_testcall.py @@ -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,