mirror of
https://github.com/open-webui/open-webui.git
synced 2026-11-08 08:27:19 +00:00
fix: apply custom model fallback before the base-model access check
With ENABLE_CUSTOM_MODEL_FALLBACK enabled, chatting with a custom workspace model whose base model was removed still failed with "Model not found" for non-admin users. The access check ran before the fallback resolution and walked the preset's base_model_id chain; has_base_model_access treats a base model without a model-table row as admin-only, so regular users were rejected before the fallback to the default (Selected) model could ever apply. Admins bypass the access check by default, which is why the fallback only appeared to work for them. Resolve the fallback model first, and when one applies, skip the missing base-model hop on the requested preset and enforce access on the fallback model that will actually serve the chat instead. check_model_access gains a check_base_model flag for this; all other callers keep the default chain enforcement. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Sc922g12Yp16TdcCt74AWi
This commit is contained in:
parent
29541cbb52
commit
ae8ef975ea
2 changed files with 35 additions and 18 deletions
|
|
@ -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')
|
||||
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue