2525from open_webui .models .models import Models
2626from open_webui .config import (
2727 CACHE_DIR ,
28+ ALCF_LIST_ENDPOINTS_URL ,
2829)
2930from open_webui .env import (
3031 MODELS_CACHE_TTL ,
@@ -132,6 +133,74 @@ def openai_reasoning_model_handler(payload):
132133 return payload
133134
134135
136+ # [ADDITION BEGINS]
137+ def _parse_list_endpoints_models (data : dict , cluster : str ) -> set :
138+ """
139+ Parse ALCF list-endpoints API response and return set of model IDs for the given cluster.
140+ Response shape: {"clusters": {"sophia": {"frameworks": {"vllm": {"models": [...]}}, ...}}}
141+ """
142+ cluster_key = cluster .lower ()
143+ clusters = data .get ("clusters" ) or {}
144+ cluster_info = clusters .get (cluster_key , {})
145+ frameworks = cluster_info .get ("frameworks" ) or {}
146+ models_set = set ()
147+ for framework_data in frameworks .values ():
148+ if isinstance (framework_data , dict ) and "models" in framework_data :
149+ models_set .update (m for m in framework_data ["models" ] if m )
150+ return models_set
151+
152+
153+ async def get_allowed_model_ids (
154+ connection_model_ids : list ,
155+ list_endpoints_url : str ,
156+ cluster : str ,
157+ key : str = None ,
158+ ) -> list :
159+ """
160+ Return model IDs that are in both the WebUI connection config and the ALCF list-endpoints
161+ response for the given cluster. Queries list_endpoints_url with optional bearer token.
162+
163+ Args:
164+ connection_model_ids: Model IDs configured in the WebUI connection.
165+ list_endpoints_url: ALCF list-endpoints URL (e.g. ALCF_LIST_ENDPOINTS_URL).
166+ cluster: Cluster name (e.g. "sophia", "metis") to read from the response.
167+ key: Optional bearer token for the request.
168+
169+ Returns:
170+ List of model IDs present in both connection config and list-endpoints (order preserved
171+ from connection_model_ids). Empty list on fetch/parse error.
172+ """
173+ if not connection_model_ids or not list_endpoints_url :
174+ return list (connection_model_ids ) if connection_model_ids else []
175+
176+ timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
177+ try :
178+ async with aiohttp .ClientSession (
179+ trust_env = True , timeout = timeout
180+ ) as session :
181+ async with session .get (
182+ list_endpoints_url ,
183+ headers = (
184+ {"Authorization" : f"Bearer { key } " } if key else {}
185+ ),
186+ ssl = AIOHTTP_CLIENT_SESSION_SSL ,
187+ ) as response :
188+ if not response .ok :
189+ log .warning (
190+ f"list_endpoints request error: { response .status } { list_endpoints_url } "
191+ )
192+ return []
193+ data = json .loads (await response .text ())
194+ except Exception as e :
195+ log .warning (f"list_endpoints fetch error: { e } " )
196+ return []
197+
198+ endpoint_models = _parse_list_endpoints_models (data , cluster )
199+ # Preserve order of connection_model_ids
200+ return [m for m in connection_model_ids if m in endpoint_models ]
201+ # [ADDITION ENDS]
202+
203+ # [ADDITION BEGINS]
135204class AGPTModelStatus :
136205 def __init__ (self , ttl = 0 ):
137206 self .session = None
@@ -142,22 +211,33 @@ def __init__(self, ttl=0):
142211 self .ttl = ttl
143212 self ._task = {}
144213
214+ def _get_key_hash (self , key : str ) -> str :
215+ """Hash the API key for secure cache key generation"""
216+ if not key :
217+ return "anonymous"
218+ return hashlib .sha256 (key .encode ()).hexdigest ()
219+
145220 async def fetch (self , cluster : str , url , key = None , user : UserModel = None , timeout = None ):
146221 _cluster = cluster .lower ()
222+
223+ # Create user-specific cache key using hashed API key
224+ key_hash = self ._get_key_hash (key )
225+ cache_key = f"{ key_hash } _{ _cluster } "
226+
147227 if self .session is None :
148228 self .session = aiohttp .ClientSession (
149229 trust_env = True ,
150230 timeout = aiohttp .ClientTimeout (total = AIOHTTP_CLIENT_TIMEOUT_MODEL_LIST )
151231 )
152232
153- if _cluster in self .last_update :
154- if time .time () - self .last_update [_cluster ] <= self .ttl :
155- log .debug (f"skip jobs query due to TTL" )
233+ if cache_key in self .last_update :
234+ if time .time () - self .last_update [cache_key ] <= self .ttl :
235+ log .debug (f"skip jobs query due to TTL for key { key_hash } " )
156236 return True
157237
158- if _cluster in self ._task :
159- if not self ._task [_cluster ].done ():
160- log .debug (f"query pending" )
238+ if cache_key in self ._task :
239+ if not self ._task [cache_key ].done ():
240+ log .debug (f"query pending for key { key_hash } " )
161241 return True
162242
163243 async def do_fetch (self , url , key = None ):
@@ -181,17 +261,16 @@ async def do_fetch(self, url, key=None):
181261 live_models .extend (model ["Models" ].split ("," ))
182262 elif model ["Model Status" ] == "starting" :
183263 starting_models .extend (model ["Models" ].split ("," ))
184-
185264 for model in status_ret ["queued" ]:
186265 queued_models .extend (model ["Models" ].split ("," ))
187266
188- self .live_models [_cluster ] = live_models
189- self .starting_models [_cluster ] = starting_models
190- self .queued_models [_cluster ] = queued_models
191- self .last_update [_cluster ] = time .time ()
192- log .debug (f"model_status_tracker:update { _cluster } live_models() { self .live_models [_cluster ]} " )
193- log .debug (f"model_status_tracker:update { _cluster } starting_models() { self .starting_models [_cluster ]} " )
194- log .debug (f"model_status_tracker:update { _cluster } queue_models() { self .queued_models [_cluster ]} " )
267+ self .live_models [cache_key ] = live_models
268+ self .starting_models [cache_key ] = starting_models
269+ self .queued_models [cache_key ] = queued_models
270+ self .last_update [cache_key ] = time .time ()
271+ log .debug (f"model_status_tracker:update { cache_key } live_models() { self .live_models [cache_key ]} " )
272+ log .debug (f"model_status_tracker:update { cache_key } starting_models() { self .starting_models [cache_key ]} " )
273+ log .debug (f"model_status_tracker:update { cache_key } queue_models() { self .queued_models [cache_key ]} " )
195274 return True
196275 else :
197276 log .warning (f"agpt_fetch_model_status request error: { response .status } { url } key={ key } user={ user } " )
@@ -200,25 +279,32 @@ async def do_fetch(self, url, key=None):
200279 log .warning (f"connection error: { e } " )
201280 return False
202281
203- self ._task [_cluster ] = asyncio .create_task (do_fetch (self , url , key ))
282+ self ._task [cache_key ] = asyncio .create_task (do_fetch (self , url , key ))
204283
205284 try :
206- return await asyncio .wait_for (self ._task [_cluster ], timeout = timeout )
285+ return await asyncio .wait_for (self ._task [cache_key ], timeout = timeout )
207286 except asyncio .TimeoutError :
208287 log .debug (f"fetch returning, but still fetching { url } " )
209288 return True
210289
211- def is_live (self , cluster : str , model_id ):
290+ def is_live (self , cluster : str , model_id , key = None ):
212291 _cluster = cluster .lower ()
213- return model_id in self .live_models [_cluster ] if _cluster in self .live_models else False
292+ key_hash = self ._get_key_hash (key )
293+ cache_key = f"{ key_hash } _{ _cluster } "
294+ return model_id in self .live_models [cache_key ] if cache_key in self .live_models else False
214295
215- def is_starting (self , cluster : str , model_id ):
296+ def is_starting (self , cluster : str , model_id , key = None ):
216297 _cluster = cluster .lower ()
217- return model_id in self .starting_models [_cluster ] if _cluster in self .starting_models else False
298+ key_hash = self ._get_key_hash (key )
299+ cache_key = f"{ key_hash } _{ _cluster } "
300+ return model_id in self .starting_models [cache_key ] if cache_key in self .starting_models else False
218301
219- def is_queued (self , cluster : str , model_id ):
302+ def is_queued (self , cluster : str , model_id , key = None ):
220303 _cluster = cluster .lower ()
221- return model_id in self .queued_models [_cluster ] if _cluster in self .queued_models else False
304+ key_hash = self ._get_key_hash (key )
305+ cache_key = f"{ key_hash } _{ _cluster } "
306+ return model_id in self .queued_models [cache_key ] if cache_key in self .queued_models else False
307+ # [ADDITION ENDS]
222308
223309async def get_headers_and_cookies (
224310 request : Request ,
@@ -499,6 +585,17 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
499585 model_ids = api_config .get ("model_ids" , [])
500586 is_aurora = api_config .get ("aurora" , False )
501587
588+ # [ADDITION BEGINS]
589+ # Filter models IDs based on the user's API key
590+ # This will remove models that the user is not allowed to see
591+ model_ids = await get_allowed_model_ids (
592+ model_ids ,
593+ ALCF_LIST_ENDPOINTS_URL .value ,
594+ api_config .get ("cluster_name" , "" ),
595+ user .api_key if user else None
596+ )
597+ # [ADDITION ENDS]
598+
502599 api_key = request .app .state .config .OPENAI_API_KEYS [idx ]
503600
504601 if enable :
@@ -591,15 +688,17 @@ async def get_all_models_responses(request: Request, user: UserModel) -> list:
591688 if "name" in model and model ["name" ] is None :
592689 del model ["name" ]
593690
691+ # [ADDITION BEGINS] - Now passing the key argument to the model_status_tracker methods
594692 if "cluster_name" in model :
595- if model_status_tracker .is_live (model ["cluster_name" ], model ["id" ]):
693+ if model_status_tracker .is_live (model ["cluster_name" ], model ["id" ], key = user . api_key if user else None ):
596694 model ["status" ] = "live"
597- elif model_status_tracker .is_starting (model ["cluster_name" ], model ["id" ]):
695+ elif model_status_tracker .is_starting (model ["cluster_name" ], model ["id" ], key = user . api_key if user else None ):
598696 model ["status" ] = "starting"
599- elif model_status_tracker .is_queued (model ["cluster_name" ], model ["id" ]):
697+ elif model_status_tracker .is_queued (model ["cluster_name" ], model ["id" ], key = user . api_key if user else None ):
600698 model ["status" ] = "queued"
601699 else :
602700 model ["status" ] = "offline"
701+ # [ADDITION ENDS]
603702
604703 if prefix_id :
605704 model ["id" ] = (
0 commit comments