"use client"; import { useAtom, useAtomValue } from "jotai"; import { CheckCircle2, Trash2, XCircle } from "lucide-react"; import { useState } from "react"; import { addManualModelMutationAtom, bulkUpdateModelsMutationAtom, createModelConnectionMutationAtom, deleteModelConnectionMutationAtom, discoverConnectionModelsMutationAtom, updateModelMutationAtom, updateModelRolesMutationAtom, } from "@/atoms/model-connections/model-connections-mutation.atoms"; import { globalModelConnectionsAtom, modelConnectionsAtom, modelProvidersAtom, modelRolesAtom, } from "@/atoms/model-connections/model-connections-query.atoms"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@/components/ui/alert-dialog"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Separator } from "@/components/ui/separator"; import type { ConnectionRead, ModelRead } from "@/contracts/types/model-connections.types"; import { ConnectionSettingsDialog } from "./model-connections/connection-settings-dialog"; import { capability, modelLabel } from "./model-connections/model-utils"; import { ProviderConnectDialog } from "./model-connections/provider-connect-dialog"; import { type ConnectionDraft, PROVIDER_ORDER, providerDisplay, providerIcon, } from "./model-connections/provider-metadata"; function StatusBadge({ connection }: { connection: ConnectionRead }) { if (connection.last_status === "OK") { return ( Healthy ); } if (connection.last_status) { return ( {connection.last_status} ); } return Not tested; } function flattenModels(connections: ConnectionRead[]) { return connections.flatMap((connection) => connection.models.map((model) => ({ ...model, connectionName: providerDisplay(connection.provider).name, connectionId: connection.id, provider: connection.provider, })) ); } function ConnectionCard({ connection }: { connection: ConnectionRead }) { const deleteConnection = useAtomValue(deleteModelConnectionMutationAtom); const providerMeta = providerDisplay(connection.provider); const providerLabel = providerMeta.name; function deleteCurrentConnection() { deleteConnection.mutate(connection.id); } return (
{providerIcon(connection.provider)} {providerLabel} {connection.scope === "GLOBAL" ? ( Default ) : null}
{connection.base_url || "Provider default endpoint"}
Delete this provider? {providerLabel} and all of its models will be removed from this search space. This cannot be undone. Cancel Delete
); } export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: number }) { const [{ data: globalConnections = [] }] = useAtom(globalModelConnectionsAtom); const [{ data: connections = [] }] = useAtom(modelConnectionsAtom); const [{ data: providers = [] }] = useAtom(modelProvidersAtom); const [{ data: roles }] = useAtom(modelRolesAtom); const createConnection = useAtomValue(createModelConnectionMutationAtom); const addManualModel = useAtomValue(addManualModelMutationAtom); const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom); const updateModel = useAtomValue(updateModelMutationAtom); const bulkUpdateModels = useAtomValue(bulkUpdateModelsMutationAtom); const updateRoles = useAtomValue(updateModelRolesMutationAtom); const [isAddProviderOpen, setIsAddProviderOpen] = useState(false); const [provider, setProvider] = useState("openai_compatible"); const [connectedConnection, setConnectedConnection] = useState(null); const [connectModels, setConnectModels] = useState([]); const selectedProvider = providers.find((item) => item.provider === provider); const sortedProviders = [...providers].sort((left, right) => { const leftIndex = PROVIDER_ORDER.indexOf(left.provider); const rightIndex = PROVIDER_ORDER.indexOf(right.provider); if (leftIndex !== -1 || rightIndex !== -1) { return ( (leftIndex === -1 ? Number.MAX_SAFE_INTEGER : leftIndex) - (rightIndex === -1 ? Number.MAX_SAFE_INTEGER : rightIndex) ); } return providerDisplay(left.provider).name.localeCompare(providerDisplay(right.provider).name); }); const allConnections = [...globalConnections, ...connections]; const enabledModels = flattenModels(allConnections).filter((model) => model.enabled); const chatModels = enabledModels.filter((model) => capability(model, "chat")); const visionModels = enabledModels.filter((model) => capability(model, "vision")); const imageModels = enabledModels.filter((model) => capability(model, "image_gen")); function resetConnectState() { setConnectedConnection(null); setConnectModels([]); } function handleConnectOpenChange(open: boolean) { setIsAddProviderOpen(open); if (!open) { resetConnectState(); } } function replaceConnectModels(updatedModels: ModelRead[]) { setConnectModels((current) => current.map((model) => updatedModels.find((updated) => updated.id === model.id) ?? model) ); } // Each provider connect form builds its own credential payload; the backend // resolver (`to_litellm`) forwards `extra.litellm_params` straight to LiteLLM. function handleCreate(draft: ConnectionDraft) { createConnection.mutate( { provider, base_url: draft.base_url, api_key: draft.api_key, scope: "SEARCH_SPACE", search_space_id: searchSpaceId, extra: draft.extra, enabled: true, }, { onSuccess: (created) => { setConnectedConnection(created); setConnectModels([]); if (draft.seedModelId) { addManualModel.mutate( { connectionId: created.id, data: { model_id: draft.seedModelId }, }, { onSuccess: (model) => setConnectModels([model]), } ); } else { discoverModels.mutate(created.id, { onSuccess: (models) => setConnectModels(models), }); } }, } ); } function openProviderDialog(providerId: string) { resetConnectState(); setProvider(providerId); setIsAddProviderOpen(true); } function refreshConnectModels() { if (!connectedConnection) return; discoverModels.mutate(connectedConnection.id, { onSuccess: (models) => setConnectModels(models), }); } function addConnectModel(modelId: string) { if (!connectedConnection) return; addManualModel.mutate( { connectionId: connectedConnection.id, data: { model_id: modelId } }, { onSuccess: (model) => setConnectModels((current) => [...current, model]), } ); } function toggleConnectModel(model: ModelRead, enabled: boolean) { updateModel.mutate( { id: model.id, data: { enabled } }, { onSuccess: (updated) => replaceConnectModels([updated]), } ); } function bulkToggleConnectModels(models: ModelRead[], enabled: boolean) { if (!connectedConnection) return; bulkUpdateModels.mutate( { connectionId: connectedConnection.id, data: { model_ids: models.map((model) => model.id), enabled }, }, { onSuccess: replaceConnectModels, } ); } function finishConnectFlow() { setIsAddProviderOpen(false); resetConnectState(); } function renderModelOption(model: ModelRead & { connectionName: string; provider: string }) { return ( {providerIcon(model.provider)} {modelLabel(model)} ยท {model.connectionName} ); } return (

Add Provider

SurfSense supports popular providers and self-hosted model endpoints.

{sortedProviders.map((item) => { const meta = providerDisplay(item.provider); return ( ); })}
{connections.length > 0 ? (

Available Providers

{connections.map((connection) => ( ))}
) : null}
Model Roles Pick which enabled model powers chat, vision, and image generation for this search space.
); }