"use client"; import * as React from "react"; import { Eye, EyeOff, Plus, Trash2, Activity, ClipboardPaste, AlertTriangle, CheckCircle2, XCircle, Info } from "lucide-react"; import { ResponsiveDialog } from "@/components/ui/sheet"; import { Button } from "@/components/ui/button"; import { Input, Field } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { Badge } from "@/components/ui/badge"; import { toast } from "@/components/ui/toast"; import { api } from "@/lib/client/api"; import { errorMessage } from "@/lib/client/humanize"; import { formatMs, cn } from "@/lib/utils"; import type { PublicEndpoint, ManualModel } from "@/lib/client/types"; import { ENDPOINT_PRESETS } from "./presets"; interface HeaderRow { key: string; value: string; } interface FormState { name: string; baseUrl: string; apiKey: string; clearKey: boolean; headers: HeaderRow[]; discover: boolean; modelsPath: string; manualModels: ManualModel[]; } const EMPTY: FormState = { name: "", baseUrl: "", apiKey: "", clearKey: false, headers: [], discover: true, modelsPath: "/models", manualModels: [] }; function fromEndpoint(e: PublicEndpoint): FormState { return { name: e.name, baseUrl: e.baseUrl, apiKey: "", clearKey: false, headers: e.headerNames.map((k) => ({ key: k, value: "" })), discover: Boolean(e.modelsPath), modelsPath: e.modelsPath || "/models", manualModels: e.manualModels.map((m) => ({ ...m })) }; } /** Client-side hint only — the server runs the authoritative SSRF check. */ export function looksPrivate(raw: string): boolean { try { const u = new URL(raw); const h = u.hostname.replace(/^\[|\]$/g, "").toLowerCase(); return h === "localhost" || h.endsWith(".localhost") || h.endsWith(".local") || h.endsWith(".internal") || h === "::1" || h.startsWith("fe80:") || h.startsWith("fc") || h.startsWith("fd") || /^(127\.|10\.|192\.168\.|169\.254\.|0\.)/.test(h) || /^172\.(1[6-9]|2\d|3[01])\./.test(h) || !h.includes("."); } catch { return false; } } export interface TestOutcome { ok: boolean; latencyMs: number; modelsAvailable: number; error?: string; models?: { id: string; displayName: string }[]; } export function EndpointSheet({ open, onOpenChange, endpoint, allowPrivate, onSaved }: { open: boolean; onOpenChange: (v: boolean) => void; endpoint: PublicEndpoint | null; allowPrivate: boolean; onSaved: (e: PublicEndpoint, test?: TestOutcome) => void }) { const editing = Boolean(endpoint); const [form, setForm] = React.useState(EMPTY); const [preset, setPreset] = React.useState("other"); const [showKey, setShowKey] = React.useState(false); const [busy, setBusy] = React.useState<"save" | "test" | null>(null); const [error, setError] = React.useState(null); const [test, setTest] = React.useState(null); const [canPaste, setCanPaste] = React.useState(false); React.useEffect(() => { if (!open) return; // eslint-disable-next-line react-hooks/set-state-in-effect setForm(endpoint ? fromEndpoint(endpoint) : EMPTY); setPreset(endpoint ? ENDPOINT_PRESETS.find((p) => endpoint.baseUrl.startsWith(p.baseUrl.replace(/\/v1$/, "")))?.id ?? "other" : "other"); setError(null); setTest(null); setShowKey(false); setCanPaste(typeof navigator !== "undefined" && Boolean(navigator.clipboard?.readText)); }, [open, endpoint]); const patch = (p: Partial) => { setForm((f) => ({ ...f, ...p })); setError(null); }; const applyPreset = (id: string) => { setPreset(id); const p = ENDPOINT_PRESETS.find((x) => x.id === id); if (!p || id === "other") return; patch({ name: form.name || p.name, baseUrl: p.baseUrl, modelsPath: p.modelsPath, discover: true }); }; const privateHost = looksPrivate(form.baseUrl); const urlOk = /^https?:\/\/\S+/i.test(form.baseUrl.trim()); const canSave = form.name.trim().length > 0 && urlOk && (form.discover ? form.modelsPath.trim().length > 0 : form.manualModels.some((m) => m.id.trim())); const payload = () => { const headers: Record = {}; for (const h of form.headers) if (h.key.trim() && h.value.trim()) headers[h.key.trim()] = h.value.trim(); const keepExistingHeaders = editing && form.headers.length > 0 && form.headers.every((h) => !h.value.trim()) && form.headers.every((h) => endpoint!.headerNames.includes(h.key.trim())); return { name: form.name.trim(), baseUrl: form.baseUrl.trim().replace(/\/+$/, ""), ...(form.clearKey ? { apiKey: null } : form.apiKey.trim() ? { apiKey: form.apiKey.trim() } : {}), ...(keepExistingHeaders ? {} : { headers: Object.keys(headers).length ? headers : null }), modelsPath: form.discover ? form.modelsPath.trim() : "", manualModels: form.manualModels.filter((m) => m.id.trim()).map((m) => ({ ...m, id: m.id.trim(), displayName: m.displayName?.trim() || undefined, contextTokens: m.contextTokens || undefined })), }; }; const save = async (andTest: boolean) => { if (!canSave) return; setBusy(andTest ? "test" : "save"); setError(null); try { if (editing) { const { endpoint: saved } = await api<{ endpoint: PublicEndpoint }>(`/api/endpoints/${endpoint!.id}`, { method: "PATCH", json: payload() }); let outcome: TestOutcome | undefined; if (andTest) { const r = await api<{ ok: boolean; latencyMs: number; models: { id: string; displayName: string }[]; error?: string; endpoint: PublicEndpoint }>(`/api/endpoints/${endpoint!.id}/sync`, { method: "POST" }); outcome = { ok: r.ok, latencyMs: r.latencyMs, modelsAvailable: r.models.length, error: r.error, models: r.models }; setTest(outcome); onSaved(r.endpoint, outcome); if (r.ok) toast.success("Endpoint reachable", `${r.models.length} models · ${formatMs(r.latencyMs)}`); else toast.error("Endpoint test failed", r.error); return; } onSaved(saved); toast.success("Endpoint saved"); onOpenChange(false); } else { const r = await api<{ endpoint: PublicEndpoint; test?: { ok: boolean; latencyMs: number; modelsAvailable: number; error?: string } }>("/api/endpoints", { method: "POST", json: { ...payload(), validate: andTest } }); if (r.test) { const outcome: TestOutcome = { ...r.test, models: r.endpoint.discoveredModels.map((m) => ({ id: m.id, displayName: m.id })) }; setTest(outcome); onSaved(r.endpoint, outcome); if (r.test.ok) toast.success(`${r.endpoint.name} connected`, `${r.test.modelsAvailable} models · ${formatMs(r.test.latencyMs)}`); else toast.warning("Saved, but the test failed", r.test.error); if (r.test.ok) onOpenChange(false); } else { onSaved(r.endpoint); toast.success("Endpoint saved", "Not tested yet."); onOpenChange(false); } } } catch (e) { setError(errorMessage(e, "Could not save the endpoint.")); } finally { setBusy(null); } }; const pasteKey = async () => { try { const t = (await navigator.clipboard.readText()).trim(); if (t) patch({ apiKey: t, clearKey: false }); } catch { toast.error("Could not read the clipboard"); } }; const presetMeta = ENDPOINT_PRESETS.find((p) => p.id === preset); return ( !busy && onOpenChange(v)} size="lg" snap="full" title={editing ? `Edit ${endpoint!.name}` : "Add custom endpoint"} description="Any server that speaks the OpenAI Chat Completions API." footer={
} >
{!editing ? (

Start from a preset

{ENDPOINT_PRESETS.map((p) => ( ))}
{presetMeta ?

{presetMeta.hint}

: null}
) : null}
patch({ name: e.target.value })} placeholder="My Ollama" maxLength={80} className="h-11 text-[16px] md:h-9 md:text-sm" /> patch({ baseUrl: e.target.value })} placeholder="http://localhost:11434/v1" inputMode="url" autoCapitalize="off" autoCorrect="off" spellCheck={false} className="h-11 font-mono text-[16px] md:h-9 md:text-sm" />
{privateHost ? (
{allowPrivate ? : }

{allowPrivate ? ( <>This server allows private addresses: the URL must be reachable from the machine running PolyLLM, not from your browser. ) : ( <> Requests are made by the PolyLLM server, not your browser. On www.polyllm.io a localhost or LAN address is unreachable and will be rejected. Expose the server through a tunnel (Cloudflare Tunnel, ngrok, Tailscale Funnel) and use that public URL, or run PolyLLM on your own machine with ALLOW_PRIVATE_ENDPOINTS=1. )}

) : null}
patch({ apiKey: e.target.value })} placeholder={editing && endpoint!.hasKey ? "Enter a new key to replace" : "Leave empty if the server needs none"} autoComplete="off" autoCapitalize="off" autoCorrect="off" spellCheck={false} className="h-11 pr-11 font-mono text-[16px] md:h-9 md:text-sm" />
{canPaste ? ( ) : null}
{editing && endpoint!.hasKey ? ( ) : null}

