// LLM configurations panel. // // One unified surface for everything LLM-config-related: server-persisted // configurations, the per-operator "active" choice (which one the assistant // uses on the next message), and 30-day spend per row. The "active" toggle // writes to the same localStorage key @crema/llm-providers-ui reads via // loadSettings/saveSettings, so the existing assistant code picks it up // without any plumbing changes. import { useCallback, useEffect, useMemo, useState } from "react" import { Pencil, Plus, Sparkles, Star, Trash2, Upload } from "lucide-react" import { useArcadiaClient } from "@crema/arcadia-client" import { loadSettings as loadActiveSettings, saveSettings as saveActiveSettings, type LLMProvidersSettings, } from "@crema/llm-providers-ui" import { Button } from "~/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "~/components/ui/card" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "~/components/ui/dialog" import { Input } from "~/components/ui/input" import { Label } from "~/components/ui/label" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "~/components/ui/select" import { Switch } from "~/components/ui/switch" import { createConfiguration, deleteConfiguration, findSpend, formatCost, getCatalog, getUsageByModel, getUsageSummary, listConfigurations, REASONING_EFFORTS, updateConfiguration, type CatalogEntry, type LlmConfiguration, type LlmConfigurationInput, type LlmProvider, type LlmUsageSummary, type ReasoningEffort, type UsageByModelRow, } from "~/lib/arcadia/llm-configs" import { listSecrets, type Secret } from "~/lib/arcadia/secrets" const PROVIDERS: LlmProvider[] = ["openai", "anthropic", "deepseek", "qwen", "lmstudio"] const LOCAL_SETTINGS_KEY = "crema.llm-providers.settings" // Curated picks for the "Seed catalog" empty-state action — operators get // a sensible starting set instead of a blank panel and 19 manual creates. const SEED_PICKS: Array<{ name: string; provider: LlmProvider; model: string }> = [ { name: "GPT-4o mini (cheap default)", provider: "openai", model: "gpt-4o-mini" }, { name: "GPT-4o", provider: "openai", model: "gpt-4o" }, { name: "Claude Sonnet 4.6", provider: "anthropic", model: "claude-sonnet-4-6" }, { name: "Claude Haiku 4.5", provider: "anthropic", model: "claude-haiku-4-5" }, { name: "DeepSeek V4 Flash", provider: "deepseek", model: "deepseek-v4-flash" }, { name: "LM Studio (local)", provider: "lmstudio", model: "local-model" }, ] export function LlmConfigurationsPanel() { const arcadia = useArcadiaClient() const [configs, setConfigs] = useState([]) const [catalog, setCatalog] = useState([]) const [usage, setUsage] = useState(null) const [usageByModel, setUsageByModel] = useState([]) const [secrets, setSecrets] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [active, setActive] = useState(null) const [editing, setEditing] = useState(null) const refresh = useCallback(async () => { setError(null) try { const [list, cat, sum, byModel, secs] = await Promise.all([ listConfigurations(arcadia), getCatalog(arcadia).catch(() => [] as CatalogEntry[]), getUsageSummary(arcadia, { days: 30 }).catch(() => null), getUsageByModel(arcadia, { days: 30 }).catch(() => [] as UsageByModelRow[]), listSecrets(arcadia).catch(() => [] as Secret[]), ]) setConfigs(list) setCatalog(cat) setUsage(sum) setUsageByModel(byModel) setSecrets(secs) } catch (e) { setError(e instanceof Error ? e.message : "Failed to load configurations.") } finally { setLoading(false) } }, [arcadia]) useEffect(() => { void refresh() if (typeof window !== "undefined") setActive(loadActiveSettings()) }, [refresh]) const isActive = useCallback( (c: LlmConfiguration) => !!active && active.providerId === c.provider && active.model === c.model && (active.secretName || "") === (c.secret_name || ""), [active], ) const onMakeActive = (c: LlmConfiguration) => { const current = loadActiveSettings() saveActiveSettings({ ...current, providerId: c.provider as LLMProvidersSettings["providerId"], model: c.model, baseURL: c.base_url || undefined, secretName: c.secret_name || undefined, }) setActive(loadActiveSettings()) } const onToggleEnabled = async (c: LlmConfiguration) => { setError(null) try { await updateConfiguration(arcadia, c.id, { enabled: !c.enabled }) await refresh() } catch (e) { setError(e instanceof Error ? e.message : "Update failed.") } } const onDelete = async (c: LlmConfiguration) => { setError(null) if (!window.confirm(`Delete "${c.name}"? Historical usage rows are preserved.`)) return try { await deleteConfiguration(arcadia, c.id) await refresh() } catch (e) { setError(e instanceof Error ? e.message : "Delete failed.") } } const onSave = async (input: LlmConfigurationInput, existing: LlmConfiguration | null) => { setError(null) try { if (existing) { await updateConfiguration(arcadia, existing.id, input) } else { await createConfiguration(arcadia, input) } setEditing(null) await refresh() } catch (e) { throw e instanceof Error ? e : new Error(String(e)) } } const onSeed = async () => { setError(null) try { // Seed sequentially to surface conflicts cleanly. for (const pick of SEED_PICKS) { try { await createConfiguration(arcadia, pick) } catch { // skip dupes — they're benign on a re-seed } } await refresh() } catch (e) { setError(e instanceof Error ? e.message : "Seed failed.") } } const onImportFromLocal = async () => { setError(null) const raw = typeof window !== "undefined" ? localStorage.getItem(LOCAL_SETTINGS_KEY) : null if (!raw) { setError("No local settings found to import.") return } try { const local = JSON.parse(raw) as LLMProvidersSettings if (!local.providerId || !local.model) { setError("Local settings are incomplete.") return } await createConfiguration(arcadia, { name: `Imported (${local.providerId})`, provider: local.providerId as LlmProvider, model: local.model, base_url: local.baseURL || null, secret_name: local.secretName || null, }) await refresh() } catch (e) { setError(e instanceof Error ? e.message : "Import failed.") } } const hasLocalSettings = typeof window !== "undefined" && !!localStorage.getItem(LOCAL_SETTINGS_KEY) return (
LLM configurations Server-persisted provider/model/secret/cost settings. Toggle the star to pick which one the Assistant uses on the next message.
{usage ? (
Spend (30d) {formatCost(usage.total_cost_cents ?? 0)} {(usage.total_requests ?? 0).toLocaleString()} req ·{" "} {(usage.total_tokens ?? 0).toLocaleString()} tok
) : null}
{hasLocalSettings && configs.length === 0 ? ( ) : null}
{error ? (

{error}

) : null} {loading ? (

Loading…

) : configs.length === 0 ? ( ) : (
    {configs.map((c) => ( onMakeActive(c)} onToggleEnabled={() => onToggleEnabled(c)} onEdit={() => setEditing(c)} onDelete={() => onDelete(c)} /> ))}
)}
{editing ? ( setEditing(null)} onSave={onSave} /> ) : null}
) } // --- Empty state --------------------------------------------------------- function EmptyState({ onSeed, onImport, }: { onSeed: () => void onImport: (() => void) | null }) { return (

