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} />