"use client"; import { useAtom, useAtomValue } from "jotai"; import { CheckCircle2, PlugZap, Plus, RefreshCcw, XCircle } from "lucide-react"; import { useMemo, useState } from "react"; import { addManualModelMutationAtom, createModelConnectionMutationAtom, discoverConnectionModelsMutationAtom, testModelMutationAtom, updateModelConnectionMutationAtom, 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: "custom", label: "OpenAI-compatible (any URL)", protocol: "OPENAI_COMPATIBLE" }, { 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, }, ]; // Free-text URL hints (datalist), mirroring OpenWebUI. These never restrict // what the user can type — any OpenAI-compatible endpoint works. const URL_SUGGESTIONS = [ "https://api.openai.com/v1", "https://api.anthropic.com/v1", "https://openrouter.ai/api/v1", "https://generativelanguage.googleapis.com/v1beta/openai", "https://api.groq.com/openai/v1", "https://api.mistral.ai/v1", "https://api.deepseek.com/v1", "https://api.x.ai/v1", "http://host.docker.internal:11434", "http://host.docker.internal:1234/v1", "http://host.docker.internal:8000/v1", ]; 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, })) ); } function ConnectionCard({ connection }: { connection: ConnectionRead }) { const verifyConnection = useAtomValue(verifyModelConnectionMutationAtom); const discoverModels = useAtomValue(discoverConnectionModelsMutationAtom); const updateConnection = useAtomValue(updateModelConnectionMutationAtom); const addManualModel = useAtomValue(addManualModelMutationAtom); const updateModel = useAtomValue(updateModelMutationAtom); const testModel = useAtomValue(testModelMutationAtom); const allowlist = Array.isArray(connection.extra?.model_ids) ? (connection.extra.model_ids as string[]) : []; const [allowlistText, setAllowlistText] = useState(allowlist.join(", ")); const [manualModelId, setManualModelId] = useState(""); const providerLabel = connection.native_provider || connection.protocol; const isLocal = connection.protocol === "OLLAMA" || !connection.base_url?.startsWith("https"); 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 addModel() { const modelId = manualModelId.trim(); if (!modelId) return; addManualModel.mutate( { connectionId: connection.id, data: { model_id: modelId } }, { onSuccess: () => setManualModelId("") } ); } return (
{getProviderIcon(providerLabel, { className: "size-4" })} {providerLabel}
{connection.base_url || "Provider default endpoint"}
{connection.last_status && connection.last_status !== "OK" ? (

{connection.last_error || "Could not list models."} Chat may still work — add model IDs manually below.

) : null} {!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 (e.g. OpenRouter).

) : null}
setManualModelId(event.target.value)} onKeyDown={(event) => { if (event.key === "Enter") { event.preventDefault(); addModel(); } }} placeholder="Add a model ID manually (for providers without /models)" />
{connection.models.map((model) => (
{getProviderIcon(providerLabel, { className: "size-4" })} {modelLabel(model)} {model.source === "MANUAL" ? ( manual ) : null}
{["chat", "vision", "image_gen"] .filter((key) => Boolean(model.capabilities?.[key])) .join(", ") || "No verified capabilities"}
))}
); } 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 updateRoles = useAtomValue(updateModelRolesMutationAtom); const visiblePresets = useMemo( () => PRESETS.filter((preset) => !(isCloud() && preset.local)), [] ); const [presetId, setPresetId] = useState(visiblePresets[0]?.id ?? "custom"); const preset = visiblePresets.find((item) => item.id === presetId) ?? visiblePresets[0]; const [baseUrl, setBaseUrl] = useState(preset?.baseUrl ?? ""); const [apiKey, setApiKey] = useState(""); // Native providers carry their endpoint inside LiteLLM, so Base URL is hidden // by default and only revealed for power users who want to override it. const [showCustomEndpoint, setShowCustomEndpoint] = useState(false); const isNative = preset?.protocol === "NATIVE"; const requiresUrl = !isNative; 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); // Native providers use LiteLLM's built-in endpoint; everything else needs // (and may prefill) a Base URL. setBaseUrl(next?.protocol === "NATIVE" ? "" : (next?.baseUrl ?? "")); setShowCustomEndpoint(false); } 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, }, { onSuccess: () => setApiKey("") } ); } 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.
{isNative && !showCustomEndpoint ? (
Uses provider default
) : ( <> setBaseUrl(event.target.value)} placeholder="https://api.example.com/v1" list="model-conn-url-suggestions" /> {URL_SUGGESTIONS.map((url) => ( )}
setApiKey(event.target.value)} placeholder={preset?.local ? "Optional for local models" : "API key"} type="password" />
{preset?.local ? (

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

) : isNative ? (

Just paste an API key — {preset?.label} routes through its native endpoint automatically. After adding, hit Discover (or add model IDs manually).

) : preset?.protocol === "OPENAI_COMPATIBLE" ? (

Enter any OpenAI-compatible endpoint (OpenRouter, Together, Groq, vLLM, LM Studio…). After adding, hit Discover to list models.

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