mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-12 22:42:13 +02:00
feat(model-connections): improve model discovery error handling and enhance UI components
This commit is contained in:
parent
407f2a9612
commit
55f004e1da
3 changed files with 55 additions and 24 deletions
|
|
@ -32,6 +32,7 @@ from app.schemas import (
|
||||||
VerifyConnectionResponse,
|
VerifyConnectionResponse,
|
||||||
)
|
)
|
||||||
from app.services.model_connection_service import (
|
from app.services.model_connection_service import (
|
||||||
|
ModelDiscoveryError,
|
||||||
derive_capabilities,
|
derive_capabilities,
|
||||||
discover_models,
|
discover_models,
|
||||||
persist_verification,
|
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,
|
search_space_id=data.search_space_id if data.scope == ConnectionScope.SEARCH_SPACE else None,
|
||||||
user_id=user.id,
|
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]
|
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)
|
conn = await _load_connection(session, connection_id)
|
||||||
await _assert_connection_access(session, user, conn, Permission.LLM_CONFIGS_CREATE.value)
|
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}
|
by_model_id = {model.model_id: model for model in conn.models}
|
||||||
for item in discovered:
|
for item in discovered:
|
||||||
db_model = by_model_id.get(item["model_id"])
|
db_model = by_model_id.get(item["model_id"])
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,10 @@ class VerifyResult:
|
||||||
message: str = ""
|
message: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
class ModelDiscoveryError(Exception):
|
||||||
|
"""User-correctable discovery failure for provider configuration issues."""
|
||||||
|
|
||||||
|
|
||||||
def _auth_headers(conn: Connection) -> dict[str, str]:
|
def _auth_headers(conn: Connection) -> dict[str, str]:
|
||||||
if not conn.api_key:
|
if not conn.api_key:
|
||||||
return {}
|
return {}
|
||||||
|
|
@ -120,6 +124,23 @@ async def persist_verification(conn: Connection) -> VerifyResult:
|
||||||
return result
|
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]:
|
def _allowlist(conn: Connection) -> set[str]:
|
||||||
raw = (conn.extra or {}).get("model_ids") or []
|
raw = (conn.extra or {}).get("model_ids") or []
|
||||||
return {str(item).strip() for item in raw if str(item).strip()}
|
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)
|
allowlist = _allowlist(conn)
|
||||||
spec = spec_for(conn.provider)
|
spec = spec_for(conn.provider)
|
||||||
|
|
||||||
if spec.discovery == "ollama":
|
try:
|
||||||
results = await _ollama_tags_then_show(conn)
|
if spec.discovery == "ollama":
|
||||||
elif spec.discovery == "openrouter":
|
results = await _ollama_tags_then_show(conn)
|
||||||
results = await _openrouter_models(conn)
|
elif spec.discovery == "openrouter":
|
||||||
elif spec.discovery == "anthropic_models":
|
results = await _openrouter_models(conn)
|
||||||
results = await _discover_anthropic_models(conn)
|
elif spec.discovery == "anthropic_models":
|
||||||
elif spec.discovery == "openai_models":
|
results = await _discover_anthropic_models(conn)
|
||||||
results = await _discover_openai_shaped_models(conn, conn.base_url)
|
elif spec.discovery == "openai_models":
|
||||||
elif spec.discovery == "bedrock_models":
|
results = await _discover_openai_shaped_models(conn, conn.base_url)
|
||||||
results = await _discover_bedrock_models(conn)
|
elif spec.discovery == "bedrock_models":
|
||||||
elif spec.discovery == "static":
|
results = await _discover_bedrock_models(conn)
|
||||||
results = _litellm_static_models(conn)
|
elif spec.discovery == "static":
|
||||||
else:
|
results = _litellm_static_models(conn)
|
||||||
results = []
|
else:
|
||||||
|
results = []
|
||||||
|
except httpx.HTTPError as exc:
|
||||||
|
raise ModelDiscoveryError(_discovery_error_message(conn, exc)) from exc
|
||||||
|
|
||||||
if allowlist:
|
if allowlist:
|
||||||
results = [item for item in results if item["model_id"] in 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__ = [
|
__all__ = [
|
||||||
|
"ModelDiscoveryError",
|
||||||
"VerifyResult",
|
"VerifyResult",
|
||||||
"derive_capabilities",
|
"derive_capabilities",
|
||||||
"discover_models",
|
"discover_models",
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { RefreshCcw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -32,7 +32,7 @@ interface ModelsSelectionPanelProps {
|
||||||
export function ModelsSelectionPanel({
|
export function ModelsSelectionPanel({
|
||||||
models,
|
models,
|
||||||
description = "Select models to make available for this provider.",
|
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",
|
manualInputPlaceholder = "Add a model ID manually",
|
||||||
refreshLabel = "Refresh models",
|
refreshLabel = "Refresh models",
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
|
|
@ -86,14 +86,14 @@ export function ModelsSelectionPanel({
|
||||||
</Button>
|
</Button>
|
||||||
{onRefresh ? (
|
{onRefresh ? (
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing}
|
||||||
aria-label={refreshLabel}
|
aria-label={refreshLabel}
|
||||||
>
|
>
|
||||||
<RefreshCcw className="h-4 w-4" />
|
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -113,7 +113,6 @@ export function ModelsSelectionPanel({
|
||||||
placeholder={manualInputPlaceholder}
|
placeholder={manualInputPlaceholder}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
|
||||||
size="sm"
|
size="sm"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={addModel}
|
onClick={addModel}
|
||||||
|
|
@ -135,9 +134,9 @@ export function ModelsSelectionPanel({
|
||||||
<Button
|
<Button
|
||||||
key={filter.key}
|
key={filter.key}
|
||||||
type="button"
|
type="button"
|
||||||
variant={isActive ? "secondary" : "outline"}
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="h-7 rounded-full px-3 text-xs"
|
className={`h-7 rounded-full px-3 text-xs ${isActive ? "" : "opacity-80"}`}
|
||||||
onClick={() => setModelFilter(isActive ? null : filter.key)}
|
onClick={() => setModelFilter(isActive ? null : filter.key)}
|
||||||
>
|
>
|
||||||
{filter.label}
|
{filter.label}
|
||||||
|
|
@ -148,7 +147,7 @@ export function ModelsSelectionPanel({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="max-h-80 overflow-y-auto rounded-xl border bg-muted/20 p-2">
|
<div className="h-80 overflow-y-auto rounded-xl border bg-muted/20 p-2">
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue