From cd5a38a6942350ebf15ca7180f2eddf30c91f566 Mon Sep 17 00:00:00 2001 From: Classic298 <27028174+Classic298@users.noreply.github.com> Date: Mon, 5 Jan 2026 20:37:41 +0100 Subject: [PATCH] feat: Models Atomic PR of #20243 (#20369) * feat: Add read-only access support for Models - Backend: Add write_access field to ModelAccessResponse - Backend: Update /models/list to return ModelAccessListResponse - Frontend: Display Read Only badge in Models list - Frontend: Disable inputs and save button when no write access - Frontend: Hide action buttons for read-only models * fix: Handle ModelAccessListResponse format in getModels API - Backend returns {items, total} instead of {data} - Update getModels API to handle both formats for backward compatibility * fix: Show read-only shared models in workspace list - Backend: Change search_models permission from 'write' to 'read' to include shared models - Backend: Keep user_id filter to only show owned/shared models (not all public) - Frontend: Handle ModelAccessListResponse format in getModels API * fix: Align Read Only badge inline with model name * fix: Correct badge placement and fix syntax error * fix: Resolve badge truncation in Models list - Add w-full to flex container for proper spacing - Wrap Badge in div to prevent truncation - Match Knowledge.svelte badge pattern * fix: Align Read Only badge with Knowledge.svelte pattern - Match Knowledge.svelte structure for badge placement - Actions only show when write_access or admin - Remove w-full from container to prevent right-overflow * fix: Return write_access from getModelById endpoint fix: Return write_access from getModelById endpoint - Use ModelAccessResponse instead of raw dict - Remove inefficient getModels call in edit page * revert * fix * fix * fix --- backend/open_webui/models/models.py | 11 +- backend/open_webui/routers/models.py | 31 +++- src/lib/components/workspace/Models.svelte | 170 +++++++++++---------- 3 files changed, 123 insertions(+), 89 deletions(-) diff --git a/backend/open_webui/models/models.py b/backend/open_webui/models/models.py index adb82755d..d8ac589d7 100755 --- a/backend/open_webui/models/models.py +++ b/backend/open_webui/models/models.py @@ -130,6 +130,10 @@ class ModelUserResponse(ModelModel): user: Optional[UserResponse] = None +class ModelAccessResponse(ModelUserResponse): + write_access: Optional[bool] = False + + class ModelResponse(ModelModel): pass @@ -139,6 +143,11 @@ class ModelListResponse(BaseModel): total: int +class ModelAccessListResponse(BaseModel): + items: list[ModelAccessResponse] + total: int + + class ModelForm(BaseModel): id: str base_model_id: Optional[str] = None @@ -292,7 +301,7 @@ class ModelsTable: db, query, filter, - permission="write", + permission="read", ) tag = filter.get("tag") diff --git a/backend/open_webui/routers/models.py b/backend/open_webui/routers/models.py index 4eb20f595..a1f642bbc 100644 --- a/backend/open_webui/routers/models.py +++ b/backend/open_webui/routers/models.py @@ -11,6 +11,8 @@ from open_webui.models.models import ( ModelModel, ModelResponse, ModelListResponse, + ModelAccessListResponse, + ModelAccessResponse, Models, ) @@ -51,7 +53,7 @@ PAGE_ITEM_COUNT = 30 @router.get( - "/list", response_model=ModelListResponse + "/list", response_model=ModelAccessListResponse ) # do NOT use "/" as path, conflicts with main.py async def get_models( query: Optional[str] = None, @@ -88,7 +90,21 @@ async def get_models( filter["user_id"] = user.id - return Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + result = Models.search_models(user.id, filter=filter, skip=skip, limit=limit, db=db) + return ModelAccessListResponse( + items=[ + ModelAccessResponse( + **model.model_dump(), + write_access=( + (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model.user_id + or has_access(user.id, "write", model.access_control, db=db) + ), + ) + for model in result.items + ], + total=result.total, + ) ########################### @@ -290,7 +306,7 @@ class ModelIdForm(BaseModel): # Note: We're not using the typical url path param here, but instead using a query parameter to allow '/' in the id -@router.get("/model", response_model=Optional[ModelResponse]) +@router.get("/model", response_model=Optional[ModelAccessResponse]) async def get_model_by_id( id: str, user=Depends(get_verified_user), db: Session = Depends(get_session) ): @@ -301,7 +317,14 @@ async def get_model_by_id( or model.user_id == user.id or has_access(user.id, "read", model.access_control, db=db) ): - return model + return ModelAccessResponse( + **model.model_dump(), + write_access=( + (user.role == "admin" and BYPASS_ADMIN_ACCESS_CONTROL) + or user.id == model.user_id + or has_access(user.id, "write", model.access_control, db=db) + ), + ) else: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, diff --git a/src/lib/components/workspace/Models.svelte b/src/lib/components/workspace/Models.svelte index 10e8c70a8..42e0549d9 100644 --- a/src/lib/components/workspace/Models.svelte +++ b/src/lib/components/workspace/Models.svelte @@ -44,6 +44,7 @@ import ViewSelector from './common/ViewSelector.svelte'; import TagSelector from './common/TagSelector.svelte'; import Pagination from '../common/Pagination.svelte'; + import Badge from '$lib/components/common/Badge.svelte'; let shiftKey = false; @@ -457,14 +458,10 @@