From 31ec939da9a5d79c184e1f9ec7a64d0ff49d853f Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sat, 27 Jun 2026 03:13:50 +0530
Subject: [PATCH 1/5] fix(model_resolver): supply dummy API key for LM Studio
when not provided
---
surfsense_backend/app/services/model_resolver.py | 4 ++++
.../unit/services/test_model_connections.py | 16 ++++++++++++++++
2 files changed, 20 insertions(+)
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(
{
From 0df51c60f372f7348382d5631512bf58c1918f3f Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sat, 27 Jun 2026 03:46:15 +0530
Subject: [PATCH 2/5] refactor(models): remove manual model registration
backend
---
.../app/routes/model_connections_routes.py | 44 -------------------
surfsense_backend/app/schemas/__init__.py | 2 -
.../app/schemas/model_connections.py | 11 -----
.../app/services/model_connection_service.py | 8 ----
4 files changed, 65 deletions(-)
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
From 3b507208bccf2ab1b149943059b9c51ace95c5d1 Mon Sep 17 00:00:00 2001
From: Anish Sarkar <104695310+AnishSarkar22@users.noreply.github.com>
Date: Sat, 27 Jun 2026 03:46:24 +0530
Subject: [PATCH 3/5] refactor(models): remove manual model creation client API
---
.../model-connections-mutation.atoms.ts | 23 +------------------
.../types/model-connections.types.ts | 6 -----
.../lib/apis/model-connections-api.service.ts | 15 ------------
3 files changed, 1 insertion(+), 43 deletions(-)
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/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
- 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 ? ( -- Leave empty to discover all models. Recommended for providers with large catalogs. -
-- Add Vertex AI model IDs from the provider's settings after connecting. + SurfSense will show supported Vertex AI models from the provider catalog.