From 590512d9ebfbaaaaf9b57a18b844b16a3e446761 Mon Sep 17 00:00:00 2001 From: mudler-agent Date: Mon, 28 Sep 2026 13:13:31 +0200 Subject: [PATCH] feat(distributed): report worker version and show models in node inspector (#12328) * ci: bump Hugo from 0.146.3 to 0.166.0 The hugo-theme-relearn submodule was bumped to 9.1.x in #12096, which requires Hugo >= 0.165.0. The pinned 0.146.3 broke the docs site build with a template error in alias.html that could not evaluate the Locale field on langs.Language. Bump HUGO_VERSION to 0.166.0 (latest stable) to satisfy the theme minimum and resolve the alias.html template error. Assisted-by: nib:claude-sonnet-4.5 [bash] [read] [edit] * feat(distributed): report worker version and show models in node inspector Workers now send their LocalAI build version and git commit at registration. The controller stores them on BackendNode and exposes them through the existing node list/detail API responses. The node inspector side pane now fetches and renders the list of loaded models (name, state, in-flight) instead of showing only a count, matching what the node detail page already displays. --------- Co-authored-by: Ettore Di Giacinto --- core/cli/agent_worker.go | 3 ++ core/http/endpoints/localai/nodes.go | 7 ++++ core/http/react-ui/src/App.css | 7 ++++ .../src/components/nodes/NodeInspector.jsx | 38 ++++++++++++++++++- core/http/react-ui/src/pages/NodeDetail.jsx | 4 ++ core/services/nodes/registry.go | 5 +++ core/services/worker/registration.go | 3 ++ 7 files changed, 66 insertions(+), 1 deletion(-) diff --git a/core/cli/agent_worker.go b/core/cli/agent_worker.go index ced179e50..11f515c51 100644 --- a/core/cli/agent_worker.go +++ b/core/cli/agent_worker.go @@ -19,6 +19,7 @@ import ( "github.com/mudler/LocalAI/core/services/jobs" mcpRemote "github.com/mudler/LocalAI/core/services/mcp" "github.com/mudler/LocalAI/core/services/messaging" + "github.com/mudler/LocalAI/internal" "github.com/mudler/LocalAI/pkg/sanitize" "github.com/mudler/cogito" "github.com/mudler/cogito/clients" @@ -94,6 +95,8 @@ func (cmd *AgentWorkerCMD) Run(ctx *cliContext.Context) error { registrationBody := map[string]any{ "name": nodeName, "node_type": "agent", + "version": internal.Version, + "commit": internal.Commit, } if cmd.RegistrationToken != "" { registrationBody["token"] = cmd.RegistrationToken diff --git a/core/http/endpoints/localai/nodes.go b/core/http/endpoints/localai/nodes.go index 331c7203b..2036f3d6e 100644 --- a/core/http/endpoints/localai/nodes.go +++ b/core/http/endpoints/localai/nodes.go @@ -109,6 +109,11 @@ type RegisterNodeRequest struct { // VRAMBudget is the worker's operator-set VRAM cap ("80%" or "12GB"). The // registry resolves and enforces it against the raw reported VRAM. VRAMBudget string `json:"vram_budget,omitempty"` + // Version is the LocalAI build version reported by the worker at + // registration. Empty for workers registered before this field existed. + Version string `json:"version,omitempty"` + // Commit is the git commit hash the worker binary was built from. + Commit string `json:"commit,omitempty"` } // RegisterNodeEndpoint registers a new backend node. @@ -195,6 +200,8 @@ func RegisterNodeEndpoint(registry *nodes.NodeRegistry, expectedToken string, au Capability: req.Capability, MaxReplicasPerModel: maxReplicasPerModel, VRAMBudget: req.VRAMBudget, + Version: req.Version, + Commit: req.Commit, } ctx := c.Request().Context() diff --git a/core/http/react-ui/src/App.css b/core/http/react-ui/src/App.css index c6ea6e71d..5bafd472a 100644 --- a/core/http/react-ui/src/App.css +++ b/core/http/react-ui/src/App.css @@ -9965,6 +9965,13 @@ button.collapsible-header:focus-visible { .node-inspector__actions .btn { justify-content: center; min-width: 0; } .node-inspector__back { align-items: center; background: transparent; border: 0; color: var(--color-primary); cursor: pointer; display: flex; font: inherit; font-size: var(--text-xs); gap: 6px; max-width: 285px; overflow: hidden; padding: 3px 0; text-overflow: ellipsis; white-space: nowrap; } .node-inspector__back:focus-visible { border-radius: var(--radius-sm); outline: 2px solid var(--color-primary); outline-offset: 3px; } +.node-inspector__models { margin: 10px 0 0; } +.node-inspector__models > dd { margin: 0; } +.node-inspector__model-list { display: grid; gap: 4px; list-style: none; margin: 6px 0 0; padding: 0; } +.node-inspector__model-row { align-items: center; display: flex; flex-wrap: wrap; gap: 6px; font-size: var(--text-xs); } +.node-inspector__model-row .cell-mono { font-family: var(--font-mono); font-size: .625rem; overflow-wrap: anywhere; } +.node-inspector__model-row .state-pill { border-radius: var(--radius-full); font-size: .5625rem; font-weight: 600; padding: 1px 7px; text-transform: capitalize; } +.node-inspector__model-row .text-muted { font-size: .5625rem; } .model-inspector__backends { margin-top: 10px; } .model-inspector__nodes { display: grid; gap: 9px; } .model-inspector__node { background: var(--color-bg-tertiary); border: 1px solid var(--color-border-subtle); border-radius: var(--radius-md); padding: 10px; } diff --git a/core/http/react-ui/src/components/nodes/NodeInspector.jsx b/core/http/react-ui/src/components/nodes/NodeInspector.jsx index c14cc9fcb..51860bf5e 100644 --- a/core/http/react-ui/src/components/nodes/NodeInspector.jsx +++ b/core/http/react-ui/src/components/nodes/NodeInspector.jsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' import StatusPill from './StatusPill' -import { formatBytes, formatCapacity, timeAgo } from './nodeStatus' +import { formatBytes, formatCapacity, timeAgo, modelStateConfig } from './nodeStatus' import { nodesApi } from '../../utils/api' import { capacityReading, nodeLifecycleAction } from '../../utils/nodeFleet' import useInspectorDrawer from './useInspectorDrawer' @@ -24,6 +24,8 @@ function ResourceBar({ label, total, available, tone }) { export default function NodeInspector({ node, open, onClose, onApprove, onDrain, onResume, onBack, backLabel }) { const [backends, setBackends] = useState(null) const [backendError, setBackendError] = useState('') + const [models, setModels] = useState(null) + const [modelError, setModelError] = useState('') const nodeId = node?.id const backRef = useRef(null) const closeRef = useRef(null) @@ -48,6 +50,19 @@ export default function NodeInspector({ node, open, onClose, onApprove, onDrain, return () => { current = false } }, [open, nodeId]) + useEffect(() => { + if (!open || !nodeId) return undefined + let current = true + setModels(null) + setModelError('') + nodesApi.getModels(nodeId).then(data => { + if (current) setModels(Array.isArray(data) ? data : []) + }).catch(error => { + if (current) setModelError(error.message || 'Unable to load models') + }) + return () => { current = false } + }, [open, nodeId]) + if (!open || !node) return null const cpuKnown = node.cpu_logical_cores > 0 && Number.isFinite(node.cpu_usage_percent) && Number.isFinite(node.cpu_load_1) const disk = capacityReading(node.total_disk, node.available_disk) @@ -74,6 +89,7 @@ export default function NodeInspector({ node, open, onClose, onApprove, onDrain,

Node

{node.address || 'No address reported'} + {node.version || '—'} {timeAgo(node.last_heartbeat)}
{Object.keys(node.labels || {}).length ? Object.entries(node.labels).map(([key, value]) => {key}={value}) : No labels}
@@ -94,6 +110,26 @@ export default function NodeInspector({ node, open, onClose, onApprove, onDrain, {backendError ? {backendError} : backends === null ? 'Loading…' : `${backends.length} backend${backends.length === 1 ? '' : 's'}`} {node.in_flight_count ?? 0} +
+
Running models
+
+ {modelError ? {modelError} + : models === null ? Loading… + : models.length === 0 ? No models loaded + :
    + {models.map(model => { + const stCfg = modelStateConfig[model.state] || modelStateConfig.idle + return ( +
  • + {model.model_name} + {model.state} + {model.in_flight ?? 0} in flight +
  • + ) + })} +
} +
+