diff --git a/litellm/proxy/auth/handle_jwt.py b/litellm/proxy/auth/handle_jwt.py index a39854fa309..df3463b9b3e 100644 --- a/litellm/proxy/auth/handle_jwt.py +++ b/litellm/proxy/auth/handle_jwt.py @@ -2038,8 +2038,9 @@ class JWTAuthManager: ), ) except Exception: - verbose_proxy_logger.debug( - "JWT DB team fallback: membership lookup failed for team_id=%s", + verbose_proxy_logger.warning( + "JWT DB team fallback: membership lookup failed for team_id=%s; " + "proceeding without per-team membership budget enforcement", candidate_team_id, exc_info=True, ) diff --git a/tests/test_litellm/proxy/auth/test_handle_jwt.py b/tests/test_litellm/proxy/auth/test_handle_jwt.py index d9e02329fd4..79ce29e534d 100644 --- a/tests/test_litellm/proxy/auth/test_handle_jwt.py +++ b/tests/test_litellm/proxy/auth/test_handle_jwt.py @@ -4827,6 +4827,56 @@ async def test_resolve_db_team_fallback_loads_team_membership(): assert team_membership.budget_id == "budget_xyz" +@pytest.mark.asyncio +async def test_resolve_db_team_fallback_survives_membership_lookup_error(): + """A transient membership-lookup failure must not deny an otherwise-authorized + request: the resolved team is still returned with a None membership (budget + enforcement degrades gracefully) instead of the error propagating as a 403.""" + user_object = LiteLLM_UserTable( + user_id="u_flaky", + user_role=LitellmUserRoles.INTERNAL_USER, + teams=["team_flaky"], + ) + + async def fake_get_team(team_id, **kwargs): + return LiteLLM_TeamTable(team_id=team_id) + + async def boom_get_membership(user_id, team_id, **kwargs): + raise Exception("transient DB error") + + with ( + patch( + "litellm.proxy.auth.handle_jwt.get_team_object", + new_callable=AsyncMock, + side_effect=fake_get_team, + ), + patch( + "litellm.proxy.auth.handle_jwt.get_team_membership", + new_callable=AsyncMock, + side_effect=boom_get_membership, + ), + ): + ( + team_id, + team_object, + team_membership, + ) = await JWTAuthManager._resolve_db_team_fallback( + user_object=user_object, + user_id="u_flaky", + requested_model=None, + enforce_team_based_model_access=True, + team_id_upsert=False, + prisma_client=None, + user_api_key_cache=MagicMock(), + parent_otel_span=None, + proxy_logging_obj=MagicMock(), + ) + + assert team_id == "team_flaky" + assert team_object is not None + assert team_membership is None + + @pytest.mark.asyncio async def test_auth_builder_db_fallback_does_not_validate_rbac_team_against_db_membership(): """When fallback_to_db_teams is on and the JWT carries an RBAC team role but no