"use client"; import { useAtom, useAtomValue } from "jotai"; import { CheckCircle2, PlugZap, RefreshCcw, XCircle } from "lucide-react"; import { useMemo, useState } from "react"; import { createModelConnectionMutationAtom, discoverConnectionModelsMutationAtom, testModelMutationAtom, updateModelMutationAtom, updateModelRolesMutationAtom, verifyModelConnectionMutationAtom, } from "@/atoms/model-connections/model-connections-mutation.atoms"; import { globalModelConnectionsAtom, modelConnectionsAtom, modelRolesAtom, } from "@/atoms/model-connections/model-connections-query.atoms"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import type { ConnectionProtocol, ConnectionRead, ModelRead, } from "@/contracts/types/model-connections.types"; import { isCloud } from "@/lib/env-config"; import { getProviderIcon } from "@/lib/provider-icons"; type Preset = { id: string; label: string; protocol: ConnectionProtocol; nativeProvider?: string; baseUrl?: string; local?: boolean; }; const PRESETS: Preset[] = [ { id: "openai", label: "OpenAI", protocol: "NATIVE", nativeProvider: "OPENAI" }, { id: "anthropic", label: "Anthropic", protocol: "NATIVE", nativeProvider: "ANTHROPIC" }, { id: "openrouter", label: "OpenRouter", protocol: "NATIVE", nativeProvider: "OPENROUTER" }, { id: "ollama", label: "Ollama", protocol: "OLLAMA", baseUrl: "http://host.docker.internal:11434", local: true, }, { id: "lmstudio", label: "LM Studio", protocol: "OPENAI_COMPATIBLE", baseUrl: "http://host.docker.internal:1234/v1", local: true, }, { id: "llamacpp", label: "llama.cpp", protocol: "OPENAI_COMPATIBLE", baseUrl: "http://host.docker.internal:8080/v1", local: true, }, { id: "localai", label: "LocalAI", protocol: "OPENAI_COMPATIBLE", baseUrl: "http://host.docker.internal:8080/v1", local: true, }, { id: "vllm", label: "vLLM", protocol: "OPENAI_COMPATIBLE", baseUrl: "http://host.docker.internal:8000/v1", local: true, }, ]; function modelLabel(model: ModelRead) { return model.display_name || model.model_id; } function capability(model: ModelRead, key: "chat" | "vision" | "image_gen") { return Boolean(model.capabilities?.[key]); } 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: connection.native_provider || connection.protocol, connectionId: connection.id, provider: connection.native_provider || connection.protocol, })) ); } export function ModelConnectionsSettings({ searchSpaceId }: { searchSpaceId: number }) { const [{ data: globalConnections = [] }] = useAtom(globalModelConnectionsAtom); const [{ data: connections = [] }] = useAtom(modelConnectionsAtom); const [{ data: roles }] = useAtom(modelRolesAtom); const createConnection = useAtomValue(createModelConnectionMutationAtom); const verifyConnection = useAtomValue(verifyModelConnectionMutationAtom); const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom); const updateModel = useAtomValue(updateModelMutationAtom); const testModel = useAtomValue(testModelMutationAtom); const updateRoles = useAtomValue(updateModelRolesMutationAtom); const visiblePresets = useMemo( () => PRESETS.filter((preset) => !(isCloud() && preset.local)), [] ); const [presetId, setPresetId] = useState(visiblePresets[0]?.id ?? "openai"); const preset = visiblePresets.find((item) => item.id === presetId) ?? visiblePresets[0]; const [baseUrl, setBaseUrl] = useState(preset?.baseUrl ?? ""); const [apiKey, setApiKey] = useState(""); 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 onPresetChange(value: string) { setPresetId(value); const next = visiblePresets.find((item) => item.id === value); setBaseUrl(next?.baseUrl ?? ""); } function handleCreate() { if (!preset) return; createConnection.mutate({ protocol: preset.protocol, native_provider: preset.nativeProvider, base_url: baseUrl || null, api_key: apiKey || null, scope: "SEARCH_SPACE", search_space_id: searchSpaceId, extra: {}, enabled: true, }); } function renderModelOption(model: ModelRead & { connectionName: string; provider: string }) { return ( {getProviderIcon(model.provider, { className: "size-4" })} {modelLabel(model)} ยท {model.connectionName} ); } return (
Model Connections Add credentials or local endpoints once, then discover reusable models.
setBaseUrl(event.target.value)} placeholder="https://api.example.com/v1" />
setApiKey(event.target.value)} placeholder="Optional for local models" type="password" />
{preset?.local ? (

Local URLs are tested from the backend container. Use host.docker.internal instead of localhost.

) : null}
{connections.map((connection) => (
{getProviderIcon(connection.native_provider || connection.protocol, { className: "size-4", })} {connection.native_provider || connection.protocol}
{connection.base_url || "Provider default endpoint"}
{connection.last_error ? (

{connection.last_error}

) : null}
{connection.models.map((model) => (
{getProviderIcon(connection.native_provider || connection.protocol, { className: "size-4", })} {modelLabel(model)}
{["chat", "vision", "image_gen"] .filter((key) => Boolean(model.capabilities?.[key])) .join(", ") || "No verified capabilities"}
))}
))}
Model Roles Pick which enabled model powers chat, vision, and image generation for this search space.
); }