fix: prevent sentinel from leaking into /v1/models API response (#23850)

The fail-closed sentinel "__access_group_no_models__" was passing
through get_complete_model_list() unchanged and reaching callers that
return model lists directly to end-users, causing it to appear as a
model ID in GET /v1/models responses.

- Extract sentinel to named constant ACCESS_GROUP_NO_MODELS_SENTINEL
  in model_checks.py so all references stay in sync
- Strip the sentinel from all_models before returning in utils.py
- Strip it from all_models_str in proxy_server.py as a safety net

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jean Carlos Nunez 2026-04-05 14:50:14 -05:00
parent 2d793a3acd
commit 82d01db3f4
3 changed files with 16 additions and 1 deletions

View file

@ -132,6 +132,13 @@ def get_key_models(
return all_models
# Sentinel used by get_key_models_with_db_access_groups to signal "key has
# access_group_ids but none resolved to any model" so that get_complete_model_list()
# does not fall through to team_models (fail-closed). Callers that return model
# lists to end-users MUST strip this value before responding.
ACCESS_GROUP_NO_MODELS_SENTINEL = "__access_group_no_models__"
async def get_key_models_with_db_access_groups(
user_api_key_dict: UserAPIKeyAuth,
proxy_model_list: List[str],
@ -168,7 +175,7 @@ async def get_key_models_with_db_access_groups(
# Fail-closed: if the access group is configured but resolves to nothing
# (deleted group, DB unavailable, or empty group), use a sentinel so
# get_complete_model_list() does NOT fall through to team_models.
key_models = db_models if db_models else ["__access_group_no_models__"]
key_models = db_models if db_models else [ACCESS_GROUP_NO_MODELS_SENTINEL]
return key_models

View file

@ -275,6 +275,7 @@ from litellm.proxy.auth.auth_utils import check_response_size_is_safe
from litellm.proxy.auth.handle_jwt import JWTHandler
from litellm.proxy.auth.litellm_license import LicenseCheck
from litellm.proxy.auth.model_checks import (
ACCESS_GROUP_NO_MODELS_SENTINEL,
get_all_fallbacks,
get_complete_model_list,
get_key_models,
@ -10871,6 +10872,8 @@ async def model_info_v1( # noqa: PLR0915
infer_model_from_keys=general_settings.get("infer_model_from_keys", False),
llm_router=llm_router,
)
# Strip the fail-closed sentinel — it must never appear in the /v1/models response.
all_models_str = [m for m in all_models_str if m != ACCESS_GROUP_NO_MODELS_SENTINEL]
if len(all_models_str) > 0:
_relevant_models = []

View file

@ -5516,6 +5516,7 @@ async def get_available_models_for_user(
"""
from litellm.proxy.auth.auth_checks import get_team_object
from litellm.proxy.auth.model_checks import (
ACCESS_GROUP_NO_MODELS_SENTINEL,
get_complete_model_list,
get_key_models_with_db_access_groups,
get_team_models,
@ -5581,6 +5582,10 @@ async def get_available_models_for_user(
only_model_access_groups=only_model_access_groups,
)
# Strip the fail-closed sentinel before returning to callers — it must never
# appear in a model listing response (e.g. GET /v1/models).
all_models = [m for m in all_models if m != ACCESS_GROUP_NO_MODELS_SENTINEL]
return all_models