From 55f004e1da4bf6297aa8dcf3215ab0ca825c8051 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Fri, 12 Jun 2026 22:50:50 +0530
Subject: [PATCH] feat(model-connections): improve model discovery error
handling and enhance UI components
---
.../app/routes/model_connections_routes.py | 11 +++-
.../app/services/model_connection_service.py | 53 ++++++++++++++-----
.../models-selection-panel.tsx | 15 +++---
3 files changed, 55 insertions(+), 24 deletions(-)
diff --git a/surfsense_backend/app/routes/model_connections_routes.py b/surfsense_backend/app/routes/model_connections_routes.py
index 2405843a7..474d376d3 100644
--- a/surfsense_backend/app/routes/model_connections_routes.py
+++ b/surfsense_backend/app/routes/model_connections_routes.py
@@ -32,6 +32,7 @@ from app.schemas import (
VerifyConnectionResponse,
)
from app.services.model_connection_service import (
+ ModelDiscoveryError,
derive_capabilities,
discover_models,
persist_verification,
@@ -313,7 +314,10 @@ async def preview_connection_models(
search_space_id=data.search_space_id if data.scope == ConnectionScope.SEARCH_SPACE else None,
user_id=user.id,
)
- discovered = await discover_models(draft)
+ try:
+ discovered = await discover_models(draft)
+ except ModelDiscoveryError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
return [_preview_model_read(item) for item in discovered]
@@ -367,7 +371,10 @@ async def discover_connection_models(
):
conn = await _load_connection(session, connection_id)
await _assert_connection_access(session, user, conn, Permission.LLM_CONFIGS_CREATE.value)
- discovered = await discover_models(conn)
+ try:
+ discovered = await discover_models(conn)
+ except ModelDiscoveryError as exc:
+ raise HTTPException(status_code=400, detail=str(exc)) from exc
by_model_id = {model.model_id: model for model in conn.models}
for item in discovered:
db_model = by_model_id.get(item["model_id"])
diff --git a/surfsense_backend/app/services/model_connection_service.py b/surfsense_backend/app/services/model_connection_service.py
index 7742e837e..c9ee2779f 100644
--- a/surfsense_backend/app/services/model_connection_service.py
+++ b/surfsense_backend/app/services/model_connection_service.py
@@ -31,6 +31,10 @@ class VerifyResult:
message: str = ""
+class ModelDiscoveryError(Exception):
+ """User-correctable discovery failure for provider configuration issues."""
+
+
def _auth_headers(conn: Connection) -> dict[str, str]:
if not conn.api_key:
return {}
@@ -120,6 +124,23 @@ async def persist_verification(conn: Connection) -> VerifyResult:
return result
+def _discovery_error_message(conn: Connection, exc: httpx.HTTPError) -> str:
+ base_url = _base_url_or_default(conn)
+ if isinstance(exc, httpx.HTTPStatusError):
+ status_code = exc.response.status_code
+ if status_code in (401, 403):
+ return "Authentication failed while discovering models."
+ if status_code == 404:
+ spec = spec_for(conn.provider)
+ if spec.transport == Transport.OPENAI_COMPATIBLE:
+ return "OpenAI-compatible servers should expose /v1/models."
+ return "Model discovery endpoint returned 404."
+ return f"Model discovery failed with HTTP {status_code}."
+ if isinstance(exc, httpx.TimeoutException):
+ return f"Model discovery timed out: {exc}"
+ return _docker_hint(base_url, exc)
+
+
def _allowlist(conn: Connection) -> set[str]:
raw = (conn.extra or {}).get("model_ids") or []
return {str(item).strip() for item in raw if str(item).strip()}
@@ -339,20 +360,23 @@ async def discover_models(conn: Connection) -> list[dict[str, Any]]:
allowlist = _allowlist(conn)
spec = spec_for(conn.provider)
- if spec.discovery == "ollama":
- results = await _ollama_tags_then_show(conn)
- elif spec.discovery == "openrouter":
- results = await _openrouter_models(conn)
- elif spec.discovery == "anthropic_models":
- results = await _discover_anthropic_models(conn)
- elif spec.discovery == "openai_models":
- results = await _discover_openai_shaped_models(conn, conn.base_url)
- elif spec.discovery == "bedrock_models":
- results = await _discover_bedrock_models(conn)
- elif spec.discovery == "static":
- results = _litellm_static_models(conn)
- else:
- results = []
+ try:
+ if spec.discovery == "ollama":
+ results = await _ollama_tags_then_show(conn)
+ elif spec.discovery == "openrouter":
+ results = await _openrouter_models(conn)
+ elif spec.discovery == "anthropic_models":
+ results = await _discover_anthropic_models(conn)
+ elif spec.discovery == "openai_models":
+ results = await _discover_openai_shaped_models(conn, conn.base_url)
+ elif spec.discovery == "bedrock_models":
+ results = await _discover_bedrock_models(conn)
+ elif spec.discovery == "static":
+ results = _litellm_static_models(conn)
+ else:
+ results = []
+ except httpx.HTTPError as exc:
+ raise ModelDiscoveryError(_discovery_error_message(conn, exc)) from exc
if allowlist:
results = [item for item in results if item["model_id"] in allowlist]
@@ -376,6 +400,7 @@ async def test_model(conn: Connection, model: Model) -> VerifyResult:
__all__ = [
+ "ModelDiscoveryError",
"VerifyResult",
"derive_capabilities",
"discover_models",
diff --git a/surfsense_web/components/settings/model-connections/models-selection-panel.tsx b/surfsense_web/components/settings/model-connections/models-selection-panel.tsx
index 01ff0d1e7..573049f6c 100644
--- a/surfsense_web/components/settings/model-connections/models-selection-panel.tsx
+++ b/surfsense_web/components/settings/model-connections/models-selection-panel.tsx
@@ -1,4 +1,4 @@
-import { RefreshCcw } from "lucide-react";
+import { RefreshCw } from "lucide-react";
import { useState } from "react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -32,7 +32,7 @@ interface ModelsSelectionPanelProps {
export function ModelsSelectionPanel({
models,
description = "Select models to make available for this provider.",
- emptyMessage = "No models yet. Use the refresh button to discover models or add one manually.",
+ emptyMessage = "No models available.",
manualInputPlaceholder = "Add a model ID manually",
refreshLabel = "Refresh models",
isRefreshing = false,
@@ -86,14 +86,14 @@ export function ModelsSelectionPanel({
{onRefresh ? (
) : null}
@@ -113,7 +113,6 @@ export function ModelsSelectionPanel({
placeholder={manualInputPlaceholder}
/>