diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index fb558cfe63..a5690e6d13 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1081,6 +1081,7 @@ async def chat_completion( metadata = {} try: model_info = None + fallback_model = None if not model_item.get('direct', False): if model_id not in request.app.state.MODELS: raise Exception('Model not found') @@ -1088,10 +1089,32 @@ async def chat_completion( model = request.app.state.MODELS[model_id] model_info = await Models.get_model_by_id(model_id) + # Resolve the custom-model fallback before the access check: the + # base-model access hop treats a missing base model as admin-only + # and would reject non-admin users before the fallback could apply. + if ( + ENABLE_CUSTOM_MODEL_FALLBACK + and model_info + and model_info.base_model_id + and model_info.base_model_id not in request.app.state.MODELS + ): + default_models = ((await Config.get('ui.default_models')) or '').split(',') + fallback_model_id = default_models[0].strip() if default_models[0] else None + + if fallback_model_id and fallback_model_id in request.app.state.MODELS: + fallback_model = request.app.state.MODELS[fallback_model_id] + # Check if user has access to the model if not BYPASS_MODEL_ACCESS_CONTROL and (user.role != 'admin' or not BYPASS_ADMIN_ACCESS_CONTROL): try: - await check_model_access(user, model, model_info=model_info) + if fallback_model is not None: + # The requested custom model still gates entry, but its + # missing base model is skipped — access is enforced on + # the fallback model that will actually serve the chat. + await check_model_access(user, model, model_info=model_info, check_base_model=False) + await check_model_access(user, fallback_model) + else: + await check_model_access(user, model, model_info=model_info) except Exception as e: raise e else: @@ -1113,19 +1136,11 @@ async def chat_completion( # Check base model existence for custom models if model_info and model_info.base_model_id: - base_model_id = model_info.base_model_id - if base_model_id not in request.app.state.MODELS: - if ENABLE_CUSTOM_MODEL_FALLBACK: - default_models = ((await Config.get('ui.default_models')) or '').split(',') - - fallback_model_id = default_models[0].strip() if default_models[0] else None - - if fallback_model_id and fallback_model_id in request.app.state.MODELS: - # Update model and form_data so routing uses the fallback model's type - model = request.app.state.MODELS[fallback_model_id] - form_data['model'] = fallback_model_id - else: - raise Exception('Model not found') + if model_info.base_model_id not in request.app.state.MODELS: + if fallback_model is not None: + # Update model and form_data so routing uses the fallback model's type + model = fallback_model + form_data['model'] = fallback_model['id'] else: raise Exception('Model not found') diff --git a/backend/open_webui/utils/models.py b/backend/open_webui/utils/models.py index 4cdefd0de5..2a666f1ebc 100644 --- a/backend/open_webui/utils/models.py +++ b/backend/open_webui/utils/models.py @@ -428,7 +428,7 @@ async def get_all_models(request, refresh: bool = False, user: UserModel = None) return models -async def check_model_access(user, model, model_info=None, db=None): +async def check_model_access(user, model, model_info=None, db=None, check_base_model=True): if model.get('arena'): meta = model.get('info', {}).get('meta', {}) access_grants = meta.get('access_grants', []) @@ -449,7 +449,7 @@ async def check_model_access(user, model, model_info=None, db=None): # One group-membership fetch shared by the direct check and every # base-model hop; skipped when no check below needs it. user_group_ids = None - if user.id != model_info.user_id or model_info.base_model_id: + if user.id != model_info.user_id or (check_base_model and model_info.base_model_id): user_group_ids = {group.id for group in await Groups.get_groups_by_member_id(user.id, db=db)} if not ( @@ -465,8 +465,10 @@ async def check_model_access(user, model, model_info=None, db=None): ): raise Exception('Model not found') - # Enforce access on chained base models - if not await has_base_model_access( + # Enforce access on chained base models. Callers substituting the base + # model (custom-model fallback) skip this hop and must enforce access + # on the substitute model instead. + if check_base_model and not await has_base_model_access( user.id, model_info, user_role=user.role, user_group_ids=user_group_ids, db=db ): raise Exception('Model not found')