Custom headers

{form.headers.length === 0 ? (

None. Useful for gateways (e.g. X-Api-Key, CF-Access-Client-Id).

) : (
    {form.headers.map((h, i) => (
  • patch({ headers: form.headers.map((x, j) => (j === i ? { ...x, key: e.target.value } : x)) })} placeholder="Header" aria-label={`Header ${i + 1} name`} autoCapitalize="off" spellCheck={false} className="h-11 w-2/5 font-mono text-[16px] md:h-9 md:text-sm" /> patch({ headers: form.headers.map((x, j) => (j === i ? { ...x, value: e.target.value } : x)) })} placeholder={editing && endpoint!.headerNames.includes(h.key) && !h.value ? "•••••• (stored)" : "Value"} aria-label={`Header ${i + 1} value`} autoCapitalize="off" spellCheck={false} className="h-11 min-w-0 flex-1 font-mono text-[16px] md:h-9 md:text-sm" />
  • ))}
)} {editing && form.headers.some((h) => endpoint!.headerNames.includes(h.key) && !h.value) ?

Stored header values are never shown. Leave them blank to keep them, or type a new value to replace; editing any value re-saves all headers.

: null}

Discover models automatically

GET {form.baseUrl.trim().replace(/\/+$/, "") || "{baseUrl}"}{form.discover ? form.modelsPath || "/models" : "…"}

