/** * ModelExecutionSelector — the per-model "Runs on / Fallback" control. * * Drop this beside any model (Models tab, persona model picker) to bind where it * executes. "Auto" clears the binding (the routing policy decides); anything else * pins a ModelRoute. Options that can't serve the model are shown but disabled * with the reason ("Offline", "Requires 20 GB VRAM", …). * * Self-contained: fetches sources/devices/manifests and the model's current * route on mount, and persists changes via the compute admin API. */ import React, { useCallback, useEffect, useMemo, useState } from "react"; import { Loader2 } from "lucide-react"; import type { ComputeDevice, ComputeSource, Modality, ModelManifest, ModelRoute, } from "@homepilot/types"; import { computeClient } from "../../api"; import { buildTargetOptions, targetLabel, type TargetOption } from "./targets"; interface Props { modelId: string; modality?: Modality; onChange?: (route: ModelRoute | null) => void; } export default function ModelExecutionSelector({ modelId, modality = "chat", onChange }: Props) { const [sources, setSources] = useState([]); const [devices, setDevices] = useState([]); const [manifests, setManifests] = useState([]); const [route, setRoute] = useState(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const load = useCallback(async () => { setLoading(true); try { const [srcs, devs, mans, routes] = await Promise.all([ computeClient.listComputeSources(), computeClient.listComputeDevices(), computeClient.listModelManifests().catch(() => []), computeClient.listRoutes().catch(() => []), ]); setSources(srcs); setDevices(devs); setManifests(mans); setRoute(routes.find((r) => r.modelId === modelId && r.modality === modality) ?? null); setError(null); } catch (e) { setError(e instanceof Error ? e.message : "Failed to load"); } finally { setLoading(false); } }, [modelId, modality]); useEffect(() => { void load(); }, [load]); const options: TargetOption[] = useMemo( () => buildTargetOptions({ modality, modelId, sources, devices, manifests }), [modality, modelId, sources, devices, manifests], ); // Fallback list = concrete targets only (no "Auto"). const fallbackOptions = options.filter((o) => o.target !== ""); const primary = route?.primaryTarget ?? ""; const fallback = route?.fallbackTargets?.[0] ?? ""; // Live status for the currently-bound target. function statusFor(target: string): { text: string; ok: boolean | null } { if (!target) return { text: "Auto — routing policy decides", ok: null }; if (target === "local") return { text: "This PC", ok: null }; if (target.startsWith("device:")) { const d = devices.find((x) => `device:${x.id}` === target); const online = d ? (d.onlineEffective ?? d.online) : false; return { text: `${targetLabel(target, sources, devices)} ${online ? "online" : "offline"}`, ok: online }; } if (target.startsWith("source:")) { const s = sources.find((x) => `source:${x.id}` === target); return { text: `${targetLabel(target, sources, devices)} ${s?.enabled ? "enabled" : "disabled"}`, ok: !!s?.enabled }; } return { text: targetLabel(target, sources, devices), ok: null }; } const status = statusFor(primary); const persist = useCallback( async (nextPrimary: string, nextFallback: string) => { setBusy(true); setError(null); try { if (!nextPrimary) { // Auto → clear the binding. if (route) await computeClient.deleteRoute(modality, modelId); setRoute(null); onChange?.(null); } else { const saved = await computeClient.upsertRoute({ modality, modelId, primaryTarget: nextPrimary, fallbackTargets: nextFallback ? [nextFallback] : [], strategy: "pinned", privacy: "private_nodes", }); setRoute(saved); onChange?.(saved); } } catch (e) { setError(e instanceof Error ? e.message : "Save failed"); } finally { setBusy(false); } }, [route, modality, modelId, onChange], ); if (loading) return ; return (
Runs on o.target !== primary)]} onChange={(v) => void persist(primary, v)} /> {busy && }
Status: {status.text}
{error && {error}}
); } function Select(props: { value: string; options: TargetOption[]; disabled?: boolean; onChange: (v: string) => void; }) { const { value, options, disabled, onChange } = props; return ( ); }