No configurations yet

Seed a starter set from the curated catalog (GPT-4o, Claude, DeepSeek, LM Studio) and tweak from there.

{onImport ? ( ) : null}
) } // --- Single row ---------------------------------------------------------- function ConfigRow({ config: c, spend, isActive, onMakeActive, onToggleEnabled, onEdit, onDelete, }: { config: LlmConfiguration spend: UsageByModelRow | undefined isActive: boolean onMakeActive: () => void onToggleEnabled: () => void onEdit: () => void onDelete: () => void }) { return (
  • {c.name} {c.tenant_id == null ? ( platform ) : null} {!c.enabled ? ( disabled ) : null} {c.provider} · {c.model} {c.secret_name ? ( <> {" "} · secret {c.secret_name} ) : null} {formatRate(c.input_cost_per_million)}/1M in ·{" "} {formatRate(c.output_cost_per_million)}/1M out {c.reasoning_effort && c.reasoning_effort !== "off" ? ( <> {" "} · think{" "} {c.reasoning_effort} ) : null}
    {spend && spend.cost_cents > 0 ? (
    {formatCost(spend.cost_cents)} 30d · {spend.requests.toLocaleString()} req
    ) : null}
  • ) } // --- Add/Edit modal ------------------------------------------------------ function ConfigDialog({ existing, catalog, secrets, onClose, onSave, }: { existing: LlmConfiguration | null catalog: CatalogEntry[] secrets: Secret[] onClose: () => void onSave: ( input: LlmConfigurationInput, existing: LlmConfiguration | null, ) => Promise }) { const [draft, setDraft] = useState( existing ? { name: existing.name, provider: existing.provider, model: existing.model, base_url: existing.base_url, secret_name: existing.secret_name, input_cost_per_million: existing.input_cost_per_million, output_cost_per_million: existing.output_cost_per_million, enabled: existing.enabled, reasoning_effort: existing.reasoning_effort, } : emptyDraft(), ) const [saving, setSaving] = useState(false) const [err, setErr] = useState(null) const modelsForProvider = useMemo( () => catalog.filter((c) => c.provider === draft.provider && c.model !== "*"), [catalog, draft.provider], ) const onSubmit = async () => { setSaving(true) setErr(null) try { await onSave(draft, existing) } catch (e) { setErr(e instanceof Error ? e.message : "Save failed.") } finally { setSaving(false) } } const valid = draft.name.trim() !== "" && draft.model.trim() !== "" return ( !open && onClose()}> {existing ? "Edit configuration" : "New configuration"} Costs auto-fill from the curated catalog when you pick a known model. Override below if you have a negotiated rate.
    setDraft({ ...draft, name: e.target.value })} placeholder="Production GPT-4o-mini" autoFocus={!existing} /> { // Auto-fill costs when picking a catalog model. const entry = modelsForProvider.find((m) => m.model === model) setDraft({ ...draft, model, ...(entry && { input_cost_per_million: entry.input_cost_per_million, output_cost_per_million: entry.output_cost_per_million, }), }) }} /> setDraft({ ...draft, secret_name: name })} /> setDraft({ ...draft, base_url: e.target.value || null })} placeholder="leave blank for provider default" /> setDraft({ ...draft, input_cost_per_million: e.target.value === "" ? null : Number(e.target.value), }) } placeholder="0.15" /> setDraft({ ...draft, output_cost_per_million: e.target.value === "" ? null : Number(e.target.value), }) } placeholder="0.60" />
    {err ? (

    {err}

    ) : null}
    ) } // --- Model picker (Select + Custom… escape) ------------------------------- function ModelPicker({ value, models, onChange, }: { value: string models: CatalogEntry[] onChange: (model: string) => void }) { const known = useMemo(() => new Set(models.map((m) => m.model)), [models]) const isCustom = value !== "" && !known.has(value) const [customMode, setCustomMode] = useState(isCustom) useEffect(() => { if (!isCustom) setCustomMode(false) }, [models, isCustom]) if (customMode) { return (
    onChange(e.target.value)} placeholder="custom-model-id" autoFocus />
    ) } return ( ) } /** * Secret picker: Select populated from /api/v1/admin/secrets, filtered to * api_key category (LLM keys live there). Includes a "(none)" option for * keyless providers (lmstudio) and "Type a name…" for secrets that haven't * been created yet — the latter switches to free-text and the user can * type any name; the proxy will fail loudly at request time if it's wrong. */ function SecretPicker({ value, secrets, onChange, }: { value: string | null secrets: Secret[] onChange: (name: string | null) => void }) { const apiKeys = useMemo( () => secrets .filter((s) => s.category === "api_key" && s.enabled) .sort((a, b) => a.name.localeCompare(b.name)), [secrets], ) const known = useMemo(() => new Set(apiKeys.map((s) => s.name)), [apiKeys]) const isCustom = value != null && value !== "" && !known.has(value) const [customMode, setCustomMode] = useState(isCustom) useEffect(() => { if (!isCustom) setCustomMode(false) }, [secrets, isCustom]) if (customMode) { return (
    onChange(e.target.value || null)} placeholder="secret-name-not-yet-in-vault" autoFocus />
    ) } // Encode null as the empty string for the Select — Radix/base-ui can't // bind an actual null/undefined value cleanly. const NONE = "__none__" const CUSTOM = "__custom__" return ( ) } function Field({ label, children, className = "", }: { label: string children: React.ReactNode className?: string }) { return (
    {children}
    ) } function emptyDraft(): LlmConfigurationInput { return { name: "", provider: "openai", model: "", base_url: null, secret_name: null, } } function formatRate(rate: number | null): string { if (rate == null) return "—" if (rate === 0) return "free" return `$${rate.toFixed(2)}` } function reasoningHint(e: ReasoningEffort): string { switch (e) { case "off": return "no thinking" case "low": return "~2k thinking tokens" case "medium": return "~8k thinking tokens" case "high": return "~24k thinking tokens" case "max": return "~64k — slowest, most thorough" } }