mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-07-26 23:51:14 +02:00
Merge pull request #1551 from AnishSarkar22/fix/models
feat(model connection): remove manual model creation
This commit is contained in:
commit
041dfaf5f2
16 changed files with 74 additions and 244 deletions
|
|
@ -24,7 +24,6 @@ from app.schemas import (
|
||||||
ConnectionRead,
|
ConnectionRead,
|
||||||
ConnectionUpdate,
|
ConnectionUpdate,
|
||||||
LlmSetupStatusRead,
|
LlmSetupStatusRead,
|
||||||
ModelCreate,
|
|
||||||
ModelPreviewRead,
|
ModelPreviewRead,
|
||||||
ModelProviderRead,
|
ModelProviderRead,
|
||||||
ModelRead,
|
ModelRead,
|
||||||
|
|
@ -619,49 +618,6 @@ async def discover_connection_models(
|
||||||
return [_model_read(model) for model in conn.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.workspace_id is not None:
|
|
||||||
await _clear_invalid_roles(session, conn.workspace_id)
|
|
||||||
await session.commit()
|
|
||||||
await session.refresh(model)
|
|
||||||
return _model_read(model)
|
|
||||||
|
|
||||||
|
|
||||||
@router.patch(
|
@router.patch(
|
||||||
"/model-connections/{connection_id}/models", response_model=list[ModelRead]
|
"/model-connections/{connection_id}/models", response_model=list[ModelRead]
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ from .model_connections import (
|
||||||
ConnectionRead,
|
ConnectionRead,
|
||||||
ConnectionUpdate,
|
ConnectionUpdate,
|
||||||
LlmSetupStatusRead,
|
LlmSetupStatusRead,
|
||||||
ModelCreate,
|
|
||||||
ModelPreviewRead,
|
ModelPreviewRead,
|
||||||
ModelProviderRead,
|
ModelProviderRead,
|
||||||
ModelRead,
|
ModelRead,
|
||||||
|
|
@ -205,7 +204,6 @@ __all__ = [
|
||||||
"MembershipRead",
|
"MembershipRead",
|
||||||
"MembershipReadWithUser",
|
"MembershipReadWithUser",
|
||||||
"MembershipUpdate",
|
"MembershipUpdate",
|
||||||
"ModelCreate",
|
|
||||||
"ModelPreviewRead",
|
"ModelPreviewRead",
|
||||||
"ModelProviderRead",
|
"ModelProviderRead",
|
||||||
"ModelRead",
|
"ModelRead",
|
||||||
|
|
|
||||||
|
|
@ -93,17 +93,6 @@ class ConnectionUpdate(BaseModel):
|
||||||
enabled: bool | None = None
|
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):
|
class ModelUpdate(BaseModel):
|
||||||
display_name: str | None = Field(None, max_length=255)
|
display_name: str | None = Field(None, max_length=255)
|
||||||
enabled: bool | None = None
|
enabled: bool | None = None
|
||||||
|
|
|
||||||
|
|
@ -203,11 +203,6 @@ def _discovery_error_message(conn: Connection, exc: httpx.HTTPError) -> str:
|
||||||
return _docker_hint(base_url, 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()}
|
|
||||||
|
|
||||||
|
|
||||||
def _litellm_info(model_string: str, model_id: str) -> dict[str, Any]:
|
def _litellm_info(model_string: str, model_id: str) -> dict[str, Any]:
|
||||||
with contextlib.suppress(Exception):
|
with contextlib.suppress(Exception):
|
||||||
info = litellm.get_model_info(model=model_string)
|
info = litellm.get_model_info(model=model_string)
|
||||||
|
|
@ -449,7 +444,6 @@ async def _discover_bedrock_models(conn: Connection) -> list[dict[str, Any]]:
|
||||||
|
|
||||||
|
|
||||||
async def discover_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)
|
spec = spec_for(conn.provider)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
@ -472,8 +466,6 @@ async def discover_models(conn: Connection) -> list[dict[str, Any]]:
|
||||||
except httpx.HTTPError as exc:
|
except httpx.HTTPError as exc:
|
||||||
raise ModelDiscoveryError(_discovery_error_message(conn, exc)) from 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
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,10 @@ def to_litellm(
|
||||||
kwargs: dict[str, Any] = {}
|
kwargs: dict[str, Any] = {}
|
||||||
if api_key:
|
if api_key:
|
||||||
kwargs["api_key"] = 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)
|
prefix = spec.litellm_prefix or str(provider)
|
||||||
model_string = f"{prefix}/{model_id}" if prefix else model_id
|
model_string = f"{prefix}/{model_id}" if prefix else model_id
|
||||||
|
|
|
||||||
|
|
@ -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"
|
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_openai_compatible_raw_resolver_does_not_append_v1() -> None:
|
def test_openai_compatible_raw_resolver_does_not_append_v1() -> None:
|
||||||
model, kwargs = to_litellm(
|
model, kwargs = to_litellm(
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@
|
||||||
--sidebar-ring: oklch(0.708 0 0);
|
--sidebar-ring: oklch(0.708 0 0);
|
||||||
--main-panel: var(--panel);
|
--main-panel: var(--panel);
|
||||||
--syntax-bg: #f5f5f5;
|
--syntax-bg: #f5f5f5;
|
||||||
--brand: oklch(0.623 0.214 259.815);
|
--brand: oklch(0.546 0.245 262.881);
|
||||||
--highlight: oklch(0.852 0.199 91.936);
|
--highlight: oklch(0.852 0.199 91.936);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -103,7 +103,7 @@ html[data-surfsense-auth-type="LOCAL"] .runtime-auth-google {
|
||||||
--sidebar-ring: oklch(0.439 0 0);
|
--sidebar-ring: oklch(0.439 0 0);
|
||||||
--main-panel: var(--panel);
|
--main-panel: var(--panel);
|
||||||
--syntax-bg: #1e1e1e;
|
--syntax-bg: #1e1e1e;
|
||||||
--brand: oklch(0.707 0.165 254.624);
|
--brand: oklch(0.546 0.245 262.881);
|
||||||
--highlight: oklch(0.852 0.199 91.936);
|
--highlight: oklch(0.852 0.199 91.936);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import type {
|
||||||
ConnectionCreateRequest,
|
ConnectionCreateRequest,
|
||||||
ConnectionRead,
|
ConnectionRead,
|
||||||
ConnectionUpdateRequest,
|
ConnectionUpdateRequest,
|
||||||
ModelCreateRequest,
|
|
||||||
ModelPreviewRead,
|
ModelPreviewRead,
|
||||||
ModelRead,
|
ModelRead,
|
||||||
ModelRoles,
|
ModelRoles,
|
||||||
|
|
@ -117,13 +116,7 @@ export const verifyModelConnectionMutationAtom = atomWithMutation((get) => {
|
||||||
if (result.ok) {
|
if (result.ok) {
|
||||||
toast.success("Connection verified");
|
toast.success("Connection verified");
|
||||||
} else {
|
} else {
|
||||||
// Non-fatal: many providers lack a /models endpoint yet still serve
|
toast.warning(result.message || "Couldn't verify this connection.");
|
||||||
// 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."
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
invalidateModelConnections(workspaceId);
|
invalidateModelConnections(workspaceId);
|
||||||
},
|
},
|
||||||
|
|
@ -170,20 +163,6 @@ export const testPreviewModelMutationAtom = atomWithMutation(() => {
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const addManualModelMutationAtom = atomWithMutation((get) => {
|
|
||||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
|
||||||
return {
|
|
||||||
mutationKey: ["models", "add-manual"],
|
|
||||||
mutationFn: ({ connectionId, data }: { connectionId: number; data: ModelCreateRequest }) =>
|
|
||||||
modelConnectionsApiService.addManualModel(connectionId, data),
|
|
||||||
onSuccess: () => {
|
|
||||||
toast.success("Model added");
|
|
||||||
invalidateModelConnections(workspaceId);
|
|
||||||
},
|
|
||||||
onError: (error: Error) => toast.error(error.message || "Failed to add model"),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const updateModelMutationAtom = atomWithMutation((get) => {
|
export const updateModelMutationAtom = atomWithMutation((get) => {
|
||||||
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
const workspaceId = Number(get(activeWorkspaceIdAtom));
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -113,7 +113,7 @@ export function BedrockConnectForm({ onDraftChange }: ProviderConnectFormProps)
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Add Bedrock model IDs from the provider's settings after connecting.
|
After entering credentials, refresh models to discover the Bedrock catalog for this region.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { useAtomValue } from "jotai";
|
||||||
import { Eye, EyeOff, Settings } from "lucide-react";
|
import { Eye, EyeOff, Settings } from "lucide-react";
|
||||||
import { useMemo, useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
import {
|
import {
|
||||||
addManualModelMutationAtom,
|
|
||||||
bulkUpdateModelsMutationAtom,
|
bulkUpdateModelsMutationAtom,
|
||||||
discoverConnectionModelsMutationAtom,
|
discoverConnectionModelsMutationAtom,
|
||||||
testPreviewModelMutationAtom,
|
testPreviewModelMutationAtom,
|
||||||
|
|
@ -50,26 +49,17 @@ export function ConnectionSettingsDialog({
|
||||||
const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom);
|
const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom);
|
||||||
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
|
const testPreviewModel = useAtomValue(testPreviewModelMutationAtom);
|
||||||
const updateConnection = useAtomValue(updateModelConnectionMutationAtom);
|
const updateConnection = useAtomValue(updateModelConnectionMutationAtom);
|
||||||
const addManualModel = useAtomValue(addManualModelMutationAtom);
|
|
||||||
const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom);
|
const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom);
|
||||||
|
|
||||||
const allowlist = Array.isArray(connection.extra?.model_ids)
|
|
||||||
? (connection.extra.model_ids as string[])
|
|
||||||
: [];
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const [baseUrlDraft, setBaseUrlDraft] = useState(connection.base_url ?? "");
|
const [baseUrlDraft, setBaseUrlDraft] = useState(connection.base_url ?? "");
|
||||||
const [apiKeyDraft, setApiKeyDraft] = useState("");
|
const [apiKeyDraft, setApiKeyDraft] = useState("");
|
||||||
const [showApiKey, setShowApiKey] = useState(false);
|
const [showApiKey, setShowApiKey] = useState(false);
|
||||||
const [allowlistText, setAllowlistText] = useState(allowlist.join(", "));
|
|
||||||
const [isSavingConnectionSettings, setIsSavingConnectionSettings] = useState(false);
|
const [isSavingConnectionSettings, setIsSavingConnectionSettings] = useState(false);
|
||||||
const [draftEnabledModelIds, setDraftEnabledModelIds] = useState(() =>
|
const [draftEnabledModelIds, setDraftEnabledModelIds] = useState(() =>
|
||||||
enabledModelIds(connection.models)
|
enabledModelIds(connection.models)
|
||||||
);
|
);
|
||||||
|
|
||||||
const isLocal =
|
|
||||||
connection.provider === "ollama_chat" ||
|
|
||||||
connection.provider === "lm_studio" ||
|
|
||||||
!connection.base_url?.startsWith("https");
|
|
||||||
const hasConnectionChanges =
|
const hasConnectionChanges =
|
||||||
baseUrlDraft.trim() !== (connection.base_url ?? "") ||
|
baseUrlDraft.trim() !== (connection.base_url ?? "") ||
|
||||||
apiKeyDraft.trim() !== (connection.api_key ?? "");
|
apiKeyDraft.trim() !== (connection.api_key ?? "");
|
||||||
|
|
@ -93,7 +83,6 @@ export function ConnectionSettingsDialog({
|
||||||
setBaseUrlDraft(connection.base_url ?? "");
|
setBaseUrlDraft(connection.base_url ?? "");
|
||||||
setApiKeyDraft(connection.api_key ?? "");
|
setApiKeyDraft(connection.api_key ?? "");
|
||||||
setShowApiKey(false);
|
setShowApiKey(false);
|
||||||
setAllowlistText(allowlist.join(", "));
|
|
||||||
setIsSavingConnectionSettings(false);
|
setIsSavingConnectionSettings(false);
|
||||||
setDraftEnabledModelIds(enabledModelIds(connection.models));
|
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) {
|
function handleToggleModel(model: SelectableModel, enabled: boolean) {
|
||||||
if (typeof model.id !== "number") return;
|
if (typeof model.id !== "number") return;
|
||||||
const modelId = model.id;
|
const modelId = model.id;
|
||||||
|
|
@ -276,41 +254,15 @@ export function ConnectionSettingsDialog({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{!isLocal ? (
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label className="text-xs">Model IDs filter (optional)</Label>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
value={allowlistText}
|
|
||||||
onChange={(event) => setAllowlistText(event.target.value)}
|
|
||||||
placeholder="Comma-separated, e.g. anthropic/claude-sonnet-4-5, google/gemini-2.5-pro"
|
|
||||||
/>
|
|
||||||
<Button size="sm" onClick={saveAllowlist} disabled={updateConnection.isPending}>
|
|
||||||
Save filter
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<p className="text-xs text-muted-foreground">
|
|
||||||
Leave empty to discover all models. Recommended for providers with large catalogs.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<Separator className="bg-muted-foreground/20" />
|
<Separator className="bg-muted-foreground/20" />
|
||||||
|
|
||||||
<ModelsSelectionPanel
|
<ModelsSelectionPanel
|
||||||
models={draftModels}
|
models={draftModels}
|
||||||
isRefreshing={discoverModels.isPending}
|
isRefreshing={discoverModels.isPending}
|
||||||
isAddingManual={addManualModel.isPending}
|
|
||||||
isUpdatingModel={isSavingConnectionSettings}
|
isUpdatingModel={isSavingConnectionSettings}
|
||||||
isBulkUpdating={isSavingConnectionSettings || bulkUpdateModels.isPending}
|
isBulkUpdating={isSavingConnectionSettings || bulkUpdateModels.isPending}
|
||||||
refreshLabel={`Refresh ${providerLabel} models`}
|
refreshLabel={`Refresh ${providerLabel} models`}
|
||||||
onRefresh={() => discoverModels.mutate(connection.id)}
|
onRefresh={() => discoverModels.mutate(connection.id)}
|
||||||
onAddManual={(modelId) =>
|
|
||||||
addManualModel.mutate({
|
|
||||||
connectionId: connection.id,
|
|
||||||
data: { model_id: modelId },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
onToggleModel={handleToggleModel}
|
onToggleModel={handleToggleModel}
|
||||||
onBulkToggle={handleBulkToggle}
|
onBulkToggle={handleBulkToggle}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -216,22 +216,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) {
|
function toggleConnectModel(model: SelectableModel, enabled: boolean) {
|
||||||
setConnectModels((current) =>
|
setConnectModels((current) =>
|
||||||
current.map((item) => (item.model_id === model.model_id ? { ...item, enabled } : item))
|
current.map((item) => (item.model_id === model.model_id ? { ...item, enabled } : item))
|
||||||
|
|
@ -292,7 +276,6 @@ export function ModelProviderConnectionsPanel({
|
||||||
previewModels={connectModels}
|
previewModels={connectModels}
|
||||||
isPreviewingModels={previewModels.isPending}
|
isPreviewingModels={previewModels.isPending}
|
||||||
onPreviewModels={refreshConnectModels}
|
onPreviewModels={refreshConnectModels}
|
||||||
onAddPreviewModel={addConnectModel}
|
|
||||||
onTogglePreviewModel={toggleConnectModel}
|
onTogglePreviewModel={toggleConnectModel}
|
||||||
onBulkTogglePreviewModels={bulkToggleConnectModels}
|
onBulkTogglePreviewModels={bulkToggleConnectModels}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
import { RefreshCw } from "lucide-react";
|
import { RefreshCw } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Badge } from "@/components/ui/badge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Checkbox } from "@/components/ui/checkbox";
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Spinner } from "@/components/ui/spinner";
|
import { Spinner } from "@/components/ui/spinner";
|
||||||
import {
|
import {
|
||||||
capability,
|
capability,
|
||||||
|
|
@ -18,14 +16,12 @@ interface ModelsSelectionPanelProps {
|
||||||
models: SelectableModel[];
|
models: SelectableModel[];
|
||||||
description?: string;
|
description?: string;
|
||||||
emptyMessage?: string;
|
emptyMessage?: string;
|
||||||
manualInputPlaceholder?: string;
|
|
||||||
refreshLabel?: string;
|
refreshLabel?: string;
|
||||||
isRefreshing?: boolean;
|
isRefreshing?: boolean;
|
||||||
isAddingManual?: boolean;
|
isRefreshDisabled?: boolean;
|
||||||
isUpdatingModel?: boolean;
|
isUpdatingModel?: boolean;
|
||||||
isBulkUpdating?: boolean;
|
isBulkUpdating?: boolean;
|
||||||
onRefresh?: () => void;
|
onRefresh?: () => void;
|
||||||
onAddManual?: (modelId: string) => void;
|
|
||||||
onToggleModel?: (model: SelectableModel, enabled: boolean) => void;
|
onToggleModel?: (model: SelectableModel, enabled: boolean) => void;
|
||||||
onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void;
|
onBulkToggle?: (models: SelectableModel[], enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -34,18 +30,15 @@ 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 available.",
|
emptyMessage = "No models available.",
|
||||||
manualInputPlaceholder = "Add a model ID manually",
|
|
||||||
refreshLabel = "Refresh models",
|
refreshLabel = "Refresh models",
|
||||||
isRefreshing = false,
|
isRefreshing = false,
|
||||||
isAddingManual = false,
|
isRefreshDisabled = false,
|
||||||
isUpdatingModel = false,
|
isUpdatingModel = false,
|
||||||
isBulkUpdating = false,
|
isBulkUpdating = false,
|
||||||
onRefresh,
|
onRefresh,
|
||||||
onAddManual,
|
|
||||||
onToggleModel,
|
onToggleModel,
|
||||||
onBulkToggle,
|
onBulkToggle,
|
||||||
}: ModelsSelectionPanelProps) {
|
}: ModelsSelectionPanelProps) {
|
||||||
const [manualModelId, setManualModelId] = useState("");
|
|
||||||
const [modelFilter, setModelFilter] = useState<ModelCapabilityFilter | null>(null);
|
const [modelFilter, setModelFilter] = useState<ModelCapabilityFilter | null>(null);
|
||||||
|
|
||||||
const filteredModels = modelFilter
|
const filteredModels = modelFilter
|
||||||
|
|
@ -54,13 +47,6 @@ export function ModelsSelectionPanel({
|
||||||
const allFilteredModelsEnabled =
|
const allFilteredModelsEnabled =
|
||||||
filteredModels.length > 0 && filteredModels.every((model) => model.enabled);
|
filteredModels.length > 0 && filteredModels.every((model) => model.enabled);
|
||||||
|
|
||||||
function addModel() {
|
|
||||||
const modelId = manualModelId.trim();
|
|
||||||
if (!modelId || !onAddManual) return;
|
|
||||||
onAddManual(modelId);
|
|
||||||
setManualModelId("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleFilteredModels() {
|
function toggleFilteredModels() {
|
||||||
const nextEnabled = !allFilteredModelsEnabled;
|
const nextEnabled = !allFilteredModelsEnabled;
|
||||||
const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled);
|
const changedModels = filteredModels.filter((model) => model.enabled !== nextEnabled);
|
||||||
|
|
@ -91,7 +77,7 @@ export function ModelsSelectionPanel({
|
||||||
size="icon"
|
size="icon"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onRefresh}
|
onClick={onRefresh}
|
||||||
disabled={isRefreshing}
|
disabled={isRefreshing || isRefreshDisabled}
|
||||||
aria-label={refreshLabel}
|
aria-label={refreshLabel}
|
||||||
>
|
>
|
||||||
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
<RefreshCw className={`h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`} />
|
||||||
|
|
@ -100,32 +86,6 @@ export function ModelsSelectionPanel({
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{onAddManual ? (
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<Input
|
|
||||||
value={manualModelId}
|
|
||||||
onChange={(event) => setManualModelId(event.target.value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
event.preventDefault();
|
|
||||||
addModel();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={manualInputPlaceholder}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
type="button"
|
|
||||||
onClick={addModel}
|
|
||||||
disabled={isAddingManual || !manualModelId.trim()}
|
|
||||||
className="relative min-w-[88px]"
|
|
||||||
>
|
|
||||||
<span className={isAddingManual ? "opacity-0" : ""}>Add model</span>
|
|
||||||
{isAddingManual ? <Spinner size="xs" className="absolute" /> : null}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{models.length > 0 ? (
|
{models.length > 0 ? (
|
||||||
<div className="flex flex-wrap items-center gap-2">
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
<span className="text-xs font-medium text-muted-foreground">Filter models</span>
|
<span className="text-xs font-medium text-muted-foreground">Filter models</span>
|
||||||
|
|
@ -139,21 +99,44 @@ export function ModelsSelectionPanel({
|
||||||
type="button"
|
type="button"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
size="sm"
|
size="sm"
|
||||||
className={`h-7 rounded-full px-3 text-xs ${isActive ? "" : "opacity-80"}`}
|
className={`h-7 rounded-full px-3 text-xs ${
|
||||||
|
isActive
|
||||||
|
? "bg-brand text-white hover:bg-brand/90"
|
||||||
|
: "opacity-80"
|
||||||
|
}`}
|
||||||
onClick={() => setModelFilter(isActive ? null : filter.key)}
|
onClick={() => setModelFilter(isActive ? null : filter.key)}
|
||||||
>
|
>
|
||||||
{filter.label}
|
{filter.label}
|
||||||
<span className="ml-1 text-muted-foreground">{count}</span>
|
<span className={`ml-1 ${isActive ? "text-white/80" : "text-muted-foreground"}`}>
|
||||||
|
{count}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<div className="h-80 overflow-y-auto rounded-xl border bg-muted/20 p-2">
|
<div
|
||||||
|
className={`overflow-y-auto rounded-xl border bg-muted/20 p-2 ${
|
||||||
|
models.length === 0 ? "h-auto border-dashed border-border/100" : "h-80"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{models.length === 0 ? (
|
{models.length === 0 ? (
|
||||||
<div className="rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
<div className="flex flex-col items-center gap-3 rounded-lg px-3 py-6 text-center text-sm text-muted-foreground">
|
||||||
{emptyMessage}
|
{emptyMessage}
|
||||||
|
{onRefresh ? (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
onClick={onRefresh}
|
||||||
|
disabled={isRefreshing || isRefreshDisabled}
|
||||||
|
className="relative"
|
||||||
|
>
|
||||||
|
<span className={isRefreshing ? "opacity-0" : ""}>Reload models</span>
|
||||||
|
{isRefreshing ? <Spinner size="sm" className="absolute" /> : null}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{filteredModels.length === 0 && modelFilter ? (
|
{filteredModels.length === 0 && modelFilter ? (
|
||||||
|
|
@ -169,21 +152,17 @@ export function ModelsSelectionPanel({
|
||||||
{filteredModels.map((model) => (
|
{filteredModels.map((model) => (
|
||||||
<div
|
<div
|
||||||
key={model.id ?? model.model_id}
|
key={model.id ?? model.model_id}
|
||||||
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-background"
|
className="flex items-center gap-3 rounded-lg px-3 py-2 transition-colors hover:bg-popover"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked={model.enabled}
|
checked={model.enabled}
|
||||||
onCheckedChange={(checked) => onToggleModel?.(model, checked === true)}
|
onCheckedChange={(checked) => onToggleModel?.(model, checked === true)}
|
||||||
disabled={!onToggleModel || isUpdatingModel}
|
disabled={!onToggleModel || isUpdatingModel}
|
||||||
|
className="border-muted-foreground/20"
|
||||||
/>
|
/>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<div className="flex items-center gap-2 text-sm font-medium">
|
<div className="flex items-center gap-2 text-sm font-medium">
|
||||||
<span className="truncate">{modelLabel(model)}</span>
|
<span className="truncate">{modelLabel(model)}</span>
|
||||||
{model.source === "MANUAL" ? (
|
|
||||||
<Badge variant="outline" className="text-[10px]">
|
|
||||||
manual
|
|
||||||
</Badge>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-muted-foreground">
|
<div className="text-xs text-muted-foreground">
|
||||||
{capabilityLabels(model) || "No discovered capabilities"}
|
{capabilityLabels(model) || "No discovered capabilities"}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ interface ProviderConnectDialogProps {
|
||||||
previewModels?: SelectableModel[];
|
previewModels?: SelectableModel[];
|
||||||
isPreviewingModels?: boolean;
|
isPreviewingModels?: boolean;
|
||||||
onPreviewModels?: (draft: ConnectionDraft) => void;
|
onPreviewModels?: (draft: ConnectionDraft) => void;
|
||||||
onAddPreviewModel?: (modelId: string) => void;
|
|
||||||
onTogglePreviewModel?: (model: SelectableModel, enabled: boolean) => void;
|
onTogglePreviewModel?: (model: SelectableModel, enabled: boolean) => void;
|
||||||
onBulkTogglePreviewModels?: (models: SelectableModel[], enabled: boolean) => void;
|
onBulkTogglePreviewModels?: (models: SelectableModel[], enabled: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -53,7 +52,6 @@ export function ProviderConnectDialog({
|
||||||
previewModels = [],
|
previewModels = [],
|
||||||
isPreviewingModels = false,
|
isPreviewingModels = false,
|
||||||
onPreviewModels,
|
onPreviewModels,
|
||||||
onAddPreviewModel,
|
|
||||||
onTogglePreviewModel,
|
onTogglePreviewModel,
|
||||||
onBulkTogglePreviewModels,
|
onBulkTogglePreviewModels,
|
||||||
}: ProviderConnectDialogProps) {
|
}: ProviderConnectDialogProps) {
|
||||||
|
|
@ -94,7 +92,8 @@ export function ProviderConnectDialog({
|
||||||
return "Select the models to enable for this provider";
|
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 =
|
const hasEnabledModel =
|
||||||
previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId);
|
previewModels.some((model) => model.enabled) || Boolean(currentDraft.seedModelId);
|
||||||
const canConnect = canSubmit && hasEnabledModel;
|
const canConnect = canSubmit && hasEnabledModel;
|
||||||
|
|
@ -130,18 +129,22 @@ export function ProviderConnectDialog({
|
||||||
<DefaultConnectForm {...formProps} />
|
<DefaultConnectForm {...formProps} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator className="bg-muted-foreground/20" />
|
{!isAzure ? (
|
||||||
|
<>
|
||||||
|
<Separator className="bg-muted-foreground/20" />
|
||||||
|
|
||||||
<ModelsSelectionPanel
|
<ModelsSelectionPanel
|
||||||
models={previewModels}
|
models={previewModels}
|
||||||
description={modelDescription}
|
description={modelDescription}
|
||||||
isRefreshing={isPreviewingModels}
|
isRefreshing={isPreviewingModels}
|
||||||
refreshLabel={`Refresh ${meta.name} models`}
|
isRefreshDisabled={!canRefreshModels}
|
||||||
onRefresh={canRefreshModels ? () => onPreviewModels?.(currentDraft) : undefined}
|
refreshLabel={`Refresh ${meta.name} models`}
|
||||||
onAddManual={onAddPreviewModel}
|
onRefresh={supportsModelRefresh ? () => onPreviewModels?.(currentDraft) : undefined}
|
||||||
onToggleModel={onTogglePreviewModel}
|
onToggleModel={onTogglePreviewModel}
|
||||||
onBulkToggle={onBulkTogglePreviewModels}
|
onBulkToggle={onBulkTogglePreviewModels}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<ConnectFormFooter
|
<ConnectFormFooter
|
||||||
onCancel={() => onOpenChange(false)}
|
onCancel={() => onOpenChange(false)}
|
||||||
|
|
|
||||||
|
|
@ -111,7 +111,7 @@ export function VertexConnectForm({ onDraftChange }: ProviderConnectFormProps) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
Add Vertex AI model IDs from the provider's settings after connecting.
|
SurfSense will show supported Vertex AI models from the provider catalog.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -74,11 +74,6 @@ export const connectionUpdateRequest = z.object({
|
||||||
enabled: z.boolean().optional(),
|
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({
|
export const modelUpdateRequest = z.object({
|
||||||
display_name: z.string().nullable().optional(),
|
display_name: z.string().nullable().optional(),
|
||||||
enabled: z.boolean().optional(),
|
enabled: z.boolean().optional(),
|
||||||
|
|
@ -142,7 +137,6 @@ export type ConnectionRead = z.infer<typeof connectionRead>;
|
||||||
export type ConnectionCreateRequest = z.infer<typeof connectionCreateRequest>;
|
export type ConnectionCreateRequest = z.infer<typeof connectionCreateRequest>;
|
||||||
export type ModelTestPreviewRequest = z.infer<typeof modelTestPreviewRequest>;
|
export type ModelTestPreviewRequest = z.infer<typeof modelTestPreviewRequest>;
|
||||||
export type ConnectionUpdateRequest = z.infer<typeof connectionUpdateRequest>;
|
export type ConnectionUpdateRequest = z.infer<typeof connectionUpdateRequest>;
|
||||||
export type ModelCreateRequest = z.infer<typeof modelCreateRequest>;
|
|
||||||
export type ModelUpdateRequest = z.infer<typeof modelUpdateRequest>;
|
export type ModelUpdateRequest = z.infer<typeof modelUpdateRequest>;
|
||||||
export type ModelsBulkUpdateRequest = z.infer<typeof modelsBulkUpdateRequest>;
|
export type ModelsBulkUpdateRequest = z.infer<typeof modelsBulkUpdateRequest>;
|
||||||
export type ModelRoles = z.infer<typeof modelRoles>;
|
export type ModelRoles = z.infer<typeof modelRoles>;
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,6 @@ import {
|
||||||
globalLlmConfigStatus,
|
globalLlmConfigStatus,
|
||||||
type LlmSetupStatus,
|
type LlmSetupStatus,
|
||||||
llmSetupStatus,
|
llmSetupStatus,
|
||||||
type ModelCreateRequest,
|
|
||||||
type ModelPreviewRead,
|
type ModelPreviewRead,
|
||||||
type ModelProviderRead,
|
type ModelProviderRead,
|
||||||
type ModelRead,
|
type ModelRead,
|
||||||
|
|
@ -18,7 +17,6 @@ import {
|
||||||
type ModelsBulkUpdateRequest,
|
type ModelsBulkUpdateRequest,
|
||||||
type ModelTestPreviewRequest,
|
type ModelTestPreviewRequest,
|
||||||
type ModelUpdateRequest,
|
type ModelUpdateRequest,
|
||||||
modelCreateRequest,
|
|
||||||
modelListResponse,
|
modelListResponse,
|
||||||
modelPreviewListResponse,
|
modelPreviewListResponse,
|
||||||
modelProviderListResponse,
|
modelProviderListResponse,
|
||||||
|
|
@ -139,19 +137,6 @@ class ModelConnectionsApiService {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
addManualModel = async (
|
|
||||||
connectionId: number,
|
|
||||||
request: ModelCreateRequest
|
|
||||||
): Promise<ModelRead> => {
|
|
||||||
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<ModelRead> => {
|
updateModel = async (id: number, request: ModelUpdateRequest): Promise<ModelRead> => {
|
||||||
const parsed = modelUpdateRequest.safeParse(request);
|
const parsed = modelUpdateRequest.safeParse(request);
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue