Skip to content

Commit 02229cc

Browse files
authored
Merge pull request #93 from argonne-lcf/enable-private-models
Enable private models
2 parents aebe808 + 26a6066 commit 02229cc

2 files changed

Lines changed: 130 additions & 25 deletions

File tree

backend/open_webui/config.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -427,6 +427,12 @@ def __getattr__(self, key):
427427
_authorized_groups_per_idp_raw,
428428
)
429429

430+
ALCF_LIST_ENDPOINTS_URL = PersistentConfig(
431+
"ALCF_LIST_ENDPOINTS_URL",
432+
"alcf.list_endpoints_url",
433+
os.environ.get("ALCF_LIST_ENDPOINTS_URL", "https://inference-api.alcf.anl.gov/resource_server/list-endpoints"),
434+
)
435+
430436
MICROSOFT_CLIENT_ID = PersistentConfig(
431437
"MICROSOFT_CLIENT_ID",
432438
"oauth.microsoft.client_id",

backend/open_webui/routers/openai.py

Lines changed: 124 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from open_webui.models.models import Models
2626
from open_webui.config import (
2727
CACHE_DIR,
28+
ALCF_LIST_ENDPOINTS_URL,
2829
)
2930
from 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]
135204
class 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

223309
async 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

Comments
 (0)