diff --git a/routes/model_routes.py b/routes/model_routes.py index 9c85054..bd209db 100644 --- a/routes/model_routes.py +++ b/routes/model_routes.py @@ -909,6 +909,34 @@ def setup_model_routes(model_discovery): require_model_list = _truthy(require_models) should_probe = require_model_list or not _truthy(skip_probe) + # 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. + from src.auth_helpers import get_current_user as _gcu_dedup + _caller = _gcu_dedup(request) or None + _db_dedup = SessionLocal() + try: + existing = ( + _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() + ) + if existing: + return { + "id": existing.id, + "name": existing.name, + "base_url": existing.base_url, + "models": json.loads(existing.cached_models) if existing.cached_models else [], + "online": True, + "status": "online", + "existing": True, + } + finally: + _db_dedup.close() + # Quick model list fetch (1s timeout — if endpoint is slow, it'll update on next refresh) _probe_timeout = 3 if (":11434" in base_url or "ollama" in base_url.lower()) else 1 model_ids = _probe_endpoint(base_url, api_key.strip() or None, timeout=_probe_timeout) if should_probe else [] diff --git a/static/js/admin.js b/static/js/admin.js index e032f3e..10947fb 100644 --- a/static/js/admin.js +++ b/static/js/admin.js @@ -952,8 +952,10 @@ function initEndpointForm() { msg.textContent = 'No model servers found. Make sure vLLM, llama.cpp, SGLang, or Ollama is running. Docker users may need OLLAMA_HOST=0.0.0.0:11434.'; msg.className = 'admin-error'; } else { - // Auto-add each discovered endpoint + // Auto-add each discovered endpoint. Server dedupes on base_url + // and returns `existing: true` for already-registered ones. let added = 0; + let skipped = 0; for (const item of items) { const base = item.url.replace('/chat/completions', '').replace(/\/$/, ''); const fd = new FormData(); @@ -961,12 +963,18 @@ function initEndpointForm() { fd.append('skip_probe', 'false'); const r = await fetch('/api/model-endpoints', { method: 'POST', body: fd }); if (r.ok) { - added++; - try { const dd = await r.json(); if (dd && dd.id) _recentlyAddedEpId = String(dd.id); } catch (_) {} + try { + const dd = await r.json(); + if (dd && dd.existing) { skipped++; } + else { added++; if (dd && dd.id) _recentlyAddedEpId = String(dd.id); } + } catch (_) { added++; } } } const totalModels = items.reduce((n, i) => n + (i.models ? i.models.length : 0), 0); - msg.innerHTML = `Found ${items.length} server${items.length !== 1 ? 's' : ''} with ${totalModels} model${totalModels !== 1 ? 's' : ''}` + (added ? ` — added ${added} new` : ' (already added)'); + const parts = [`Found ${items.length} server${items.length !== 1 ? 's' : ''} with ${totalModels} model${totalModels !== 1 ? 's' : ''}`]; + if (added) parts.push(`added ${added} new`); + if (skipped) parts.push(`${skipped} already added`); + msg.innerHTML = parts.join(' — '); msg.className = 'admin-success'; loadEndpoints(); }