diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 1b9ed9cc53..b0107cc178 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -822,12 +822,18 @@ if audit_level != AuditLevel.NONE: async def get_models(request: Request, refresh: bool = False, user=Depends(get_verified_user)): all_models = await get_all_models(request, refresh=refresh, user=user) - models = [] - for model in all_models: - # Filter out filter pipelines - if 'pipeline' in model and model['pipeline'].get('type', None) == 'filter': - continue + # Filter out filter pipelines + models = [model for model in all_models if not ('pipeline' in model and model['pipeline'].get('type', None) == 'filter')] + # Chat requests resolve models by ID from request.app.state.MODELS, where + # duplicate IDs collapse to the last model. Return the same effective list. + models = list({model['id']: model for model in models}.values()) + + # Access-filter first so the per-model payload work below only runs for + # models the caller can actually see. + models = await get_filtered_models(models, user) + + for model in models: # Remove profile image URL to reduce payload size if model.get('info', {}).get('meta', {}).get('profile_image_url'): model['info']['meta'].pop('profile_image_url', None) @@ -841,13 +847,6 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v except Exception as e: log.debug(f'Error processing model tags: {e}') model['tags'] = [] - pass - - models.append(model) - - # Chat requests resolve models by ID from request.app.state.MODELS, where - # duplicate IDs collapse to the last model. Return the same effective list. - models = list({model['id']: model for model in models}.values()) model_order_list = await Config.get('ui.model_order_list') if model_order_list: @@ -860,11 +859,10 @@ async def get_models(request: Request, refresh: bool = False, user=Depends(get_v ) ) - models = await get_filtered_models(models, user) - - log.debug( - f'/api/models returned filtered models accessible to the user: {json.dumps([model.get("id") for model in models])}' - ) + if log.isEnabledFor(logging.DEBUG): + log.debug( + f'/api/models returned filtered models accessible to the user: {json.dumps([model.get("id") for model in models])}' + ) return {'data': models} diff --git a/backend/open_webui/models/functions.py b/backend/open_webui/models/functions.py index 2c011a13c7..c9594705b9 100644 --- a/backend/open_webui/models/functions.py +++ b/backend/open_webui/models/functions.py @@ -275,11 +275,17 @@ class FunctionsTable: result = await db.execute(select(Function).filter_by(type='filter', is_active=True, is_global=True)) return [FunctionModel.model_validate(function) for function in result.scalars().all()] + async def get_active_function_ids_by_type( + self, type: str, db: AsyncSession | None = None + ) -> list[tuple[str, bool]]: + """Return (id, is_global) for active functions without fetching plugin source.""" + async with get_async_db_context(db) as db: + result = await db.execute(select(Function.id, Function.is_global).filter_by(type=type, is_active=True)) + return [(id, bool(is_global)) for id, is_global in result.all()] + async def get_active_filter_ids(self, db: AsyncSession | None = None) -> list[tuple[str, bool]]: """Return (id, is_global) for active filters without fetching plugin source.""" - async with get_async_db_context(db) as db: - result = await db.execute(select(Function.id, Function.is_global).filter_by(type='filter', is_active=True)) - return [(id, bool(is_global)) for id, is_global in result.all()] + return await self.get_active_function_ids_by_type('filter', db=db) async def get_global_action_functions(self, db: AsyncSession | None = None) -> list[FunctionModel]: async with get_async_db_context(db) as db: diff --git a/backend/open_webui/socket/utils.py b/backend/open_webui/socket/utils.py index b337b08f40..010d1f34f6 100644 --- a/backend/open_webui/socket/utils.py +++ b/backend/open_webui/socket/utils.py @@ -102,12 +102,18 @@ class RedisDict: # Serialize values once — reused for both the fingerprint and the write. serialized = {k: json.dumps(v) for k, v in mapping.items()} + digest = hashlib.sha256() + for key in sorted(serialized): + digest.update(key.encode()) + digest.update(b'\0') + digest.update(serialized[key].encode()) + digest.update(b'\0') + signature = digest.hexdigest() # Skip the write when the prepared mapping is identical to the last one # this process wrote. The check is per-instance (not distributed), but # still eliminates the majority of redundant writes because each pod # typically produces the same model list on consecutive refreshes. - signature = hashlib.sha256(json.dumps(serialized, sort_keys=True).encode()).hexdigest() if signature == self._last_signature: return diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 9421975dcf..6b5de1daae 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -129,13 +129,13 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) # One query per type: the global sets are subsets of the active sets, so # deriving them from the same rows halves the function-table queries. if ENABLE_PLUGINS: - active_actions = await Functions.get_functions_by_type('action', active_only=True) - global_action_ids = {function.id for function in active_actions if function.is_global} - enabled_action_ids = {function.id for function in active_actions} + active_actions = await Functions.get_active_function_ids_by_type('action') + global_action_ids = {function_id for function_id, is_global in active_actions if is_global} + enabled_action_ids = {function_id for function_id, _ in active_actions} - active_filters = await Functions.get_functions_by_type('filter', active_only=True) - global_filter_ids = {function.id for function in active_filters if function.is_global} - enabled_filter_ids = {function.id for function in active_filters} + active_filters = await Functions.get_active_function_ids_by_type('filter') + global_filter_ids = {function_id for function_id, is_global in active_filters if is_global} + enabled_filter_ids = {function_id for function_id, _ in active_filters} else: global_action_ids = set() enabled_action_ids = set() @@ -328,16 +328,27 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) all_function_valves = await Functions.get_function_valves_by_ids(list(all_function_ids)) functions_cache = get_functions_cache(request) + # Global actions and filters appear in every model, so priorities and item + # lists are memoized across the loop instead of rebuilt per model. + action_priorities = {} + def get_action_priority(action_id): + if action_id in action_priorities: + return action_priorities[action_id] + priority = 0 try: function_module = functions_cache.get(action_id) if function_module and hasattr(function_module, 'Valves'): valves_db = all_function_valves.get(action_id) valves = function_module.Valves(**(valves_db if valves_db else {})) - return getattr(valves, 'priority', 0) + priority = getattr(valves, 'priority', 0) except Exception: - pass - return 0 + priority = 0 + action_priorities[action_id] = priority + return priority + + action_items_by_id = {} + filter_items_by_id = {} for model in models: action_ids = [ @@ -355,30 +366,45 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) model['actions'] = [] for action_id in action_ids: - action_function = functions_by_id.get(action_id) - if action_function is None: - log.info(f'Action not found: {action_id}') - continue + items = action_items_by_id.get(action_id) + if items is None: + action_function = functions_by_id.get(action_id) + if action_function is None: + log.info(f'Action not found: {action_id}') + action_items_by_id[action_id] = [] + continue - function_module = functions_cache.get(action_id) - if function_module is None: - log.info(f'Failed to load action module: {action_id}') - continue - model['actions'].extend(get_action_items_from_module(action_function, function_module)) + function_module = functions_cache.get(action_id) + if function_module is None: + log.info(f'Failed to load action module: {action_id}') + action_items_by_id[action_id] = [] + continue + items = get_action_items_from_module(action_function, function_module) + action_items_by_id[action_id] = items + # Shallow copies keep per-model item dicts independent, as before + model['actions'].extend({**item} for item in items) model['filters'] = [] for filter_id in filter_ids: - filter_function = functions_by_id.get(filter_id) - if filter_function is None: - log.info(f'Filter not found: {filter_id}') - continue + items = filter_items_by_id.get(filter_id) + if items is None: + filter_function = functions_by_id.get(filter_id) + if filter_function is None: + log.info(f'Filter not found: {filter_id}') + filter_items_by_id[filter_id] = [] + continue - function_module = functions_cache.get(filter_id) - if function_module is None: - log.info(f'Failed to load filter module: {filter_id}') - continue - if getattr(function_module, 'toggle', None): - model['filters'].extend(get_filter_items_from_module(filter_function, function_module)) + function_module = functions_cache.get(filter_id) + if function_module is None: + log.info(f'Failed to load filter module: {filter_id}') + filter_items_by_id[filter_id] = [] + continue + if getattr(function_module, 'toggle', None): + items = get_filter_items_from_module(filter_function, function_module) + else: + items = [] + filter_items_by_id[filter_id] = items + model['filters'].extend({**item} for item in items) log.debug(f'get_all_models() returned {len(models)} models')