From 4df2d9a7aace72ff25541bbc37fcc3be37943b8d Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Thu, 20 Aug 2026 03:15:01 +0200 Subject: [PATCH] perf: filter workspace models by access in SQL instead of loading every model (#28795) Exporting workspace models loaded every model row, built a full response object with its owner for each, and only then dropped the ones the caller may not see. On a large model table that made the export endpoint slow in proportion to models the user cannot even access. The owner-or-grant check now happens in the query itself, reusing the permission filter this file already applies to the paginated list endpoint, so only visible rows are ever hydrated. The by-user wrapper had one caller left and is gone with it. Measured with 500 workspace models of which 3 are visible to the caller: 5 queries and ~12.7 ms before, 4 queries and ~2.8 ms after. The resulting set is unchanged for owner, public, direct-user, group and multi-grant entries, and base model entries stay excluded as before. --- backend/open_webui/models/models.py | 31 +++++++++++++--------------- backend/open_webui/routers/models.py | 2 +- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index 3239225caf..708eb2b7e5 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -244,9 +244,21 @@ class ModelsTable: log.error('Skipping model %r during get_all_models due to error: %s', model.id, exc) return models - async def get_models(self, db: AsyncSession | None = None) -> list[ModelUserResponse]: + async def get_models( + self, writable_by_user_id: str | None = None, db: AsyncSession | None = None + ) -> list[ModelUserResponse]: async with get_async_db_context(db) as db: - result = await db.execute(select(Model).filter(Model.base_model_id != None)) + stmt = select(Model).filter(Model.base_model_id != None) + + if writable_by_user_id: + user_group_ids = { + group.id for group in await Groups.get_groups_by_member_id(writable_by_user_id, db=db) + } + stmt = self._has_permission( + db, stmt, {'user_id': writable_by_user_id, 'group_ids': user_group_ids}, permission='write' + ) + + result = await db.execute(stmt) all_models = result.scalars().all() user_ids = list(set(model.user_id for model in all_models)) @@ -319,21 +331,6 @@ class ModelsTable: for model in all_models ] - async def get_models_by_user_id(self, user_id: str, db: AsyncSession | None = None) -> list[ModelUserResponse]: - models = await self.get_models(db=db) - user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user_id, db=db)} - - # One grants query for all non-owned models instead of one per model - accessible_ids = await AccessGrants.get_accessible_resource_ids( - user_id=user_id, - resource_type='model', - resource_ids=[model.id for model in models if model.user_id != user_id], - permission='write', - user_group_ids=user_group_ids, - db=db, - ) - return [model for model in models if model.user_id == user_id or model.id in accessible_ids] - def _has_permission(self, db, query, filter: dict, permission: str = 'read'): return AccessGrants.has_permission_filter( db=db, diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 6b618fed39..97fa5a5308 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -353,7 +353,7 @@ async def export_models( if user.role == 'admin' and BYPASS_ADMIN_ACCESS_CONTROL: return await Models.get_models(db=db) else: - return await Models.get_models_by_user_id(user.id, db=db) + return await Models.get_models(writable_by_user_id=user.id, db=db) ############################