diff --git a/surfsense_backend/app/routes/model_connections_routes.py b/surfsense_backend/app/routes/model_connections_routes.py index 84e9b830d..b7edfcc77 100644 --- a/surfsense_backend/app/routes/model_connections_routes.py +++ b/surfsense_backend/app/routes/model_connections_routes.py @@ -21,7 +21,6 @@ from app.schemas import ( ConnectionCreate, ConnectionRead, ConnectionUpdate, - ModelCreate, ModelPreviewRead, ModelProviderRead, ModelRead, @@ -618,49 +617,6 @@ async def discover_connection_models( return [_model_read(model) for model in conn.models] -@router.post("/model-connections/{connection_id}/models", response_model=ModelRead) -async def add_manual_model( - connection_id: int, - data: ModelCreate, - session: AsyncSession = Depends(get_async_session), - auth: AuthContext = Depends(get_auth_context), -): - conn = await _load_connection(session, connection_id) - await _assert_connection_access( - session, auth, conn, Permission.LLM_CONFIGS_UPDATE.value - ) - - model_id = data.model_id.strip() - if not model_id: - raise HTTPException(status_code=400, detail="model_id is required") - if any(existing.model_id == model_id for existing in conn.models): - raise HTTPException( - status_code=400, detail="Model already exists on this connection" - ) - - capabilities = derive_capabilities(conn, model_id) - model = Model( - connection_id=conn.id, - model_id=model_id, - display_name=data.display_name or None, - source=ModelSource.MANUAL, - capabilities_override={}, - enabled=True, - catalog={}, - ) - _apply_model_facts(model, capabilities) - session.add(model) - await session.commit() - await session.refresh(model) - conn = await _load_connection(session, connection_id) - await _default_unset_roles(session, conn, list(conn.models)) - if conn.search_space_id is not None: - await _clear_invalid_roles(session, conn.search_space_id) - await session.commit() - await session.refresh(model) - return _model_read(model) - - @router.patch( "/model-connections/{connection_id}/models", response_model=list[ModelRead] ) diff --git a/surfsense_backend/app/schemas/__init__.py b/surfsense_backend/app/schemas/__init__.py index f111f0226..5f5252fbd 100644 --- a/surfsense_backend/app/schemas/__init__.py +++ b/surfsense_backend/app/schemas/__init__.py @@ -43,7 +43,6 @@ from .model_connections import ( ConnectionCreate, ConnectionRead, ConnectionUpdate, - ModelCreate, ModelPreviewRead, ModelProviderRead, ModelRead, @@ -203,7 +202,6 @@ __all__ = [ "MembershipRead", "MembershipReadWithUser", "MembershipUpdate", - "ModelCreate", "ModelPreviewRead", "ModelProviderRead", "ModelRead", diff --git a/surfsense_backend/app/schemas/model_connections.py b/surfsense_backend/app/schemas/model_connections.py index 0eec666c1..0f656dad3 100644 --- a/surfsense_backend/app/schemas/model_connections.py +++ b/surfsense_backend/app/schemas/model_connections.py @@ -93,17 +93,6 @@ class ConnectionUpdate(BaseModel): enabled: bool | None = None -class ModelCreate(BaseModel): - """Manually register a model id on a connection. - - For providers without a usable ``/models`` endpoint (Perplexity, MiniMax, - Azure deployments, etc.) or to pin a single model from a noisy provider. - """ - - model_id: str = Field(..., max_length=255) - display_name: str | None = Field(None, max_length=255) - - class ModelUpdate(BaseModel): display_name: str | None = Field(None, max_length=255) enabled: bool | None = None diff --git a/surfsense_backend/app/services/model_connection_service.py b/surfsense_backend/app/services/model_connection_service.py index cdfd1d725..be442bcb2 100644 --- a/surfsense_backend/app/services/model_connection_service.py +++ b/surfsense_backend/app/services/model_connection_service.py @@ -202,11 +202,6 @@ def _discovery_error_message(conn: Connection, exc: httpx.HTTPError) -> str: 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()} - - def _litellm_info(model_string: str, model_id: str) -> dict[str, Any]: with contextlib.suppress(Exception): info = litellm.get_model_info(model=model_string) @@ -438,7 +433,6 @@ async def _discover_bedrock_models(conn: Connection) -> list[dict[str, Any]]: async def discover_models(conn: Connection) -> list[dict[str, Any]]: - allowlist = _allowlist(conn) spec = spec_for(conn.provider) try: @@ -459,8 +453,6 @@ async def discover_models(conn: Connection) -> list[dict[str, Any]]: 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] return results diff --git a/surfsense_backend/app/services/model_resolver.py b/surfsense_backend/app/services/model_resolver.py index f31b658a4..590290b0c 100644 --- a/surfsense_backend/app/services/model_resolver.py +++ b/surfsense_backend/app/services/model_resolver.py @@ -59,6 +59,10 @@ def to_litellm( kwargs: dict[str, Any] = {} if api_key: kwargs["api_key"] = api_key + elif provider == "lm_studio": + # LiteLLM's OpenAI-compatible adapter expects an api_key value even + # when LM Studio accepts unauthenticated local requests. + kwargs["api_key"] = "not-needed" prefix = spec.litellm_prefix or str(provider) model_string = f"{prefix}/{model_id}" if prefix else model_id diff --git a/surfsense_backend/tests/unit/services/test_model_connections.py b/surfsense_backend/tests/unit/services/test_model_connections.py index b4e7c18d7..efc056465 100644 --- a/surfsense_backend/tests/unit/services/test_model_connections.py +++ b/surfsense_backend/tests/unit/services/test_model_connections.py @@ -64,6 +64,22 @@ def test_openai_compatible_resolver_uses_explicit_api_base() -> None: assert ensure_v1("http://example.com/v1") == "http://example.com/v1" +def test_lm_studio_resolver_supplies_dummy_api_key_when_empty() -> None: + model, kwargs = to_litellm( + { + "provider": "lm_studio", + "base_url": "http://host.docker.internal:1234/v1", + "api_key": None, + "extra": {}, + }, + "tinyllama-1.1b-chat-v0.6", + ) + + assert model == "openai/tinyllama-1.1b-chat-v0.6" + assert kwargs["api_base"] == "http://host.docker.internal:1234/v1" + assert kwargs["api_key"] == "not-needed" + + def test_ollama_resolver_uses_native_api_base() -> None: model, kwargs = to_litellm( { diff --git a/surfsense_web/atoms/model-connections/model-connections-mutation.atoms.ts b/surfsense_web/atoms/model-connections/model-connections-mutation.atoms.ts index f00bf76f9..e9ff559c0 100644 --- a/surfsense_web/atoms/model-connections/model-connections-mutation.atoms.ts +++ b/surfsense_web/atoms/model-connections/model-connections-mutation.atoms.ts @@ -4,7 +4,6 @@ import type { ConnectionCreateRequest, ConnectionRead, ConnectionUpdateRequest, - ModelCreateRequest, ModelPreviewRead, ModelRead, ModelRoles, @@ -95,13 +94,7 @@ export const verifyModelConnectionMutationAtom = atomWithMutation((get) => { if (result.ok) { toast.success("Connection verified"); } else { - // Non-fatal: many providers lack a /models endpoint yet still serve - // chat. Guide the user to add model IDs manually instead of alarming. - toast.warning( - result.message - ? `${result.message} Chat may still work — add model IDs manually.` - : "Couldn't list models. Chat may still work — add model IDs manually." - ); + toast.warning(result.message || "Couldn't verify this connection."); } invalidateModelConnections(searchSpaceId); }, @@ -148,20 +141,6 @@ export const testPreviewModelMutationAtom = atomWithMutation(() => { }; }); -export const addManualModelMutationAtom = atomWithMutation((get) => { - const searchSpaceId = Number(get(activeSearchSpaceIdAtom)); - return { - mutationKey: ["models", "add-manual"], - mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) => - modelConnectionsApiService.addManualModel(connectionId, data), - onSuccess: () => { - toast.success("Model added"); - invalidateModelConnections(searchSpaceId); - }, - onError: (error: Error) => toast.error(error.message || "Failed to add model"), - }; -}); - export const updateModelMutationAtom = atomWithMutation((get) => { const searchSpaceId = Number(get(activeSearchSpaceIdAtom)); return { diff --git a/surfsense_web/components/settings/model-connections/bedrock-connect-form.tsx b/surfsense_web/components/settings/model-connections/bedrock-connect-form.tsx index f76308421..0da545bd8 100644 --- a/surfsense_web/components/settings/model-connections/bedrock-connect-form.tsx +++ b/surfsense_web/components/settings/model-connections/bedrock-connect-form.tsx @@ -113,7 +113,7 @@ export function BedrockConnectForm({ onDraftChange }: ProviderConnectFormProps)

) : null}

- Add Bedrock model IDs from the provider's settings after connecting. + After entering credentials, refresh models to discover the Bedrock catalog for this region.

); diff --git a/surfsense_web/components/settings/model-connections/connection-settings-dialog.tsx b/surfsense_web/components/settings/model-connections/connection-settings-dialog.tsx index 1f16c3bd0..0120e28ed 100644 --- a/surfsense_web/components/settings/model-connections/connection-settings-dialog.tsx +++ b/surfsense_web/components/settings/model-connections/connection-settings-dialog.tsx @@ -2,7 +2,6 @@ import { useAtomValue } from "jotai"; import { Eye, EyeOff, Settings } from "lucide-react"; import { useMemo, useState } from "react"; import { - addManualModelMutationAtom, bulkUpdateModelsMutationAtom, discoverConnectionModelsMutationAtom, testPreviewModelMutationAtom, @@ -50,26 +49,17 @@ export function ConnectionSettingsDialog({ const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom); const testPreviewModel = useAtomValue(testPreviewModelMutationAtom); const updateConnection = useAtomValue(updateModelConnectionMutationAtom); - const addManualModel = useAtomValue(addManualModelMutationAtom); const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom); - const allowlist = Array.isArray(connection.extra?.model_ids) - ? (connection.extra.model_ids as string[]) - : []; const [isOpen, setIsOpen] = useState(false); const [baseUrlDraft, setBaseUrlDraft] = useState(connection.base_url ?? ""); const [apiKeyDraft, setApiKeyDraft] = useState(""); const [showApiKey, setShowApiKey] = useState(false); - const [allowlistText, setAllowlistText] = useState(allowlist.join(", ")); const [isSavingConnectionSettings, setIsSavingConnectionSettings] = useState(false); const [draftEnabledModelIds, setDraftEnabledModelIds] = useState(() => enabledModelIds(connection.models) ); - const isLocal = - connection.provider === "ollama_chat" || - connection.provider === "lm_studio" || - !connection.base_url?.startsWith("https"); const hasConnectionChanges = baseUrlDraft.trim() !== (connection.base_url ?? "") || apiKeyDraft.trim() !== (connection.api_key ?? ""); @@ -93,7 +83,6 @@ export function ConnectionSettingsDialog({ setBaseUrlDraft(connection.base_url ?? ""); setApiKeyDraft(connection.api_key ?? ""); setShowApiKey(false); - setAllowlistText(allowlist.join(", ")); setIsSavingConnectionSettings(false); setDraftEnabledModelIds(enabledModelIds(connection.models)); } @@ -168,17 +157,6 @@ export function ConnectionSettingsDialog({ } } - function saveAllowlist() { - const ids = allowlistText - .split(",") - .map((value) => value.trim()) - .filter(Boolean); - updateConnection.mutate({ - id: connection.id, - data: { extra: { ...(connection.extra ?? {}), model_ids: ids } }, - }); - } - function handleToggleModel(model: SelectableModel, enabled: boolean) { if (typeof model.id !== "number") return; const modelId = model.id; @@ -276,41 +254,15 @@ export function ConnectionSettingsDialog({ - {!isLocal ? ( -
- -
- setAllowlistText(event.target.value)} - placeholder="Comma-separated, e.g. anthropic/claude-sonnet-4-5, google/gemini-2.5-pro" - /> - -
-

- Leave empty to discover all models. Recommended for providers with large catalogs. -

-
- ) : null} - discoverModels.mutate(connection.id)} - onAddManual={(modelId) => - addManualModel.mutate({ - connectionId: connection.id, - data: { model_id: modelId }, - }) - } onToggleModel={handleToggleModel} onBulkToggle={handleBulkToggle} /> diff --git a/surfsense_web/components/settings/model-connections/model-provider-connections-panel.tsx b/surfsense_web/components/settings/model-connections/model-provider-connections-panel.tsx index a703ab1c8..13fd39894 100644 --- a/surfsense_web/components/settings/model-connections/model-provider-connections-panel.tsx +++ b/surfsense_web/components/settings/model-connections/model-provider-connections-panel.tsx @@ -201,22 +201,6 @@ export function ModelProviderConnectionsPanel({ ); } - function addConnectModel(modelId: string) { - setConnectModels((current) => { - if (current.some((model) => model.model_id === modelId)) return current; - return [ - ...current, - { - model_id: modelId, - display_name: modelId, - source: "MANUAL", - enabled: true, - metadata: {}, - }, - ]; - }); - } - function toggleConnectModel(model: SelectableModel, enabled: boolean) { setConnectModels((current) => current.map((item) => (item.model_id === model.model_id ? { ...item, enabled } : item)) @@ -277,7 +261,6 @@ export function ModelProviderConnectionsPanel({ previewModels={connectModels} isPreviewingModels={previewModels.isPending} onPreviewModels={refreshConnectModels} - onAddPreviewModel={addConnectModel} onTogglePreviewModel={toggleConnectModel} onBulkTogglePreviewModels={bulkToggleConnectModels} /> 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 3c6990afb..b6258b7b2 100644 --- a/surfsense_web/components/settings/model-connections/models-selection-panel.tsx +++ b/surfsense_web/components/settings/model-connections/models-selection-panel.tsx @@ -1,10 +1,7 @@ import { RefreshCw } from "lucide-react"; import { useState } from "react"; -import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; -import { Input } from "@/components/ui/input"; -import { Spinner } from "@/components/ui/spinner"; import { capability, capabilityLabels, @@ -18,14 +15,12 @@ interface ModelsSelectionPanelProps { models: SelectableModel[]; description?: string; emptyMessage?: string; - manualInputPlaceholder?: string; refreshLabel?: string; isRefreshing?: boolean; - isAddingManual?: boolean; + isRefreshDisabled?: boolean; isUpdatingModel?: boolean; isBulkUpdating?: boolean; onRefresh?: () => void; - onAddManual?: (modelId: string) => void; onToggleModel?: (model: SelectableModel, enabled: boolean) => void; onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void; } @@ -34,18 +29,15 @@ export function ModelsSelectionPanel({ models, description = "Select models to make available for this provider.", emptyMessage = "No models available.", - manualInputPlaceholder = "Add a model ID manually", refreshLabel = "Refresh models", isRefreshing = false, - isAddingManual = false, + isRefreshDisabled = false, isUpdatingModel = false, isBulkUpdating = false, onRefresh, - onAddManual, onToggleModel, onBulkToggle, }: ModelsSelectionPanelProps) { - const [manualModelId, setManualModelId] = useState(""); const [modelFilter, setModelFilter] = useState(null); const filteredModels = modelFilter @@ -54,13 +46,6 @@ export function ModelsSelectionPanel({ const allFilteredModelsEnabled = filteredModels.length > 0 && filteredModels.every((model) => model.enabled); - function addModel() { - const modelId = manualModelId.trim(); - if (!modelId || !onAddManual) return; - onAddManual(modelId); - setManualModelId(""); - } - function toggleFilteredModels() { const nextEnabled = !allFilteredModelsEnabled; const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled); @@ -91,7 +76,7 @@ export function ModelsSelectionPanel({ size="icon" type="button" onClick={onRefresh} - disabled={isRefreshing} + disabled={isRefreshing || isRefreshDisabled} aria-label={refreshLabel} > @@ -100,32 +85,6 @@ export function ModelsSelectionPanel({ - {onAddManual ? ( -
- setManualModelId(event.target.value)} - onKeyDown={(event) => { - if (event.key === "Enter") { - event.preventDefault(); - addModel(); - } - }} - placeholder={manualInputPlaceholder} - /> - -
- ) : null} - {models.length > 0 ? (
Filter models @@ -179,11 +138,6 @@ export function ModelsSelectionPanel({
{modelLabel(model)} - {model.source === "MANUAL" ? ( - - manual - - ) : null}
{capabilityLabels(model) || "No discovered capabilities"} diff --git a/surfsense_web/components/settings/model-connections/provider-connect-dialog.tsx b/surfsense_web/components/settings/model-connections/provider-connect-dialog.tsx index 2eee2cf8c..2fba3bcbf 100644 --- a/surfsense_web/components/settings/model-connections/provider-connect-dialog.tsx +++ b/surfsense_web/components/settings/model-connections/provider-connect-dialog.tsx @@ -33,7 +33,6 @@ interface ProviderConnectDialogProps { previewModels?: SelectableModel[]; isPreviewingModels?: boolean; onPreviewModels?: (draft: ConnectionDraft) => void; - onAddPreviewModel?: (modelId: string) => void; onTogglePreviewModel?: (model: SelectableModel, enabled: boolean) => void; onBulkTogglePreviewModels?: (models: SelectableModel[], enabled: boolean) => void; } @@ -53,7 +52,6 @@ export function ProviderConnectDialog({ previewModels = [], isPreviewingModels = false, onPreviewModels, - onAddPreviewModel, onTogglePreviewModel, onBulkTogglePreviewModels, }: ProviderConnectDialogProps) { @@ -94,7 +92,8 @@ export function ProviderConnectDialog({ return "Select the models to enable for this provider"; })(); - const canRefreshModels = !isAzure && !isVertex && (!isBedrock || canSubmit); + const supportsModelRefresh = !isAzure && !isVertex; + const canRefreshModels = supportsModelRefresh && canSubmit; const hasEnabledModel = previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId); const canConnect = canSubmit && hasEnabledModel; @@ -130,18 +129,22 @@ export function ProviderConnectDialog({ )} - + {!isAzure ? ( + <> + - onPreviewModels?.(currentDraft) : undefined} - onAddManual={onAddPreviewModel} - onToggleModel={onTogglePreviewModel} - onBulkToggle={onBulkTogglePreviewModels} - /> + onPreviewModels?.(currentDraft) : undefined} + onToggleModel={onTogglePreviewModel} + onBulkToggle={onBulkTogglePreviewModels} + /> + + ) : null}
onOpenChange(false)} diff --git a/surfsense_web/components/settings/model-connections/vertex-connect-form.tsx b/surfsense_web/components/settings/model-connections/vertex-connect-form.tsx index 1027742bc..3701ea14e 100644 --- a/surfsense_web/components/settings/model-connections/vertex-connect-form.tsx +++ b/surfsense_web/components/settings/model-connections/vertex-connect-form.tsx @@ -111,7 +111,7 @@ export function VertexConnectForm({ onDraftChange }: ProviderConnectFormProps) {
)}

- Add Vertex AI model IDs from the provider's settings after connecting. + SurfSense will show supported Vertex AI models from the provider catalog.

); diff --git a/surfsense_web/contracts/types/model-connections.types.ts b/surfsense_web/contracts/types/model-connections.types.ts index 0f0c7591e..73dff23eb 100644 --- a/surfsense_web/contracts/types/model-connections.types.ts +++ b/surfsense_web/contracts/types/model-connections.types.ts @@ -74,11 +74,6 @@ export const connectionUpdateRequest = z.object({ enabled: z.boolean().optional(), }); -export const modelCreateRequest = z.object({ - model_id: z.string().min(1), - display_name: z.string().nullable().optional(), -}); - export const modelUpdateRequest = z.object({ display_name: z.string().nullable().optional(), enabled: z.boolean().optional(), @@ -135,7 +130,6 @@ export type ConnectionRead = z.infer; export type ConnectionCreateRequest = z.infer; export type ModelTestPreviewRequest = z.infer; export type ConnectionUpdateRequest = z.infer; -export type ModelCreateRequest = z.infer; export type ModelUpdateRequest = z.infer; export type ModelsBulkUpdateRequest = z.infer; export type ModelRoles = z.infer; diff --git a/surfsense_web/lib/apis/model-connections-api.service.ts b/surfsense_web/lib/apis/model-connections-api.service.ts index c69bcbef2..6083642f5 100644 --- a/surfsense_web/lib/apis/model-connections-api.service.ts +++ b/surfsense_web/lib/apis/model-connections-api.service.ts @@ -8,7 +8,6 @@ import { connectionUpdateRequest, type GlobalLlmConfigStatus, globalLlmConfigStatus, - type ModelCreateRequest, type ModelPreviewRead, type ModelProviderRead, type ModelRead, @@ -16,7 +15,6 @@ import { type ModelsBulkUpdateRequest, type ModelTestPreviewRequest, type ModelUpdateRequest, - modelCreateRequest, modelListResponse, modelPreviewListResponse, modelProviderListResponse, @@ -112,19 +110,6 @@ class ModelConnectionsApiService { }); }; - addManualModel = async ( - connectionId: number, - request: ModelCreateRequest - ): Promise => { - const parsed = modelCreateRequest.safeParse(request); - if (!parsed.success) { - throw new ValidationError(parsed.error.issues.map((issue) => issue.message).join(", ")); - } - return baseApiService.post(`/api/v1/model-connections/${connectionId}/models`, modelRead, { - body: parsed.data, - }); - }; - updateModel = async (id: number, request: ModelUpdateRequest): Promise => { const parsed = modelUpdateRequest.safeParse(request); if (!parsed.success) {