patch({ discover: v })} aria-label="Discover models automatically" />
{form.discover ? (
patch({ modelsPath: e.target.value })} placeholder="/models" autoCapitalize="off" spellCheck={false} className="h-11 font-mono text-[16px] md:h-9 md:text-sm" />
) : null}

Manual models {form.discover ? "(optional overrides)" : ""}

{form.discover ? "Declare capabilities for discovered ids (vision, tools, reasoning, context) or add ids the listing misses." : "Discovery is off: list every model id the server accepts."}

{form.manualModels.length ? (
    {form.manualModels.map((m, i) => { const upd = (p: Partial) => patch({ manualModels: form.manualModels.map((x, j) => (j === i ? { ...x, ...p } : x)) }); return (
  • upd({ id: e.target.value })} placeholder="model id (e.g. llama3.1:8b)" aria-label={`Model ${i + 1} id`} autoCapitalize="off" spellCheck={false} className="h-11 min-w-0 flex-1 font-mono text-[16px] md:h-9 md:text-sm" />
    upd({ displayName: e.target.value })} placeholder="Display name (optional)" aria-label={`Model ${i + 1} display name`} className="h-11 text-[16px] md:h-9 md:text-sm" /> upd({ contextTokens: e.target.value ? Number(e.target.value) : undefined })} placeholder="Context tokens (optional)" aria-label={`Model ${i + 1} context tokens`} className="h-11 text-[16px] md:h-9 md:text-sm" />
    {(["vision", "tools", "reasoning"] as const).map((cap) => ( ))}
  • ); })}
) : (

Discovered models start as text-only. Add an entry to unlock vision, tools or reasoning controls for a specific id.

)}
{test ? (

{test.ok ? : } {test.ok ? `Reachable in ${formatMs(test.latencyMs)} · ${test.modelsAvailable} model${test.modelsAvailable === 1 ? "" : "s"}` : `Failed after ${formatMs(test.latencyMs)}`}

{!test.ok && test.error ?

{test.error}

: null} {test.ok && test.models?.length ? (
    {test.models.slice(0, 30).map((m) => (
  • {m.id}
  • ))} {test.models.length > 30 ?
  • +{test.models.length - 30} more
  • : null}
) : null}
) : null} {error ?

{error}

: null}
); }