From 11bd45f02e9d04eac720ce25302cd410a9904b24 Mon Sep 17 00:00:00 2001 From: Praising Harris Ratnam Date: Sun, 18 Oct 2026 22:58:16 +0530 Subject: [PATCH] fix(auth_checks): preserve union semantics for key models with access groups --- litellm/proxy/auth/auth_checks.py | 26 +++++++++--------- tests/unit/proxy/auth/test_auth_checks.py | 33 +++++++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/litellm/proxy/auth/auth_checks.py b/litellm/proxy/auth/auth_checks.py index c231a53a6a8..d182ea9ba53 100644 --- a/litellm/proxy/auth/auth_checks.py +++ b/litellm/proxy/auth/auth_checks.py @@ -5423,20 +5423,20 @@ async def can_key_call_model( code=status.HTTP_403_FORBIDDEN, ) if key_models: - allowed_models = [m for m in key_models if m in models_from_groups] - if not allowed_models: - raise ModelAccessDeniedProxyException( - message=model_access_denied_client_message(model=model), - internal_message=( - f"key not allowed to access model. Key models={key_models} have no intersection " - f"with access_group_ids models={models_from_groups}. Tried to access {model}" - ), - type=ProxyErrorTypes.get_model_access_error_type_for_object(object_type="key"), - param="model", - code=status.HTTP_403_FORBIDDEN, - ) - else: + allowed_models = list(dict.fromkeys([*key_models, *models_from_groups])) + elif models_from_groups: allowed_models = models_from_groups + else: + raise ModelAccessDeniedProxyException( + message=model_access_denied_client_message(model=model), + internal_message=( + f"key not allowed to access model. Key has access_group_ids={key_access_group_ids} " + f"which resolved to no models. Tried to access {model}" + ), + type=ProxyErrorTypes.get_model_access_error_type_for_object(object_type="key"), + param="model", + code=status.HTTP_403_FORBIDDEN, + ) return can_object_call_model( model=model, diff --git a/tests/unit/proxy/auth/test_auth_checks.py b/tests/unit/proxy/auth/test_auth_checks.py index fb56f6af573..14ad7596fb4 100644 --- a/tests/unit/proxy/auth/test_auth_checks.py +++ b/tests/unit/proxy/auth/test_auth_checks.py @@ -1641,12 +1641,11 @@ async def test_can_key_call_model_empty_models_empty_access_groups_denied(): @pytest.mark.asyncio -async def test_can_key_call_model_disjoint_models_and_access_groups_denied(): - """Test can_key_call_model denies when key models and access_group_ids models are disjoint.""" +async def test_can_key_call_model_union_preserves_both_key_and_group_models(): + """Test can_key_call_model preserves union semantics when key models and access_group_ids are both set.""" from unittest.mock import AsyncMock, patch from litellm.proxy.auth.auth_checks import can_key_call_model - from litellm.proxy._types import ProxyException user_api_key_object = UserAPIKeyAuth( token="test-token", @@ -1658,7 +1657,11 @@ async def test_can_key_call_model_disjoint_models_and_access_groups_denied(): { "model_name": "claude-3", "litellm_params": {"model": "anthropic/claude-3", "api_key": "test"}, - } + }, + { + "model_name": "gpt-3.5-turbo", + "litellm_params": {"model": "openai/gpt-3.5-turbo", "api_key": "test"}, + }, ] ) @@ -1667,13 +1670,21 @@ async def test_can_key_call_model_disjoint_models_and_access_groups_denied(): new_callable=AsyncMock, return_value=["claude-3"], ): - with pytest.raises(ProxyException): - await can_key_call_model( - model="claude-3", - llm_model_list=[], - valid_token=user_api_key_object, - llm_router=router, - ) + # Access gpt-3.5-turbo (from key_models) + assert await can_key_call_model( + model="gpt-3.5-turbo", + llm_model_list=[], + valid_token=user_api_key_object, + llm_router=router, + ) is True + + # Access claude-3 (from access_group_ids) + assert await can_key_call_model( + model="claude-3", + llm_model_list=[], + valid_token=user_api_key_object, + llm_router=router, + ) is True @pytest.mark.asyncio