diff --git a/routes/model_routes.py b/routes/model_routes.py index 6220305d3..14d1b94e6 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -5,6 +5,7 @@ import re import uuid import json import socket +import hashlib import time as _time import logging import httpx @@ -850,6 +851,14 @@ def _visible_models(cached_models, hidden_models, pinned_models=None): return [m for m in merged if m not in hidden] +def _api_key_fingerprint(api_key: Optional[str]) -> str: + """Stable, non-secret label for distinguishing same-URL credentials.""" + key = (api_key or "").strip() + if not key: + return "" + return hashlib.sha256(key.encode("utf-8")).hexdigest()[:8] + + def setup_model_routes(model_discovery): router = APIRouter(prefix="/api") @@ -1397,6 +1406,7 @@ def setup_model_routes(model_discovery): "name": r.name, "base_url": r.base_url, "has_key": bool(r.api_key), + "api_key_fingerprint": _api_key_fingerprint(r.api_key), "is_enabled": r.is_enabled, "models": visible, "pinned_models": pinned, @@ -1463,21 +1473,34 @@ def setup_model_routes(model_discovery): ) explicit_timeout = _explicit_model_list_timeout(base_url, requested_kind, refresh_timeout) - # Dedupe: if an endpoint with the same base_url already exists and - # is reachable by the caller (shared or owned by them), return it - # instead of creating a duplicate row. Fixes "Scan for Servers" - # re-adding manually-added endpoints under their host:port name. + # Dedupe: if an endpoint with the same base_url and compatible + # credentials already exists and is reachable by the caller (shared or + # owned by them), return it instead of creating a duplicate row. Keep + # same-url/different-key rows distinct so users can group the same + # provider URL under multiple credentials. from src.auth_helpers import get_current_user as _gcu_dedup _caller = _gcu_dedup(request) or None + _incoming_api_key = api_key.strip() _db_dedup = SessionLocal() try: - existing = ( + _same_url_rows = ( _db_dedup.query(ModelEndpoint) .filter(ModelEndpoint.base_url == base_url) .filter((ModelEndpoint.owner.is_(None)) | (ModelEndpoint.owner == _caller)) .order_by(ModelEndpoint.owner.desc()) # prefer owned over shared - .first() + .all() ) + existing = None + _empty_key_existing = None + for _candidate in _same_url_rows: + _candidate_key = (getattr(_candidate, "api_key", None) or "").strip() + if _candidate_key == _incoming_api_key: + existing = _candidate + break + if _incoming_api_key and not _candidate_key and _empty_key_existing is None: + _empty_key_existing = _candidate + if existing is None and _incoming_api_key and _empty_key_existing is not None: + existing = _empty_key_existing if existing: changed = False # Persist any incoming pinned IDs onto the existing row. An @@ -1526,6 +1549,8 @@ def setup_model_routes(model_discovery): "id": existing.id, "name": existing.name, "base_url": existing.base_url, + "has_key": bool(existing.api_key), + "api_key_fingerprint": _api_key_fingerprint(existing.api_key), "models": _visible_models( existing_models, getattr(existing, "hidden_models", None), @@ -1599,6 +1624,8 @@ def setup_model_routes(model_discovery): "id": ep_id, "name": name.strip(), "base_url": base_url, + "has_key": bool(api_key.strip()), + "api_key_fingerprint": _api_key_fingerprint(api_key), "models": _merge_model_ids(model_ids, _pinned), "pinned_models": _pinned, "online": bool(model_ids) or bool(_pinned) or bool(ping.get("reachable")), @@ -1948,6 +1975,8 @@ def setup_model_routes(model_discovery): "name": ep.name, "model_type": ep.model_type, "base_url": ep.base_url, + "has_key": bool(ep.api_key), + "api_key_fingerprint": _api_key_fingerprint(ep.api_key), "pinned_models": _normalize_model_ids(getattr(ep, "pinned_models", None)), "endpoint_kind": getattr(ep, "endpoint_kind", None) or "auto", "model_refresh_mode": getattr(ep, "model_refresh_mode", None) or "auto", diff --git a/static/js/admin.js b/static/js/admin.js index 5211bf62d..5d3d4a356 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -417,6 +417,9 @@ async function loadEndpoints() { const justAddedClass = (_recentlyAddedEpId && String(ep.id) === _recentlyAddedEpId) ? ' adm-ep-just-added' : ''; const category = ep.category || (_isLocalEndpoint(ep.base_url) ? 'local' : 'api'); const kindLabel = ep.endpoint_kind && ep.endpoint_kind !== 'auto' ? ep.endpoint_kind.toUpperCase() : ''; + const keyLabel = ep.has_key + ? (ep.api_key_fingerprint ? ` (key ${esc(ep.api_key_fingerprint)})` : ' (key set)') + : ''; return `