diff --git a/backend/open_webui/events.py b/backend/open_webui/events.py index 71f9f15f1b..4aa30f9128 100644 --- a/backend/open_webui/events.py +++ b/backend/open_webui/events.py @@ -8,7 +8,7 @@ import uuid from types import SimpleNamespace from typing import Any -from open_webui.env import VERSION +from open_webui.env import ENABLE_PLUGINS, VERSION from open_webui.models.config import Config from pydantic import BaseModel, ConfigDict, Field, model_validator from open_webui.retrieval.web.utils import validate_url @@ -1025,6 +1025,9 @@ class WebhookEventSink: async def dispatch_event_functions(app: Any, event: Event, request: Any | None = None) -> None: + if not ENABLE_PLUGINS: + return + from open_webui.models.functions import Functions from open_webui.utils.plugin import get_function_module_from_cache diff --git a/backend/open_webui/functions.py b/backend/open_webui/functions.py index 4a82cf7f26..0cbfa8b980 100644 --- a/backend/open_webui/functions.py +++ b/backend/open_webui/functions.py @@ -20,7 +20,7 @@ from starlette.responses import Response, StreamingResponse from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL from open_webui.constants import ERROR_MESSAGES -from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL from open_webui.models.functions import Functions from open_webui.models.models import Models from open_webui.models.users import UserModel @@ -69,6 +69,9 @@ async def get_function_module_by_id(request: Request, pipe_id: str): async def get_function_models(request): + if not ENABLE_PLUGINS: + return [] + pipes = await Functions.get_functions_by_type('pipe', active_only=True) pipe_models = [] diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 82eed2f4d6..887594829c 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -83,6 +83,7 @@ from open_webui.env import ( ENABLE_COMPRESSION_MIDDLEWARE, ENABLE_CUSTOM_MODEL_FALLBACK, ENABLE_EASTER_EGGS, + ENABLE_PLUGINS, EXTERNAL_PWA_MANIFEST_URL, # OAuth Back-Channel Logout ENABLE_OAUTH_BACKCHANNEL_LOGOUT, @@ -1927,6 +1928,7 @@ async def get_app_config(request: Request): 'enable_public_active_users_count': ENABLE_PUBLIC_ACTIVE_USERS_COUNT, 'enable_easter_eggs': ENABLE_EASTER_EGGS, 'enable_direct_connections': config.get('direct.enable'), + 'enable_plugins': ENABLE_PLUGINS, 'enable_folders': config.get('folders.enable'), 'folder_max_file_count': config.get('folders.max_file_count'), 'enable_channels': config.get('channels.enable'), diff --git a/backend/open_webui/routers/functions.py b/backend/open_webui/routers/functions.py index 8917bb37f0..93d2879c18 100644 --- a/backend/open_webui/routers/functions.py +++ b/backend/open_webui/routers/functions.py @@ -10,7 +10,7 @@ import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.config import CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_PLUGINS from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.functions import ( @@ -46,11 +46,17 @@ router = APIRouter() @router.get('/', response_model=list[FunctionResponse]) async def get_functions(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_functions(db=db) @router.get('/list', response_model=list[FunctionUserResponse]) async def get_function_list(user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_function_list(db=db) @@ -65,6 +71,9 @@ async def get_functions( user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session), ): + if not ENABLE_PLUGINS: + return [] + return await Functions.get_functions(include_valves=include_valves, db=db) diff --git a/backend/open_webui/routers/tools.py b/backend/open_webui/routers/tools.py index c830753b5a..b8ef51a280 100644 --- a/backend/open_webui/routers/tools.py +++ b/backend/open_webui/routers/tools.py @@ -10,7 +10,7 @@ import aiohttp from fastapi import APIRouter, Depends, HTTPException, Request, status from open_webui.config import BYPASS_ADMIN_ACCESS_CONTROL, CACHE_DIR from open_webui.constants import ERROR_MESSAGES -from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT +from open_webui.env import AIOHTTP_CLIENT_SESSION_SSL, AIOHTTP_CLIENT_TIMEOUT, ENABLE_PLUGINS from open_webui.events import EVENTS, publish_event from open_webui.internal.db import get_async_session from open_webui.models.access_grants import AccessGrants @@ -72,20 +72,23 @@ async def get_tools( tools = [] # Local Tools - tools_cache = get_tools_cache(request) - for tool in await Tools.get_tools(defer_content=True, db=db): - tool_module = tools_cache.get(tool.id) - has_user_valves = ( - hasattr(tool_module, 'UserValves') if tool_module else (tool.meta.has_user_valves if tool.meta else False) - ) - tools.append( - ToolUserResponse( - **{ - **tool.model_dump(), - 'has_user_valves': has_user_valves, - } + if ENABLE_PLUGINS: + tools_cache = get_tools_cache(request) + for tool in await Tools.get_tools(defer_content=True, db=db): + tool_module = tools_cache.get(tool.id) + has_user_valves = ( + hasattr(tool_module, 'UserValves') + if tool_module + else (tool.meta.has_user_valves if tool.meta else False) + ) + tools.append( + ToolUserResponse( + **{ + **tool.model_dump(), + 'has_user_valves': has_user_valves, + } + ) ) - ) # OpenAPI Tool Servers server_access_grants = {} @@ -199,6 +202,9 @@ async def get_tools( @router.get('/list', response_model=list[ToolAccessResponse]) async def get_tool_list(user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session)): + if not ENABLE_PLUGINS: + return [] + if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: tools = await Tools.get_tools(defer_content=True, db=db) else: diff --git a/backend/open_webui/utils/actions.py b/backend/open_webui/utils/actions.py index eb5a84d0d4..585a409b72 100644 --- a/backend/open_webui/utils/actions.py +++ b/backend/open_webui/utils/actions.py @@ -4,7 +4,7 @@ import sys from typing import Any from fastapi import Request -from open_webui.env import GLOBAL_LOG_LEVEL +from open_webui.env import ENABLE_PLUGINS, GLOBAL_LOG_LEVEL from open_webui.models.functions import Functions from open_webui.models.users import UserModel from open_webui.socket.main import get_event_call, get_event_emitter @@ -17,6 +17,9 @@ log = logging.getLogger(__name__) async def chat_action(request: Request, action_id: str, form_data: dict, user: Any): + if not ENABLE_PLUGINS: + raise Exception('Plugins are disabled by ENABLE_PLUGINS=false') + if '.' in action_id: action_id, sub_action_id = action_id.split('.') else: diff --git a/backend/open_webui/utils/filter.py b/backend/open_webui/utils/filter.py index 84aebdaacb..98eb24dd02 100644 --- a/backend/open_webui/utils/filter.py +++ b/backend/open_webui/utils/filter.py @@ -1,6 +1,7 @@ import inspect import logging +from open_webui.env import ENABLE_PLUGINS from open_webui.models.functions import Functions from open_webui.utils.plugin import ( get_function_module_from_cache, @@ -19,6 +20,9 @@ async def get_function_module(request, function_id, load_from_db=True): async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = None): + if not ENABLE_PLUGINS: + return [] + async def get_priority(function_id): try: function_module = await get_function_module(request, function_id) @@ -64,6 +68,9 @@ async def get_sorted_filter_ids(request, model: dict, enabled_filter_ids: list = # Grant these filters the discernment to pass what serves # and refuse what harms, for every soul in the house. async def process_filter_functions(request, filter_functions, filter_type, form_data, extra_params): + if not ENABLE_PLUGINS: + return form_data, {} + skip_files = None for function in filter_functions: diff --git a/backend/open_webui/utils/middleware.py b/backend/open_webui/utils/middleware.py index b507aa66c5..349ad06f1a 100644 --- a/backend/open_webui/utils/middleware.py +++ b/backend/open_webui/utils/middleware.py @@ -33,6 +33,7 @@ from open_webui.env import ( CHAT_RESPONSE_STREAM_DELTA_CHUNK_SIZE, ENABLE_API_OUTLET_FILTERS, ENABLE_CHAT_RESPONSE_BASE64_IMAGE_URL_CONVERSION, + ENABLE_PLUGINS, ENABLE_QUERIES_CACHE, ENABLE_REALTIME_CHAT_SAVE, ENABLE_RESPONSES_API_STATEFUL, @@ -2421,19 +2422,20 @@ async def process_chat_payload(request, form_data, user, metadata, model): except Exception as e: raise e - try: - filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - filter_functions = await Functions.get_functions_by_ids(filter_ids) + if ENABLE_PLUGINS: + try: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) - form_data, flags = await process_filter_functions( - request=request, - filter_functions=filter_functions, - filter_type='inlet', - form_data=form_data, - extra_params=extra_params, - ) - except Exception as e: - raise Exception(f'{e}') + form_data, flags = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='inlet', + form_data=form_data, + extra_params=extra_params, + ) + except Exception as e: + raise Exception(f'{e}') features = form_data.pop('features', None) or {} extra_params['__features__'] = features @@ -2618,6 +2620,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): mcp_tools_dict = {} if tool_ids: + db_tool_ids = [] for tool_id in tool_ids: if tool_id.startswith('server:mcp:'): try: @@ -2669,18 +2672,21 @@ async def process_chat_payload(request, form_data, user, metadata, model): } ) continue + elif ENABLE_PLUGINS: + db_tool_ids.append(tool_id) - tools_dict = await get_tools( - request, - tool_ids, - user, - { - **extra_params, - '__model__': models[task_model_id], - '__messages__': form_data['messages'], - '__files__': metadata.get('files', []), - }, - ) + if db_tool_ids: + tools_dict = await get_tools( + request, + db_tool_ids, + user, + { + **extra_params, + '__model__': models[task_model_id], + '__messages__': form_data['messages'], + '__files__': metadata.get('files', []), + }, + ) if mcp_tools_dict: tools_dict = {**tools_dict, **mcp_tools_dict} @@ -2737,7 +2743,7 @@ async def process_chat_payload(request, form_data, user, metadata, model): # Inject builtin tools for native function calling based on enabled features and model capability. # Only inject when the request originates from the UI (identified by session_id). # API callers don't expect hidden tools; they can explicitly request tools via tool_ids. - if use_builtin_tools: + if ENABLE_PLUGINS and use_builtin_tools: # Add file context to user messages chat_id = metadata.get('chat_id') form_data['messages'] = await add_file_context(form_data.get('messages', []), chat_id, user) @@ -3371,16 +3377,19 @@ async def outlet_filter_handler(ctx): '__model__': model, } - filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - filter_functions = await Functions.get_functions_by_ids(filter_ids) + if ENABLE_PLUGINS: + filter_ids = await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + filter_functions = await Functions.get_functions_by_ids(filter_ids) - outlet_result, _ = await process_filter_functions( - request=request, - filter_functions=filter_functions, - filter_type='outlet', - form_data=outlet_data, - extra_params=extra_params, - ) + outlet_result, _ = await process_filter_functions( + request=request, + filter_functions=filter_functions, + filter_type='outlet', + form_data=outlet_data, + extra_params=extra_params, + ) + else: + outlet_result = outlet_data if outlet_result and outlet_result.get('messages'): if not is_temp_chat and messages_map: @@ -3620,10 +3629,14 @@ async def streaming_chat_response_handler(response, ctx): '__model__': model, } - filter_functions = [ - await Functions.get_function_by_id(filter_id) - for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) - ] + filter_functions = ( + [ + await Functions.get_function_by_id(filter_id) + for filter_id in await get_sorted_filter_ids(request, model, metadata.get('filter_ids', [])) + ] + if ENABLE_PLUGINS + else [] + ) # Standard streaming response handler # event_caller is optional — only needed for direct (client-side) tools @@ -3907,7 +3920,8 @@ async def streaming_chat_response_handler(response, ctx): model_capabilities = model.get('info', {}).get('meta', {}).get('capabilities') or {} builtin_tools_meta = model.get('info', {}).get('meta', {}).get('builtinTools', {}) DETECT_CODE_INTERPRETER = ( - bool(features.get('code_interpreter')) + ENABLE_PLUGINS + and bool(features.get('code_interpreter')) and builtin_tools_meta.get('code_interpreter', True) and await Config.get('code_interpreter.enable') and model_capabilities.get('code_interpreter', True) diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 39e139b053..4012685965 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -9,7 +9,7 @@ from open_webui.config import ( BYPASS_ADMIN_ACCESS_CONTROL, DEFAULT_ARENA_MODEL, ) -from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, GLOBAL_LOG_LEVEL +from open_webui.env import BYPASS_MODEL_ACCESS_CONTROL, ENABLE_PLUGINS, GLOBAL_LOG_LEVEL from open_webui.functions import get_function_models from open_webui.models.access_grants import AccessGrants from open_webui.models.config import Config @@ -125,11 +125,23 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) ] models = models + arena_models - global_action_ids = {function.id for function in await Functions.get_global_action_functions()} - enabled_action_ids = {function.id for function in await Functions.get_functions_by_type('action', active_only=True)} + global_action_ids = ( + {function.id for function in await Functions.get_global_action_functions()} if ENABLE_PLUGINS else set() + ) + enabled_action_ids = ( + {function.id for function in await Functions.get_functions_by_type('action', active_only=True)} + if ENABLE_PLUGINS + else set() + ) - global_filter_ids = {function.id for function in await Functions.get_global_filter_functions()} - enabled_filter_ids = {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + global_filter_ids = ( + {function.id for function in await Functions.get_global_filter_functions()} if ENABLE_PLUGINS else set() + ) + enabled_filter_ids = ( + {function.id for function in await Functions.get_functions_by_type('filter', active_only=True)} + if ENABLE_PLUGINS + else set() + ) custom_models = await Models.get_all_models() @@ -157,8 +169,9 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) if 'info' in model: if 'meta' in model['info']: - action_ids.extend(model['info']['meta'].get('actionIds', [])) - filter_ids.extend(model['info']['meta'].get('filterIds', [])) + if ENABLE_PLUGINS: + action_ids.extend(model['info']['meta'].get('actionIds', [])) + filter_ids.extend(model['info']['meta'].get('filterIds', [])) if 'params' in model['info']: del model['info']['params'] @@ -211,10 +224,10 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) if custom_model.meta: meta = custom_model.meta.model_dump() - if 'actionIds' in meta: + if ENABLE_PLUGINS and 'actionIds' in meta: action_ids.extend(meta['actionIds']) - if 'filterIds' in meta: + if ENABLE_PLUGINS and 'filterIds' in meta: filter_ids.extend(meta['filterIds']) model['action_ids'] = action_ids diff --git a/backend/open_webui/utils/plugin.py b/backend/open_webui/utils/plugin.py index 53531c4350..4e3c6e5423 100644 --- a/backend/open_webui/utils/plugin.py +++ b/backend/open_webui/utils/plugin.py @@ -13,6 +13,7 @@ from typing import Any from open_webui.env import ( ENABLE_PIP_INSTALL_FRONTMATTER_REQUIREMENTS, + ENABLE_PLUGINS, OFFLINE_MODE, PIP_OPTIONS, PIP_PACKAGE_INDEX_OPTIONS, @@ -203,6 +204,9 @@ def replace_imports(content): # May the intent of the one who wrote it survive every # import and transformation, as a deed survives the generations. async def load_tool_module_by_id(tool_id, content=None): + if not ENABLE_PLUGINS: + raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + if content is None: tool = await Tools.get_tool_by_id(tool_id) if not tool: @@ -251,6 +255,9 @@ async def load_tool_module_by_id(tool_id, content=None): async def load_function_module_by_id(function_id: str, content: str | None = None): + if not ENABLE_PLUGINS: + raise RuntimeError('Plugins are disabled by ENABLE_PLUGINS=false') + if content is None: function = await Functions.get_function_by_id(function_id) if not function: @@ -447,6 +454,10 @@ async def install_tool_and_function_dependencies(): and then installing them using pip. Duplicates or similar version specifications are handled by pip as much as possible. """ + if not ENABLE_PLUGINS: + log.info('ENABLE_PLUGINS is disabled, skipping tool and function dependencies.') + return + function_list = await Functions.get_functions(active_only=True) tool_list = await Tools.get_tools() diff --git a/backend/open_webui/utils/tools.py b/backend/open_webui/utils/tools.py index 6c33882c50..6fc0f4172f 100644 --- a/backend/open_webui/utils/tools.py +++ b/backend/open_webui/utils/tools.py @@ -35,6 +35,7 @@ from open_webui.env import ( AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER, AIOHTTP_CLIENT_TIMEOUT_TOOL_SERVER_DATA, ENABLE_FORWARD_USER_INFO_HEADERS, + ENABLE_PLUGINS, FORWARD_SESSION_INFO_HEADER_CHAT_ID, FORWARD_SESSION_INFO_HEADER_MESSAGE_ID, REDIS_KEY_PREFIX, @@ -253,6 +254,9 @@ async def get_updated_tool_function(function: Callable, extra_params: dict): async def get_tools(request: Request, tool_ids: list[str], user: UserModel, extra_params: dict) -> dict[str, dict]: """Load tools for the given tool_ids, checking access control.""" + if not ENABLE_PLUGINS: + return {} + if not tool_ids: return {} @@ -469,6 +473,9 @@ async def get_builtin_tools( Get built-in tools for native function calling. Only returns tools when BOTH the global config is enabled AND the model capability allows it. """ + if not ENABLE_PLUGINS: + return {} + tools_dict = {} builtin_functions = [] features = features or {}